This commit is contained in:
toom1996
2026-09-20 00:44:14 +08:00
parent b04a511b26
commit 4b409b5a29
83 changed files with 12072 additions and 218 deletions

View File

@ -36,16 +36,16 @@ const fullLoginLabel = t('go to full login')
<button type="button" @click="close()" class="-mt-1 text-[11px] uppercase tracking-widest opacity-60 transition-opacity hover:opacity-100">{closeLabel}</button>
</div>
<p class="mb-7 text-[11px] uppercase tracking-widest text-zinc-400">{prompt}</p>
<p class="mb-7 text-[11px] uppercase tracking-widest text-[var(--c-gray-600)]">{prompt}</p>
<form @submit.prevent="submit()" class="space-y-6" novalidate>
<div>
<label class="mb-2 block text-[11px] uppercase tracking-widest text-zinc-500">{accountLabel}</label>
<label class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{accountLabel}</label>
<input type="text" x-model="account" autocomplete="username" required
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
</div>
<div>
<label class="mb-2 block text-[11px] uppercase tracking-widest text-zinc-500">{passwordLabel}</label>
<label class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{passwordLabel}</label>
<input type="password" x-model="password" autocomplete="current-password" required
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
</div>
@ -60,7 +60,7 @@ const fullLoginLabel = t('go to full login')
</form>
<div class="mt-6 border-t border-black/10 pt-4 text-center">
<button type="button" @click="goFullLogin()" class="text-[10px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">{fullLoginLabel}</button>
<button type="button" @click="goFullLogin()" class="text-[10px] uppercase tracking-[0.2em] text-[var(--c-gray-600)] transition-colors hover:text-black">{fullLoginLabel}</button>
</div>
</div>
</div>

View File

@ -93,13 +93,13 @@
@apply m-0 text-[12px] tracking-[0.22em] uppercase font-bold;
}
.showsmodal-x {
@apply appearance-none bg-transparent border-0 w-6 h-6 inline-flex items-center justify-center cursor-pointer text-[14px] leading-none text-[#999] transition-colors hover:text-black;
@apply appearance-none bg-transparent border-0 w-6 h-6 inline-flex items-center justify-center cursor-pointer text-[14px] leading-none text-[var(--c-gray-600)] transition-colors hover:text-black;
}
.showsmodal-search {
@apply px-4 pt-1 pb-3 shrink-0;
}
.showsmodal-input {
@apply w-full border border-black/15 px-3 py-2 text-[13px] outline-none bg-[#fafafa] focus:bg-white focus:border-black;
@apply w-full border border-black/15 px-3 py-2 text-[13px] outline-none bg-[var(--c-gray-50)] focus:bg-white focus:border-black;
}
.showsmodal-tip {
@apply mt-2 text-[11px] leading-snug text-black/45;
@ -120,10 +120,10 @@
@apply flex-1 min-h-0 overflow-y-auto px-2 py-1 overscroll-contain;
}
.showsmodal-hint {
@apply text-[10px] tracking-[0.22em] uppercase text-[#999] px-3 py-2;
@apply text-[10px] tracking-[0.22em] uppercase text-[var(--c-gray-600)] px-3 py-2;
}
.showsmodal-loading {
@apply flex justify-center py-6 text-[#bdbdbd];
@apply flex justify-center py-6 text-[var(--c-gray-500)];
}
.showsmodal-list {
@apply list-none m-0 p-0;
@ -142,10 +142,10 @@
@apply flex-1 truncate;
}
.showsmodal-empty {
@apply py-6 text-center text-[#999] text-[13px];
@apply py-6 text-center text-[var(--c-gray-600)] text-[13px];
}
.showsmodal-foot {
@apply flex items-center justify-between px-4 py-3 border-t border-black/5 shrink-0 text-[11px] text-[#888];
@apply flex items-center justify-between px-4 py-3 border-t border-black/5 shrink-0 text-[11px] text-[var(--c-gray-700)];
}
.showsmodal-ok {
@apply appearance-none bg-black text-white border border-black px-4 py-1.5 cursor-pointer text-[12px] tracking-[0.1em] transition-colors hover:bg-black/85;

View File

@ -41,7 +41,7 @@ const { locale } = getI18n(Astro)
justify-content: space-between;
border-top: 1px solid #000;
padding-top: 18px;
@apply text-[11px] tracking-[0.25em] uppercase text-[#666];
@apply text-[11px] tracking-[0.25em] uppercase text-[var(--c-gray-800)];
}
.cs-title {
@apply font-black tracking-[-0.02em];
@ -54,7 +54,7 @@ const { locale } = getI18n(Astro)
margin: 0 0 18px;
}
.cs-desc {
@apply text-[13px] text-[#666];
@apply text-[13px] text-[var(--c-gray-800)];
line-height: 1.7;
max-width: 52ch;
margin: 0 0 28px;

View File

@ -1,7 +1,7 @@
---
// 走秀 / 街拍详情页共享的全屏大图灯箱:左栏缩略图条 + 大图 + 上/下·左/右翻页 + 单图收藏。
//
// 视觉遵循全站「黑白编辑风」:直角细框、mono 小字 + 宽字距、零填充计数、hover 反色(bg-black + 白字)。
// 视觉遵循全站「黑白编辑风」:直角细框、小字 + 宽字距、零填充计数、hover 反色(bg-black + 白字)。
// 交互逻辑全部在 src/lib/gallery.ts(createGalleryView);本组件只负责标记。
//
// 各页独有的附加层通过具名插槽 details 注入(走秀页塞「当前 look 的细节图簇」,街拍页不传)。
@ -29,7 +29,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
class="fixed inset-0 z-[100] bg-white flex flex-col select-none"
aria-hidden="true"
>
{/* 计数:mono + 0.22em 字距(全站「小标与计数」档),直角细框,零填充 */}
{/* 计数:小字 + 0.22em 字距(全站「小标与计数」档),直角细框,零填充 */}
<div
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
x-text="`${String(lbIndex + 1).padStart(2, '0')} / ${String(imageCount).padStart(2, '0')}`"
@ -71,7 +71,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
data-lb-index={i}
data-thumb-url={im.thumb}
@click={`showLb(${i})`}
class={`lb-thumb relative block w-full ${thumbAspect} mb-2 overflow-hidden cursor-pointer transition-opacity duration-200 opacity-40 hover:opacity-90 bg-[#eee] bg-cover bg-center`}
class={`lb-thumb relative block w-full ${thumbAspect} mb-2 overflow-hidden cursor-pointer transition-opacity duration-200 opacity-40 hover:opacity-90 bg-[var(--c-gray-200)] bg-cover bg-center`}
x-bind:class="lbIndex === Number($el.dataset.lbIndex) ? 'opacity-100 ring-1 ring-black' : ''"
aria-label={`${i + 1}`}
>
@ -87,7 +87,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
:data-lb-index="5 + i"
:data-thumb-url="im.thumb"
@click="showLb(5 + i)"
class={`lb-thumb relative block w-full ${thumbAspect} mb-2 overflow-hidden cursor-pointer transition-opacity duration-200 opacity-40 hover:opacity-90 bg-[#eee] bg-cover bg-center`}
class={`lb-thumb relative block w-full ${thumbAspect} mb-2 overflow-hidden cursor-pointer transition-opacity duration-200 opacity-40 hover:opacity-90 bg-[var(--c-gray-200)] bg-cover bg-center`}
x-bind:class="lbIndex === Number($el.dataset.lbIndex) ? 'opacity-100 ring-1 ring-black' : ''"
:aria-label="String(5 + i + 1)"
>

View File

@ -1,6 +1,6 @@
---
// 加载屏:首屏/整页切换时遮盖未渲染内容,避免 FOUC。
// 轻量原则(用户要求"朴实流畅"):
// 轻量原则(追求朴实流畅):
// - 只用静态大字 + 小标 + 一条 2px 细线进度(transform/opacity 动画,最便宜);
// - 去掉昂贵的多层 text-shadow 位移动画与外部字体;最短展示时间压缩到 ~350ms,跟手不拖沓。
@ -48,14 +48,14 @@ const { t } = getI18n(Astro)
font-size: 0.72rem;
letter-spacing: 0.34em;
text-transform: uppercase;
color: #888;
color: var(--c-gray-700);
animation: ls-pulse 1.4s ease-in-out infinite;
}
#loading-screen .ls-bar {
margin-top: 2em;
width: min(180px, 60vw);
height: 2px;
background: #e6e6e6;
background: var(--c-gray-300);
overflow: hidden;
}
#loading-screen .ls-bar::before {

View File

@ -89,11 +89,11 @@ const homeHref = href(locale, ROUTES.home)
<section class="mt-16">
{/* 每行张数切换(应对图集图片很多的情况):2/3/4/5 列,移动端固定 2 列 */}
<div class="flex items-center justify-between mb-5 border-b border-black/10 pb-3">
<span class="text-[11px] uppercase tracking-[0.22em] text-[#999]">
<span class="text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]">
{(imageTotal || images.length)} LOOKS
</span>
<div class="flex items-center gap-1" role="group" aria-label="Images per row">
<span class="text-[11px] uppercase tracking-[0.18em] text-[#999] mr-2 hidden sm:inline">PER ROW</span>
<span class="text-[11px] uppercase tracking-[0.18em] text-[var(--c-gray-600)] mr-2 hidden sm:inline">PER ROW</span>
{/* 每行张数切换:只有「3 张 / 5 张」两档,用竖条图标直观表达列数(避免文字数字按钮)。
density 只影响 md+;移动端固定 2 列(5 列在手机上不可用)。 */}
<button
@ -145,7 +145,7 @@ const homeHref = href(locale, ROUTES.home)
data-fav-id={im.id}
data-fav-url={im.raw}
@click={`openLb(${i})`}
class="relative bg-[#f2f2f2] cursor-zoom-in group"
class="relative bg-[var(--c-gray-100)] cursor-zoom-in group"
>
<img
src={im.url}
@ -202,10 +202,10 @@ const homeHref = href(locale, ROUTES.home)
x-show="!isAuthed && preview"
x-cloak
login-redirect
class="relative bg-[#f2f2f2] cursor-pointer group overflow-hidden"
class="relative bg-[var(--c-gray-100)] cursor-pointer group overflow-hidden"
>
{/* 与真实图片等高的 3:4 图位:锁图标 + 提示,点击跳登录 */}
<div class="w-full aspect-[2/3] flex items-center justify-center text-black group-hover:text-[#9a9a9a] transition-colors pointer-events-none">
<div class="w-full aspect-[2/3] flex items-center justify-center text-black group-hover:text-[var(--c-gray-700)] transition-colors pointer-events-none">
<div class="flex flex-col items-center gap-2">
<svg class="w-7 h-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<rect x="4.5" y="10.5" width="15" height="10" rx="2" />
@ -231,7 +231,7 @@ const homeHref = href(locale, ROUTES.home)
:data-fav-id="im.id"
:data-fav-url="im.raw"
@click="openLb(5 + i)"
class="relative bg-[#f2f2f2] cursor-zoom-in group"
class="relative bg-[var(--c-gray-100)] cursor-zoom-in group"
>
<img
:src="im.url"
@ -285,7 +285,7 @@ const homeHref = href(locale, ROUTES.home)
</article>
) : (
<div class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
<p class="text-sm text-[#888] uppercase tracking-widest mb-4">
<p class="text-sm text-[var(--c-gray-700)] uppercase tracking-widest mb-4">
{error === "missing" ? t('No article ID specified') : t('Failed to load article. Please refresh or try later.')}
</p>
@ -312,13 +312,13 @@ const homeHref = href(locale, ROUTES.home)
x-cloak
class="absolute right-4 sm:right-6 bottom-[72px] z-30 flex flex-col items-end gap-2"
>
<span class="text-[10px] uppercase tracking-[0.22em] text-[#999]">Details</span>
<span class="text-[10px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]">Details</span>
<div class="flex max-w-[min(70vw,440px)] gap-2 overflow-x-auto no-scrollbar">
<template x-for="(d, i) in currentDetailList" :key="d.url">
<button
type="button"
@click.stop="openDetailLbAt(i)"
class="relative flex-none w-[52px] sm:w-[68px] aspect-[2/3] overflow-hidden border border-black/15 bg-[#eee] cursor-zoom-in transition-colors hover:border-black"
class="relative flex-none w-[52px] sm:w-[68px] aspect-[2/3] overflow-hidden border border-black/15 bg-[var(--c-gray-200)] cursor-zoom-in transition-colors hover:border-black"
:aria-label="`Detail ${i + 1}`"
>
<img :src="d.thumb" :alt="d.name || ''" loading="lazy" decoding="async" class="w-full h-full object-cover" />
@ -459,7 +459,7 @@ const homeHref = href(locale, ROUTES.home)
// 为什么要把这个类型写出来:`Alpine.data` 的泛型 T 靠**对象字面量**推断,而这里用了对象展开
// ({...createGalleryView()}),推断会退化成 {},于是方法里的 this 只剩 Alpine 魔法属性,
// 所有 this.xxx 都报「属性不存在」。给回调标注返回类型,T 才有确定值。
// (@types/alpinejs 那个自引用泛型约束的老毛病,详见 MEMORY.md。)
// (@types/alpinejs 那个自引用泛型约束的老毛病。)
type ArticleExtras = {
density: number
currentDetailName: string

View File

@ -72,7 +72,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
data-fav-id={im.id}
data-fav-url={im.raw}
@click={`openLb(${i})`}
class="relative bg-[#f2f2f2] cursor-zoom-in group"
class="relative bg-[var(--c-gray-100)] cursor-zoom-in group"
>
<img
src={im.url}
@ -99,7 +99,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
</button>
{im.name && (
<figcaption class="text-[11px] uppercase tracking-widest text-[#999] mt-2">
<figcaption class="text-[11px] uppercase tracking-widest text-[var(--c-gray-600)] mt-2">
{im.name}
</figcaption>
)}
@ -114,10 +114,10 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
x-show="!isAuthed && preview"
x-cloak
login-redirect
class="relative bg-[#f2f2f2] cursor-pointer group overflow-hidden"
class="relative bg-[var(--c-gray-100)] cursor-pointer group overflow-hidden"
>
{/* 与真实图片等高的 3:4 图位:锁图标 + 提示,点击跳登录 */}
<div class="w-full aspect-[3/4] flex items-center justify-center text-[#c4c4c4] group-hover:text-[#9a9a9a] transition-colors pointer-events-none">
<div class="w-full aspect-[3/4] flex items-center justify-center text-[var(--c-gray-600)] group-hover:text-[var(--c-gray-700)] transition-colors pointer-events-none">
<div class="flex flex-col items-center gap-2">
<svg class="w-7 h-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<rect x="4.5" y="10.5" width="15" height="10" rx="2" />
@ -128,7 +128,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
</div>
{/* 与真实图 figcaption 同高的 caption 行(正常图显示 img01 等小字):
占位显示 +N photos,保证遮罩格子与真实图片格子总高一致 */}
<figcaption class="text-[11px] uppercase tracking-widest text-[#cfcfcf] mt-2">+{lockedCount} {t('photos')}</figcaption>
<figcaption class="text-[11px] uppercase tracking-widest text-[var(--c-gray-600)] mt-2">+{lockedCount} {t('photos')}</figcaption>
</figure>
))
}
@ -142,7 +142,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
:data-fav-id="im.id"
:data-fav-url="im.raw"
@click="openLb(5 + i)"
class="relative bg-[#f2f2f2] cursor-zoom-in group"
class="relative bg-[var(--c-gray-100)] cursor-zoom-in group"
>
<img
:src="im.url"
@ -166,7 +166,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
</svg>
</button>
<template x-if="im.name">
<figcaption class="text-[11px] uppercase tracking-widest text-[#999] mt-2" x-text="im.name"></figcaption>
<figcaption class="text-[11px] uppercase tracking-widest text-[var(--c-gray-600)] mt-2" x-text="im.name"></figcaption>
</template>
</figure>
</template>
@ -177,7 +177,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
</article>
) : (
<div class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
<p class="text-sm text-[#888] uppercase tracking-widest mb-4">
<p class="text-sm text-[var(--c-gray-700)] uppercase tracking-widest mb-4">
{error === "missing" ? t('No street snap ID specified') : t('Failed to load street snap. Please refresh or try later.')}
</p>
@ -204,7 +204,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
import { createGalleryView } from "@/lib/gallery"
// 图集 + 大图灯箱的共享逻辑全在 lib/gallery.ts;街拍详情没有额外扩展,只补一个 init 调共享初始化。
// 回调显式标注返回类型:对象展开会让 Alpine.data 的泛型 T 退化成 {},this.xxx 全部报错(见 Item.astro 说明)。
// 回调显式标注返回类型:对象展开会让 Alpine.data 的泛型 T 退化成 {},this.xxx 全部报错(见 RunwayLookCard.astro 说明)。
document.addEventListener("alpine:init", () => {
Alpine.data(
"streetSnapView",

View File

@ -32,12 +32,12 @@ const noHistoryLabel = t("no history yet")
const browseLabel = t("browse the archive")
---
<Layout title={titleLabel} description={accountDesc} keywords={accountKeywords}>
<!-- 页面版心与 Layout 一致:黑白编辑风,衬线大标题 + 等宽小标 + 粗黑细线 -->
<!-- 页面版心与 Layout 一致:黑白编辑风,粗黑大标题 + 宽字距小标 + 粗黑细线 -->
<main class="account-root mx-auto w-full max-w-[1400px] px-5 py-14 md:px-10 md:py-20 font-sans" x-data="accountPage" data-locale={locale}>
<!-- ===== 未登录:整版引导 ===== -->
<div x-show="!user" x-cloak class="flex flex-col items-center justify-center border border-black/15 px-6 py-32 text-center">
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{pleaseLabel}</p>
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{pleaseLabel}</p>
<p class="mb-12 text-5xl font-black uppercase leading-none tracking-tight md:text-7xl">{titleLabel}</p>
<a href={loginUrl} class="inline-flex items-center justify-center border border-black bg-black px-10 py-4 text-[12px] font-bold uppercase tracking-[0.22em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a>
</div>
@ -45,19 +45,19 @@ const browseLabel = t("browse the archive")
<!-- ===== 已登录 ===== -->
<div x-show="user" x-cloak>
<!-- 报头:衬线大标题会员名 + 档案标签 -->
<!-- 报头:粗黑大标题会员名 + 档案标签 -->
<header class="flex flex-wrap items-end justify-between gap-x-16 gap-y-8 border-b-2 border-black pb-10 pt-2">
<div class="min-w-0">
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{t("member profile")}</p>
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{t("member profile")}</p>
<div class="flex flex-wrap items-end gap-x-4">
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl" x-text="(user?.username || '')"></h1>
<!-- VIP 徽标:仅高级用户(图集详情页可见高清原图)显示 -->
<span x-show="user?.tier === 'vip'" x-cloak class="mb-2 inline-block border border-black bg-black px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.3em] text-white">{t("vip")}</span>
</div>
</div>
<div class="shrink-0 pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
<p>Portfolio Studio</p>
<p class="flex items-center justify-end gap-2 text-zinc-500">Archive
<div class="shrink-0 pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-[var(--c-gray-600)]">
<p>{SITE_NAME}</p>
<p class="flex items-center justify-end gap-2 text-[var(--c-gray-700)]">Archive
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span>
</p>
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
@ -69,7 +69,7 @@ const browseLabel = t("browse the archive")
<!-- 左:编号目录式垂直导航(移动端折成横向条) -->
<nav class="lg:sticky lg:top-12 lg:self-start" aria-label="Account sections">
<p class="mb-3 hidden text-[10px] uppercase tracking-[0.3em] text-zinc-400 lg:block">Index</p>
<p class="mb-3 hidden text-[10px] uppercase tracking-[0.3em] text-[var(--c-gray-600)] lg:block">Index</p>
<div class="flex gap-10 overflow-x-auto border-b border-black/15 text-[12px] uppercase tracking-[0.2em] lg:flex-col lg:gap-0 lg:overflow-visible lg:border-b-0 lg:border-t lg:border-black/10">
<button type="button" @click="tab='saved'" :class="tab==='saved' ? 'border-black text-black' : 'border-transparent text-black/40 hover:text-black'" class="-mb-px whitespace-nowrap border-b-2 px-0.5 pb-4 pt-1 text-left transition-colors lg:mb-0 lg:w-full lg:border-b-0 lg:border-l-2 lg:px-5 lg:py-4">
<span class="mr-3 text-[10px] opacity-60">01</span>{savedLooksLabel}
@ -86,7 +86,7 @@ const browseLabel = t("browse the archive")
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
<h2 class="text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{savedLooksLabel}</h2>
<p class="pb-1 text-[11px] uppercase tracking-[0.22em] text-zinc-400"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
<p class="pb-1 text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
</div>
<!-- 粒度切换:图集 / 单图(服务端 kind 过滤分页) -->
@ -105,7 +105,7 @@ const browseLabel = t("browse the archive")
<div x-show="favorites.length > 0" class="grid grid-cols-2 gap-x-4 gap-y-10 sm:grid-cols-3 lg:grid-cols-3 xl:grid-cols-4 lg:gap-x-5">
<template x-for="f in favorites" :key="f.id">
<div class="group">
<a :href="itemHref(f)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
<a :href="itemHref(f)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[var(--c-gray-100)] transition-colors group-hover:border-black">
<img :src="coverUrl(f)" :alt="f.title" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]" />
</a>
<button @click="removeFav(f.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
@ -113,7 +113,7 @@ const browseLabel = t("browse the archive")
<a :href="itemHref(f)" target="_blank" rel="noopener" class="block">
<p class="truncate text-[13px] font-medium leading-snug" x-text="f.title"></p>
</a>
<p class="mt-1.5 truncate text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="f.brand || '—'"></p>
<p class="mt-1.5 truncate text-[10px] uppercase tracking-[0.16em] text-[var(--c-gray-600)]" x-text="f.brand || '—'"></p>
</div>
</div>
</template>
@ -138,8 +138,8 @@ const browseLabel = t("browse the archive")
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
<h2 class="text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{historyLabel}</h2>
<div class="flex items-baseline gap-8 pb-1">
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-400"><span class="font-bold text-black" x-text="histTotal"></span> {itemsLabel}</p>
<button x-show="histTotal > 0" @click="clearAllHistory()" class="text-[11px] uppercase tracking-[0.2em] text-zinc-400 underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
<p class="text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="histTotal"></span> {itemsLabel}</p>
<button x-show="histTotal > 0" @click="clearAllHistory()" class="text-[11px] uppercase tracking-[0.2em] text-[var(--c-gray-600)] underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
</div>
</div>
@ -153,7 +153,7 @@ const browseLabel = t("browse the archive")
<div x-show="histTotal > 0" class="grid grid-cols-2 gap-x-4 gap-y-10 sm:grid-cols-3 lg:grid-cols-3 xl:grid-cols-4 lg:gap-x-5">
<template x-for="h in history" :key="h.id">
<div class="group">
<a :href="itemHref(h)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
<a :href="itemHref(h)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[var(--c-gray-100)] transition-colors group-hover:border-black">
<img :src="coverUrl(h)" :alt="h.data?.title || ''" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]" />
</a>
<button @click="removeHist(h.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
@ -161,7 +161,7 @@ const browseLabel = t("browse the archive")
<a :href="itemHref(h)" target="_blank" rel="noopener" class="block">
<p class="truncate text-[13px] font-medium leading-snug" x-text="h.data?.title || '…'"></p>
</a>
<p class="mt-1.5 truncate text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="h.data?.brand || '—'"></p>
<p class="mt-1.5 truncate text-[10px] uppercase tracking-[0.16em] text-[var(--c-gray-600)]" x-text="h.data?.brand || '—'"></p>
</div>
</div>
</template>
@ -202,7 +202,7 @@ const browseLabel = t("browse the archive")
const PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="300" height="400">' +
'<rect width="300" height="400" fill="#f1f1f1"/>' +
'<rect width="300" height="400" fill="var(--c-gray-100)"/>' +
'<text x="150" y="200" font-family="sans-serif" font-size="16" letter-spacing="3" fill="#9ca3af" text-anchor="middle">LOOK</text>' +
'</svg>'
)

View File

@ -79,7 +79,7 @@ const streetItems: {
<!-- Runway -->
<div class="panel" x-show="activeTab === 'backstage'" x-transition.opacity.duration.300ms>
<div class="container px-md md:px-xl text-2xl md:text-2xl-1 font-medium mb-4">
<div class="text-2xl font-medium mb-4">
Hot Brand
</div>
@ -207,20 +207,16 @@ const streetItems: {
<style>
@reference "tailwindcss";
:root {
--brand: #cf5a44;
}
/* 标签 */
.tab {
@apply relative pb-[6px] text-[#bbb] transition-colors duration-200 no-underline;
@apply relative pb-[6px] text-[var(--c-gray-600)] transition-colors duration-200 no-underline;
}
.tab:hover {
@apply text-[#666];
@apply text-[var(--c-gray-800)];
}
.tab::after {
content: "";
@apply absolute left-0 right-0 bottom-0 h-[2px] bg-[var(--brand)] scale-x-0 origin-left transition-transform duration-[250ms] ease-in-out;
@apply absolute left-0 right-0 bottom-0 h-[2px] bg-black scale-x-0 origin-left transition-transform duration-[250ms] ease-in-out;
}
.tab.is-active {
@apply text-black;
@ -283,7 +279,7 @@ const streetItems: {
position: relative;
display: block;
height: clamp(500px, 80vh, 900px);
background: #f0f0f0;
background: var(--c-gray-100);
overflow: hidden;
}
.hs-img-box > img {
@ -307,7 +303,7 @@ const streetItems: {
position: absolute;
inset: 0;
z-index: 0;
background-image: linear-gradient(100deg, #f0f0f0 30%, #e6e6e6 50%, #f0f0f0 70%);
background-image: linear-gradient(100deg, var(--c-gray-100) 30%, var(--c-gray-300) 50%, var(--c-gray-100) 70%);
background-size: 200% 100%;
animation: hs-shimmer 1.3s ease-in-out infinite;
transition: opacity 0.35s ease;
@ -321,7 +317,7 @@ const streetItems: {
display: flex;
align-items: center;
justify-content: center;
color: #bdbdbd;
color: var(--c-gray-500);
pointer-events: none;
z-index: 2;
transition: opacity 0.3s ease;
@ -390,22 +386,18 @@ const streetItems: {
overflow: hidden;
text-overflow: ellipsis;
}
@media (min-width: 768px) {
.hs-caption {
padding: 60px 28px 26px;
}
.hs-cap-brand {
font-size: 26px;
}
.hs-cap-meta {
font-size: 15px;
}
.hs-caption {
@apply md:px-7 md:pb-7 md:pt-14;
}
@media (max-width: 768px) {
.hs-swiper,
.hs-img-box {
height: clamp(340px, 56vh, 540px);
}
.hs-cap-brand {
@apply md:text-[26px];
}
.hs-cap-meta {
@apply md:text-[15px];
}
.hs-swiper,
.hs-img-box {
@apply max-md:h-[clamp(340px,56vh,540px)];
}
/* ========================= Street Style ========================= */
@ -416,17 +408,17 @@ const streetItems: {
@apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-[14px] md:gap-[18px];
}
.ss-card {
@apply relative block w-full overflow-hidden bg-[#f0f0f0] no-underline text-inherit;
@apply relative block w-full overflow-hidden bg-[var(--c-gray-100)] no-underline text-inherit;
aspect-ratio: 3 / 4;
}
.ss-img-box {
@apply absolute inset-0 w-full h-full overflow-hidden;
background: #f0f0f0;
background: var(--c-gray-100);
}
.ss-img-box > img {
@apply absolute inset-0 w-full h-full object-cover;
opacity: 0;
transition: opacity 0.45s ease, transform 0.7s ease;
transition: opacity 0.45s ease, transform 0.4s ease;
}
.ss-img-box.is-loaded > img {
opacity: 1;
@ -443,7 +435,7 @@ const streetItems: {
position: absolute;
inset: 0;
z-index: 0;
background-image: linear-gradient(100deg, #f0f0f0 30%, #e6e6e6 50%, #f0f0f0 70%);
background-image: linear-gradient(100deg, var(--c-gray-100) 30%, var(--c-gray-300) 50%, var(--c-gray-100) 70%);
background-size: 200% 100%;
animation: hs-shimmer 1.3s ease-in-out infinite;
transition: opacity 0.35s ease;
@ -458,7 +450,7 @@ const streetItems: {
display: flex;
align-items: center;
justify-content: center;
color: #bdbdbd;
color: var(--c-gray-500);
pointer-events: none;
transition: opacity 0.3s ease;
}
@ -590,7 +582,12 @@ const streetItems: {
// 图片加载完成(每张图的 Alpine 组件 dispatch "img-ready")后更新 Swiper
window.addEventListener("img-ready", scheduleSwiperUpdate)
</script>
{/* 图片加载状态组件独立成块,刻意不依赖上面的 swiper 模块:
避免 swiper 依赖预构建异常(如 Vite 504 Outdated Optimize Dep)时,整段脚本不执行、
连 Alpine 的 imgLoader 注册一起失效,导致 x-data="imgLoader()" 报未定义。 */}
<script>
/* ---------------- 共享图片加载状态组件 ---------------- */
// Hot Brand 与 Street Style 复用同一份逻辑:
// 1) init() 检查 complete + naturalWidth,缓存已完成的图片立即置位,不一直显示骨架;

View File

@ -46,7 +46,7 @@ const loginKeywords = locale === 'cn'
<meta name="description" content={loginDesc} />
<meta name="keywords" content={loginKeywords} />
</head>
<!-- 全站一致:白底 + 衬线大标题 + 等宽小标 + 粗黑细线,直角无阴影 -->
<!-- 全站一致:白底 + 粗黑大标题 + 宽字距小标 + 粗黑细线,直角无阴影 -->
<body class="min-h-screen bg-white text-black antialiased selection:bg-black selection:text-white">
<main class="mx-auto w-full max-w-[1400px] px-5 py-10 md:px-10 md:py-14 font-sans">
@ -73,16 +73,16 @@ const loginKeywords = locale === 'cn'
</div>
</header>
<!-- 报头:与账户页同一套 masthead(mono 眉标 + 衬线巨标题 + 档案标签 + 2px 粗黑线) -->
<!-- 报头:与账户页同一套 masthead(宽字距眉标 + 粗黑巨标题 + 档案标签 + 2px 粗黑线) -->
<section class="border-b-2 border-black pb-10">
<div class="flex flex-wrap items-end justify-between gap-x-16 gap-y-8">
<div>
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{membersLabel}</p>
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{membersLabel}</p>
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl">{loginLabel}</h1>
</div>
<div class="pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
<p>Portfolio Studio</p>
<p class="flex items-center justify-end gap-2 text-zinc-500">Archive
<div class="pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-[var(--c-gray-600)]">
<p>{SITE_NAME}</p>
<p class="flex items-center justify-end gap-2 text-[var(--c-gray-700)]">Archive
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span>
</p>
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
@ -94,17 +94,17 @@ const loginKeywords = locale === 'cn'
<div class="grid gap-12 pt-12 md:grid-cols-[minmax(0,1fr)_320px] md:gap-16 md:pt-16">
<div class="w-full max-w-md">
<p class="mb-10 text-[11px] uppercase tracking-[0.22em] text-zinc-400">{promptLabel}</p>
<p class="mb-10 text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]">{promptLabel}</p>
<form x-data="loginForm" data-locale={locale} data-fail-label={failLabel} @submit.prevent="submit()" class="space-y-8" novalidate>
<div>
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-zinc-500">{accountLabel}</label>
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-700)]">{accountLabel}</label>
<input type="text" x-model="account" autocomplete="username" required autofocus
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors focus:border-black focus:ring-0" />
</div>
<div>
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-zinc-500">{passwordLabel}</label>
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-700)]">{passwordLabel}</label>
<input type="password" x-model="password" autocomplete="current-password" required
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors focus:border-black focus:ring-0" />
</div>
@ -119,16 +119,16 @@ const loginKeywords = locale === 'cn'
</form>
<div class="mt-10 border-t border-black/15 pt-5">
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.2em] text-[var(--c-gray-600)] transition-colors hover:text-black">
<span aria-hidden="true">←</span>{backLabel}
</a>
</div>
</div>
<aside class="border-t border-black/10 pt-8 md:border-l md:border-t-0 md:pl-8 md:pt-0">
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-zinc-400">Archive note</p>
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">Archive note</p>
<p class="text-lg leading-relaxed text-black/80">{t('minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. integrated with 2026 color forecast intelligence and ai palette generator.')}</p>
<div class="mt-8 border-t border-black/10 pt-5 text-[10px] uppercase tracking-[0.3em] text-zinc-400">
<div class="mt-8 border-t border-black/10 pt-5 text-[10px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">
<p class="mb-3">Runway · Street · Archive</p>
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
</div>

View File

@ -162,7 +162,7 @@ const runwayGroups = [
<!-- Card List -->
<template x-for="it in items" :key="it.id">
<CollectionCard clien />
<CollectionCard />
</template>
</div>