update
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
toom1996
2026-08-24 21:52:47 +08:00
parent 3814cf40ed
commit fbbac26903
10 changed files with 721 additions and 551 deletions

View File

@ -0,0 +1,81 @@
# 2026-08-24 工作日志
## 品牌弹窗「热门推荐」做成独立 HOT 字母 tab(19:52)
- 用户要求:把品牌弹窗里的「热门推荐」单独做成一个 letter(字母条里的独立 tab),并且打开弹窗默认就选中这个热门字母。
- 范围:仅品牌筛选弹窗的 A-Z 字母条(`showsmodal-az`),存在于 `src/pages/cn/shows.astro` 与 `src/components/RunwayLooks.astro`(en /en/runway-looks/)。**en /shows/ 没这个弹窗(只有侧边栏),未动**。
- 改动(两文件一致):
1. 字母条最前插入 `<button class="az az-hot" @click="modalPickLetter('HOT')">HOT</button>`;原 A-Z 不变。
2. `modalPickLetter(l)` 顶部加 `if (l === "HOT") { this.modalKeyword=""; await this.modalLoadHot(); return }`。
3. `modalLoadHot()` 开头设 `this.modalLetter = "HOT"`(让 HOT 按钮 `active` 高亮 + 提示「热门推荐」显示)。
4. `openBrandModal()` 把 `this.modalLetter = ""` 改成 `= "HOT"`,打开即默认选中热门。
5. 提示文案 `x-show="!modalKeyword && !modalLetter"` 改为 `&& modalLetter === 'HOT'`(只在 HOT 态显示「热门推荐」)。
6. CSS 新增 `.showsmodal-az .az-hot { w-auto px-2 font-sans font-bold tracking-[0.12em] text-[9px] }`(HOT 是 3 字母,宽于 24px 方块)。
- 验证:`npm run build` BUILD_EXIT=0;两文件 read_lints 0;dist 的 cn/shows 与 en/runway-looks `index.html` 均含 `az-hot` + `modalPickLetter('HOT')` 各 1 处;`python -m http.server 8088` 托管 dist/client 预览两页弹窗。
- ⚠️ 假数据 id(`hot-chanel` 等)仍非真实 hashid,勾选假热门去筛仍返回空,等真实 SSG `/api/ssg/brands/popular` 接通即生效。
## 删除侧边栏 "Search designer" 搜索框(17:06)
- 用户要求去掉 DESIGNERS 侧边栏的 "Search designer…" 搜索框及相关的 JS。
- 范围:仅侧边栏那个输入框(用 `keyword` state 驱动)。**品牌弹窗里的"搜索品牌名…"(`modalKeyword`/`modalSearch`)是另一套,保留不动**。
- 三文件统一改(`src/pages/shows.astro` en、`src/pages/cn/shows.astro`、`src/components/RunwayLooks.astro`):
1. 删 `<input class="showssearch" ... x-model.debounce.150ms="keyword" />`。
2. 删 `keyword: ""` state。
3. `visibleBrandItems` getter 去掉 `if (kw) {...} else {...}` 的 keyword 分支,简化为只走字母分组展示(else 分支)。
4. reset(`clearAll`/`resetFilter` 之类)里删 `this.keyword = ""`。
5. `shows.astro` 额外把 `x-show="hasMoreBrands && !keyword"` 改回 `x-show="hasMoreBrands"`(Show all 链接不再因搜索态隐藏)。
6. 删 `.showssearch` 死 CSS(该类仅被上述输入框使用)。
- 验证:`npm run build` BUILD_EXIT=0;三文件 `read_lints` 0 错误;`(?<![a-zA-Z])keyword(?![A-Za-z])` 全仓 0 残留(仅留 `modalKeyword`);cn/shows 与 shows 产物 `Search designer`/`showssearch` 各 0 处。
## 品牌筛选「最多只能选一个」修复
- 用户反馈品牌筛选只能单选。排查发现:仓库里品牌筛选逻辑本就是多选(`selectedBrands` 数组 + `toggleBrand` 增删),但 **DESIGNERS 侧边栏 `<ul class="showschecks">` 漏了 `x-for="item in visibleBrandItems"`**,导致品牌勾选框一行都不渲染——侧边栏品牌列表完全失效,用户只能走弹窗。
- cn 版 `src/pages/cn/shows.astro` 弹窗还硬卡了「最多 5 个」(`modalMaxReached` + `:disabled` + `toggleModalBrand` 内 `>=5 return` + 底部「已选 X / 最多 5 个」)。
- 改动:
- `src/pages/cn/shows.astro` 与 `src/pages/shows.astro`(en):补回 `<ul>` 上的 `x-for="item in visibleBrandItems" :key="item.key"`,让 DESIGNERS 多选列表正常渲染(按字母分组 + checkbox + `toggleBrand` 多选)。
- cn 版额外:删除 `modalMaxReached` 计算属性、`toggleModalBrand` 的 5 上限 guard、`@change` 上的 `:disabled`、底部文案改「已选 X 个品牌」——弹窗改为**无上限纯多选**。
- en 版 `shows.astro` 没有品牌弹窗(只用侧边栏列表 + "Show all N designers" 展开),补 x-for 后即恢复正常多选。
- 验证:`npm run build` BUILD_EXIT=0;产物 HTML 含 `visibleBrandItems`(cn/en 各 1)、`modalMaxReached` 残留 0、`最多 5 个` 残留 0。
- **注意**:去掉 5 上限后,若用户勾选大量品牌(如 50+),`getArticles({ brand_ids: [...] })` 会带长数组请求后端;后端若用 `IN (...)` 应无碍,但极端多选需关注接口性能(用户暂未提,先不限制)。
## 品牌筛选「改成单选」纠正(en 版)
- **纠正**:上一笔把品牌筛选做成"多选"是理解反了。用户原话"最多只能选一个"的真实意图是**要单选**(最多 1 个),且**只改 en 版**(`src/pages/shows.astro`)。
- 改动(仅 en):`toggleBrand(id)` 由增删数组改为「已选则清空、否则替换为单元素数组」`this.selectedBrands = [id]` / `= []`。`selectedBrands` 仍为数组(接口 / pills / `:checked` 绑定都依赖数组形态),只是长度恒 ≤ 1,checkbox 视觉上等同 radio。
- **cn 版保持多选**(用户说"你就改en的就行"),即 cn/shows.astro 仍是补回 x-for + 去 5 上限的多选状态。⚠️ 现状 en 单选 / cn 多选,两端行为不一致,是用户明确要求的。
- 验证:`npm run build` BUILD_EXIT=0;`read_lints` 0 错误;预览 `http://localhost:8090/shows/`。
## 品牌筛选「cn 也改单选」纠正(14:36)
- 用户又提"现在品牌的筛选不是最多 5 个吗,我希望最多只能选一个"。说明他认知里 cn 仍是旧"最多 5 个"弹窗(实际上一条我已把 cn 改成无上限多选)。最终意图 = **品牌筛选全局单选(max 1)**。
- 这次把 cn 版也改成单选:`src/pages/cn/shows.astro` 的 `toggleBrand`(侧边栏)+ `toggleModalBrand`(弹窗)都由增删数组改为「已选则清空、否则置单元素数组」;弹窗底部计数文案由「已选 X 个品牌」改为「已选 X / 1 个品牌」明示上限。
- 现状:**en + cn 均已单选(max 1)**,两端一致。en 在上一笔已完成,cn 本笔补齐。
- 验证:`npm run build` BUILD_EXIT=0;`read_lints` 0 错误;预览 `http://localhost:8090/cn/shows/`。
## 年份筛选改为前端写死(不再调后端 getYears)
- 用户要求年份/季节这类筛选"改成品牌那种的数据,直接写死就好了,因为不会特别多"。
- 排查:Collection(selectedCt: rtw/menswear/couture/resort/pre_fall) 与 Season(selectedSeason: spring/fall) 本就是前端写死的静态 `<li>` checkbox;**只有 Year 是动态的**——frontmatter 调 `getYears(30)` 从后端拉年份、后端挂了才回落 `yearSeed=[2026..2021]`。
- 改动(3 文件一致:`src/pages/shows.astro` en、`src/pages/cn/shows.astro`、`src/components/RunwayLooks.astro`):
- 删除 `import { ... getYears ... }`、删除 `let yearChips` / `yearSeed` / `getYears` try-catch 整段。
- frontmatter 新增 `const yearOptions = [2026, 2025, 2024, 2023, 2022, 2021, 2020, 2019, 2018, 2017]`(写死,2026→2017 共 10 个)。
- 渲染 `{yearChips.map}` → `{yearOptions.map}`,其余(checkbox / x-model=selectedYear / refreshFilter)不变,年份仍为多选。
- 验证:`npm run build` BUILD_EXIT=0;`getYears|yearChips` 全仓 0 残留;cn 产物含 `>2017<`/`>2026<`/`value="2026"`。
- ⚠️ 取舍:写死后**2005–2016 这批老秀场将无法按年份筛选**(网格仍展示,只是筛不出)。当前 `yearOptions` 是一行可编辑数组,要补老年份直接往里加即可。用户未提,先保持 10 年窗口。
- **现状**:Collection/Season/Year 三套筛选均为前端写死静态数据;仅 DESIGNERS(品牌)仍由后端 `getBrands()` 拉取(用户未要求改品牌来源)。
## 年份区间改为"当今年份往前 15 年 + X年前",Collection/Season 换写死示例数据
- 用户纠正:年份应为 2026 往前推 15 年 → **到 2011**(之前误写成 2001),并加末项「2011年前」。
- 年份改为动态生成:`const currentYear = new Date().getFullYear(); const yearFloor = currentYear - 15;` 循环 push `yearFloor..currentYear`,末项 push `{value:'before'+yearFloor, label: yearFloor+'年前'}`。SSR 构建时 `new Date()` 取构建年(现 2026),故产物固定 2026→2011 + 「2011年前」共 17 项,`selectedYear` 为 string[](含 `"before2011"` 哨兵)。
- Collection / Season 改为写死示例(假)数据数组 `collectionOptions` / `seasonOptions`({value,label}),模板 `map` 渲染(与 Year 同套路)。Collection 现 8 项(rtw/menswear/womenswear/couture/resort/pre_fall/bridal/swimwear),Season 现 4 项(spring/fall/cruise/holiday)。`ctLabel()` 的 map 在 shows.astro / cn/shows.astro 两处补了 womenswear/bridal/swimwear 标签(RunwayLooks 无 ctLabel)。
- **仍内联渲染**(用户 earlier:「数量<10 不用弹窗」):Collection(8)/Season(4) 均 <10 内联;Year 虽 17 项但用户要求「都展示出来」故也内联。
- 验证:`npm run build` BUILD_EXIT=0;`read_lints` 0;cn 产物含 `2011年前`/`value="2011"`/`value="2026"`/`Womenswear`/`Cruise`/`before2011`。
- ⚠️ 假数据提醒:Collection 里 womenswear/bridal/swimwear、Season 里 cruise/holiday 是示例占位,后端 `collection_type`/`season` 未必识别 → 勾这些可能筛不到结果。确认后直接改 `collectionOptions`/`seasonOptions` 数组即可(已加注释说明)。`before2011` 同理需后端支持才能真正筛「2011 年前」,目前仅展示。
## 其他(同日)
- 上午修 RunwayLooks.astro / shows.astro / cn/shows.astro 三处 `<script>` 块 `ROUTES`/`clientHref` ts(2304):frontmatter 的 import 无法在客户端 `<script>` 作用域使用,需在各自脚本块内重新 import(见 MEMORY.md「Astro frontmatter 与 script 作用域隔离」)。
## 新增「热门品牌 / HOT」区(SSG 接口 + 假数据回落)
- 用户要求:选择品牌时加一个热门品牌区,进来最先展示热门品牌;数据走 SSG 接口获取,先写假数据看效果。
- 关键架构点:**SSG 接口 `getSsgPopularBrands()`(src/lib/api.ts,调 `/api/ssg/brands/popular`,用 `BASE_API_SSG` 8091 内部端口,仅 Node 构建期可用)不能在浏览器端调**。所以改为 **frontmatter 构建期**调 `getSsgPopularBrands(12)`,映射成 `{id,name,article_count}`,拿不到就回落前端写死的 `FAKE_HOT_BRANDS`(12 个时装屋:Chanel/Dior/Gucci/.../Bottega Veneta),烤进 `<script id="showshotbrands-data">` JSON,Alpine 在 `alpine:init` 里 `getElementById` 读出 `hotBrands`。
- 三文件改动一致(`src/pages/shows.astro` en、`src/pages/cn/shows.astro`、`src/components/RunwayLooks.astro`):
- import 加 `getSsgPopularBrands`;frontmatter 加 `FAKE_HOT_BRANDS` 常量 + `hotBrands` 抓取/回落;markup 加 `showshotbrands-data` JSON 脚本(RunwayLooks 额外补了缺失的 `showsbrands-data` 脚本——它之前只靠运行时 `getBrands` 撑弹窗,`allBrands` 实际为空)。
- Alpine:`alpine:init` 读 `hotBrands` 入组件;`pills` 的 findName 兼容 `hotBrands`(假 id 也能显示名)。
- **侧边栏 HOT 区**:shows.astro / cn/shows.astro 的 DESIGNERS `showsg-b` 顶部插入「HOT · Popular / 热门品牌 · HOT」分组,渲染 `hotBrands` 为 chip(checkbox 绑 `toggleBrand`/`selectedBrands`,含 `has-[:checked]` 选中高亮,纯 Tailwind 工具类,无需改 `<style>`)。RunwayLooks 侧边栏品牌列表是注释掉的,故只改弹窗、不插图侧栏 HOT。
- **弹窗热门**:`modalLoadHot()` 由「`getBrands({featured:1})`」改为「`this.hotBrands` 映射成 modalResults」,进入弹窗即先展示热门("热门推荐" 标题原已存在);搜索/选字母时仍走 `getBrands` 拉全量。
- 验证:`npm run build` BUILD_EXIT=0;三文件 `read_lints` 0;dist 三页(shows/cn/shows/en/runway-looks)均含 `showshotbrands-data` + `Chanel`/`Bottega Veneta`。
- ⚠️ 假数据取舍:FAKE id 形如 `hot-chanel`,**非真实 hashid** → 勾选假热门品牌去筛选会返回空(后端不认)。等真实 SSG `/api/ssg/brands/popular` 接通后,返回真实 hashid,筛选即生效。RunwayLooks 弹窗 `modalMaxReached` 返回 `>=1`(且 `:disabled` 据此),实际等同单选——属既有状态,未动。

View File

@ -28,6 +28,11 @@
- **正确做法**:用 `w-full`(强制 100% 父容器,常配 `max-w-[xxx]`),或 `w-full mx-auto`(保留大屏居中)。 - **正确做法**:用 `w-full`(强制 100% 父容器,常配 `max-w-[xxx]`),或 `w-full mx-auto`(保留大屏居中)。
- 适用范围:所有 Layout 内的页内根容器(包括 showsroot / blogs / about 等),**避免 `m-auto` 单独使用**。 - 适用范围:所有 Layout 内的页内根容器(包括 showsroot / blogs / about 等),**避免 `m-auto` 单独使用**。
## Astro frontmatter 与 `<script>` 作用域隔离(通用坑)
- **frontmatter 里的 `import` / 变量,在 `<script>`(客户端打包脚本)里拿不到**。两者是分别打包的:frontmatter 是服务端渲染期,`<script>` 是独立客户端 bundle。脚本里直接用 frontmatter 引的 `ROUTES` / `clientHref` 等,esbuild 不报错(当全局变量),但 **TS 语言服务报 `ts(2304) 找不到名称`**,运行时更是 `ReferenceError`。
- **正确做法**:脚本里要用什么,就在该 `<script>` 顶部重新 `import`。例:`import { clientHref, ROUTES } from "@/lib/routes"`。
- 排查方法:在 `<script>` 块里 `import` 行 grep,确认用到的标识符都已在脚本作用域内 import。本仓库曾在 RunwayLooks.astro、pages/shows.astro、pages/cn/shows.astro 三处脚本用了 `clientHref`/`ROUTES` 却只在 frontmatter import,统一补 import 修复(2026-08-24)。
## i18n 脚本内翻译陷阱(2026-08-23 踩坑) ## i18n 脚本内翻译陷阱(2026-08-23 踩坑)
- **Astro 对「打包型 `<script>`(module)不做 `{...}` 模板插值**。若在前端 `<script>` 里写 `slot.innerHTML = \`...{t('Hi')}...\``,构建后 `t('Hi')` 会原样留存在打包 JS 中 → 运行时 `t is not defined` 报错、脚本中断。 - **Astro 对「打包型 `<script>`(module)不做 `{...}` 模板插值**。若在前端 `<script>` 里写 `slot.innerHTML = \`...{t('Hi')}...\``,构建后 `t('Hi')` 会原样留存在打包 JS 中 → 运行时 `t is not defined` 报错、脚本中断。
- **正确做法**:在 frontmatter 用 `const hiLabel = t('Hi')` 预翻译,再用 `<script define:vars={{ hiLabel, logoutLabel }}>` 把字符串注入脚本,脚本内用 `${hiLabel}` 引用。define:vars 会把 `const hiLabel="..."` 注入到该 page 的脚本作用域(en 得英文、cn 得中文)。 - **正确做法**:在 frontmatter 用 `const hiLabel = t('Hi')` 预翻译,再用 `<script define:vars={{ hiLabel, logoutLabel }}>` 把字符串注入脚本,脚本内用 `${hiLabel}` 引用。define:vars 会把 `const hiLabel="..."` 注入到该 page 的脚本作用域(en 得英文、cn 得中文)。

568
package-lock.json generated
View File

@ -9,11 +9,11 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@astrojs/alpinejs": "^1.0.0", "@astrojs/alpinejs": "^1.0.0",
"@astrojs/node": "^11.1.2", "@astrojs/node": "^11.1.4",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@types/alpinejs": "^3.13.11", "@types/alpinejs": "^3.13.11",
"alpinejs": "^3.16.1", "alpinejs": "^3.16.1",
"astro": "^7.1.6", "astro": "^7.2.4",
"swiper": "^11.2.10", "swiper": "^11.2.10",
"tailwindcss": "^4.3.3" "tailwindcss": "^4.3.3"
}, },
@ -208,9 +208,9 @@
} }
}, },
"node_modules/@astrojs/internal-helpers": { "node_modules/@astrojs/internal-helpers": {
"version": "0.10.2", "version": "0.10.4",
"resolved": "https://registry.npmmirror.com/@astrojs/internal-helpers/-/internal-helpers-0.10.2.tgz", "resolved": "https://registry.npmmirror.com/@astrojs/internal-helpers/-/internal-helpers-0.10.4.tgz",
"integrity": "sha512-yt7fMgPYqSM4Tmr+taTW6Per+hjJ8Pk6lA1PAcDyqzOt8HzJ6Kje5WzCxA2Sd+9wsUW7uhkLeoTMK0cXPwH9rQ==", "integrity": "sha512-nozZSy/mKYLqe4YrqbKtdOszedAfXYCtw3wZ0d+CAjz4GqQ4L9rl1ltIL5BlgwmYVinJg/RZ0MgGuWOdlyRZlA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
@ -224,30 +224,29 @@
} }
}, },
"node_modules/@astrojs/markdown-satteri": { "node_modules/@astrojs/markdown-satteri": {
"version": "0.3.5", "version": "0.3.7",
"resolved": "https://registry.npmmirror.com/@astrojs/markdown-satteri/-/markdown-satteri-0.3.5.tgz", "resolved": "https://registry.npmmirror.com/@astrojs/markdown-satteri/-/markdown-satteri-0.3.7.tgz",
"integrity": "sha512-CvWVEFAbay7YO+i9SaqDJubipA5ckiVB89QWoMJ5XC0m5CtFg8JwZ7Kau6X9sYY7FZURH0w2l03ISH2jOS/RDQ==", "integrity": "sha512-NHcHbrKW/opbZnTZQ5nH293BdcK2VV0tjuzI88CLnvy2njiEJVwUQ5KFnYd4NoWgHJCY/I1CyjGWCR4Vv3khXQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@astrojs/internal-helpers": "0.10.2", "@astrojs/internal-helpers": "0.10.4",
"@astrojs/prism": "4.0.2", "@astrojs/prism": "4.0.2",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"hast-util-from-html": "^2.0.3", "satteri": "^0.10.3"
"satteri": "^0.9.1"
} }
}, },
"node_modules/@astrojs/node": { "node_modules/@astrojs/node": {
"version": "11.1.2", "version": "11.1.4",
"resolved": "https://registry.npmjs.org/@astrojs/node/-/node-11.1.2.tgz", "resolved": "https://registry.npmmirror.com/@astrojs/node/-/node-11.1.4.tgz",
"integrity": "sha512-lbOjylG18s8wURjT9teqa/SPXVtVpBgm9vccEg/BXlrW3ktqqgFv23vtOswEfluDmx2Z74ngyGE6hrGC14qWow==", "integrity": "sha512-R5tNIHm5ihyEYa0w1mLlfwLpwF1ArRmzHD/is5PUHYRY43G1Xm0k3D2HRJXgjey8/7+1DvDMJLUeBxXwgAyNDg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@astrojs/internal-helpers": "0.10.2", "@astrojs/internal-helpers": "0.10.4",
"send": "^1.2.1", "send": "^1.2.1",
"server-destroy": "^1.0.1" "server-destroy": "^1.0.1"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "^7.0.0" "astro": "^7.2.1"
} }
}, },
"node_modules/@astrojs/prism": { "node_modules/@astrojs/prism": {
@ -296,12 +295,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.29.7", "version": "7.29.8",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.29.7" "@babel/types": "^7.29.8"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -311,9 +310,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.29.7", "version": "7.29.8",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.29.7", "@babel/helper-string-parser": "^7.29.7",
@ -324,9 +323,9 @@
} }
}, },
"node_modules/@bruits/satteri-darwin-arm64": { "node_modules/@bruits/satteri-darwin-arm64": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz",
"integrity": "sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow==", "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -337,9 +336,9 @@
] ]
}, },
"node_modules/@bruits/satteri-darwin-x64": { "node_modules/@bruits/satteri-darwin-x64": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz",
"integrity": "sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q==", "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -350,9 +349,9 @@
] ]
}, },
"node_modules/@bruits/satteri-linux-arm64-gnu": { "node_modules/@bruits/satteri-linux-arm64-gnu": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz",
"integrity": "sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA==", "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -363,9 +362,9 @@
] ]
}, },
"node_modules/@bruits/satteri-linux-arm64-musl": { "node_modules/@bruits/satteri-linux-arm64-musl": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz",
"integrity": "sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ==", "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -376,9 +375,9 @@
] ]
}, },
"node_modules/@bruits/satteri-linux-x64-gnu": { "node_modules/@bruits/satteri-linux-x64-gnu": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz",
"integrity": "sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw==", "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -389,9 +388,9 @@
] ]
}, },
"node_modules/@bruits/satteri-linux-x64-musl": { "node_modules/@bruits/satteri-linux-x64-musl": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz",
"integrity": "sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q==", "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -402,9 +401,9 @@
] ]
}, },
"node_modules/@bruits/satteri-wasm32-wasi": { "node_modules/@bruits/satteri-wasm32-wasi": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz",
"integrity": "sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ==", "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@ -413,7 +412,7 @@
"dependencies": { "dependencies": {
"@emnapi/core": "1.11.1", "@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1", "@emnapi/runtime": "1.11.1",
"@napi-rs/wasm-runtime": "^1.1.6" "@napi-rs/wasm-runtime": "^1.2.3"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@ -451,9 +450,9 @@
} }
}, },
"node_modules/@bruits/satteri-win32-arm64-msvc": { "node_modules/@bruits/satteri-win32-arm64-msvc": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz",
"integrity": "sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg==", "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -464,9 +463,9 @@
] ]
}, },
"node_modules/@bruits/satteri-win32-x64-msvc": { "node_modules/@bruits/satteri-win32-x64-msvc": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz",
"integrity": "sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g==", "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1510,9 +1509,9 @@
} }
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.2.1", "version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
"integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==", "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -1526,8 +1525,8 @@
"url": "https://github.com/sponsors/Brooooooklyn" "url": "https://github.com/sponsors/Brooooooklyn"
}, },
"peerDependencies": { "peerDependencies": {
"@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
"@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
} }
}, },
"node_modules/@oslojs/encoding": { "node_modules/@oslojs/encoding": {
@ -1811,38 +1810,16 @@
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/pluginutils": {
"version": "5.4.0",
"resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
"integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@shikijs/core": { "node_modules/@shikijs/core": {
"version": "4.3.1", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-4.4.3.tgz",
"integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==", "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/primitive": "4.3.1", "@shikijs/primitive": "4.4.3",
"@shikijs/types": "4.3.1", "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2", "@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.5",
"hast-util-to-html": "^9.0.5" "hast-util-to-html": "^9.0.5"
}, },
"engines": { "engines": {
@ -1850,12 +1827,12 @@
} }
}, },
"node_modules/@shikijs/engine-javascript": { "node_modules/@shikijs/engine-javascript": {
"version": "4.3.1", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz",
"integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==", "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/types": "4.3.1", "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2", "@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.6" "oniguruma-to-es": "^4.3.6"
}, },
@ -1864,12 +1841,12 @@
} }
}, },
"node_modules/@shikijs/engine-oniguruma": { "node_modules/@shikijs/engine-oniguruma": {
"version": "4.3.1", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz",
"integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==", "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/types": "4.3.1", "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2" "@shikijs/vscode-textmate": "^10.0.2"
}, },
"engines": { "engines": {
@ -1877,51 +1854,51 @@
} }
}, },
"node_modules/@shikijs/langs": { "node_modules/@shikijs/langs": {
"version": "4.3.1", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-4.4.3.tgz",
"integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==", "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/types": "4.3.1" "@shikijs/types": "4.4.3"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@shikijs/primitive": { "node_modules/@shikijs/primitive": {
"version": "4.3.1", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/@shikijs/primitive/-/primitive-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/@shikijs/primitive/-/primitive-4.4.3.tgz",
"integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==", "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/types": "4.3.1", "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2", "@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4" "@types/hast": "^3.0.5"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@shikijs/themes": { "node_modules/@shikijs/themes": {
"version": "4.3.1", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-4.4.3.tgz",
"integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==", "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/types": "4.3.1" "@shikijs/types": "4.4.3"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@shikijs/types": { "node_modules/@shikijs/types": {
"version": "4.3.1", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-4.4.3.tgz",
"integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==", "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/vscode-textmate": "^10.0.2", "@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4" "@types/hast": "^3.0.5"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
@ -2556,6 +2533,21 @@
"am-i-vibing": "dist/cli.mjs" "am-i-vibing": "dist/cli.mjs"
} }
}, },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
@ -2597,20 +2589,19 @@
} }
}, },
"node_modules/astro": { "node_modules/astro": {
"version": "7.1.6", "version": "7.2.4",
"resolved": "https://registry.npmmirror.com/astro/-/astro-7.1.6.tgz", "resolved": "https://registry.npmmirror.com/astro/-/astro-7.2.4.tgz",
"integrity": "sha512-83x9rYbHazMaZkYrAFRVZXSQx2moFkz0F7cjTDUF3GWfS0a3p2vZXG1ZdhV86rStHApQCodBJW+XTD37xISIrQ==", "integrity": "sha512-+cuLsBns2wwUHI9a10xZMbjrF91m7+QNwqTVeljTx0B8Lf+8h0LgVGjdVIL2FALDKD2I565lczeeS3BFC+KdZg==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@astrojs/compiler-rs": "^0.3.2", "@astrojs/compiler-rs": "^0.3.2",
"@astrojs/internal-helpers": "0.10.2", "@astrojs/internal-helpers": "0.10.4",
"@astrojs/markdown-satteri": "0.3.5", "@astrojs/markdown-satteri": "0.3.7",
"@astrojs/telemetry": "3.3.3", "@astrojs/telemetry": "3.3.3",
"@capsizecss/unpack": "^4.0.0", "@capsizecss/unpack": "^4.0.0",
"@clack/prompts": "^1.1.0", "@clack/prompts": "^1.1.0",
"@oslojs/encoding": "^1.1.0", "@oslojs/encoding": "^1.1.0",
"@rollup/pluginutils": "^5.3.0",
"am-i-vibing": "^0.4.0", "am-i-vibing": "^0.4.0",
"aria-query": "^5.3.2", "aria-query": "^5.3.2",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
@ -2623,6 +2614,7 @@
"dset": "^3.1.4", "dset": "^3.1.4",
"es-module-lexer": "^2.0.0", "es-module-lexer": "^2.0.0",
"esbuild": "^0.28.0", "esbuild": "^0.28.0",
"find-process": "^2.1.1",
"flattie": "^1.1.1", "flattie": "^1.1.1",
"fontace": "~0.4.1", "fontace": "~0.4.1",
"get-tsconfig": "5.0.0-beta.4", "get-tsconfig": "5.0.0-beta.4",
@ -2649,7 +2641,7 @@
"tinyexec": "^1.0.4", "tinyexec": "^1.0.4",
"tinyglobby": "^0.2.15", "tinyglobby": "^0.2.15",
"ultrahtml": "^1.6.0", "ultrahtml": "^1.6.0",
"unifont": "~0.7.4", "unifont": "~0.7.5",
"unstorage": "^1.17.5", "unstorage": "^1.17.5",
"vite": "^8.0.13", "vite": "^8.0.13",
"vitefu": "^1.1.2", "vitefu": "^1.1.2",
@ -2673,7 +2665,7 @@
"sharp": "^0.34.0 || ^0.35.0" "sharp": "^0.34.0 || ^0.35.0"
}, },
"peerDependencies": { "peerDependencies": {
"@astrojs/markdown-remark": "7.2.2" "@astrojs/markdown-remark": "7.2.4"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@astrojs/markdown-remark": { "@astrojs/markdown-remark": {
@ -2716,6 +2708,22 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/character-entities-html4": { "node_modules/character-entities-html4": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", "resolved": "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
@ -2775,6 +2783,24 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/comma-separated-tokens": { "node_modules/comma-separated-tokens": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@ -2786,12 +2812,12 @@
} }
}, },
"node_modules/commander": { "node_modules/commander": {
"version": "11.1.0", "version": "14.0.3",
"resolved": "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz", "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16" "node": ">=20"
} }
}, },
"node_modules/common-ancestor-path": { "node_modules/common-ancestor-path": {
@ -2962,9 +2988,9 @@
} }
}, },
"node_modules/devalue": { "node_modules/devalue": {
"version": "5.9.0", "version": "5.9.1",
"resolved": "https://registry.npmmirror.com/devalue/-/devalue-5.9.0.tgz", "resolved": "https://registry.npmmirror.com/devalue/-/devalue-5.9.1.tgz",
"integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==", "integrity": "sha512-+17vil3EVQRzvtDJSFuTWEb8XJRvXqAiV3qZyQWD398QeXUa6CxsUyMdD1fxzEhUrd4FojitFz7lhIHBTlV4fw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/devlop": { "node_modules/devlop": {
@ -3003,18 +3029,6 @@
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
} }
}, },
"node_modules/dom-serializer/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/domelementtype": { "node_modules/domelementtype": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz",
@ -3094,9 +3108,9 @@
} }
}, },
"node_modules/entities": { "node_modules/entities": {
"version": "6.0.1", "version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@ -3106,9 +3120,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.1.tgz", "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
"integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
@ -3159,12 +3173,6 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/etag": { "node_modules/etag": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -3227,6 +3235,20 @@
} }
} }
}, },
"node_modules/find-process": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/find-process/-/find-process-2.1.1.tgz",
"integrity": "sha512-SrQDx3QhlmHM90iqn9rdjCQcw/T+WlpOkHFsjoRgB+zTpDfltNA1VSNYeYELwhUTJy12UFxqjWhmhOrJc+o4sA==",
"license": "MIT",
"dependencies": {
"chalk": "~4.1.2",
"commander": "^14.0.3",
"loglevel": "^1.9.2"
},
"bin": {
"find-process": "dist/cjs/bin/find-process.js"
}
},
"node_modules/flattie": { "node_modules/flattie": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/flattie/-/flattie-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/flattie/-/flattie-1.1.1.tgz",
@ -3324,55 +3346,13 @@
"uncrypto": "^0.1.3" "uncrypto": "^0.1.3"
} }
}, },
"node_modules/hast-util-from-html": { "node_modules/has-flag": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
"integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"devlop": "^1.1.0",
"hast-util-from-parse5": "^8.0.0",
"parse5": "^7.0.0",
"vfile": "^6.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-parse5": {
"version": "8.0.3",
"resolved": "https://registry.npmmirror.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"devlop": "^1.0.0",
"hastscript": "^9.0.0",
"property-information": "^7.0.0",
"vfile": "^6.0.0",
"vfile-location": "^5.0.0",
"web-namespaces": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-parse-selector": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "engines": {
"@types/hast": "^3.0.0" "node": ">=8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hast-util-to-html": { "node_modules/hast-util-to-html": {
@ -3411,23 +3391,6 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hastscript": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz",
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-parse-selector": "^4.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-3.0.3.tgz", "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-3.0.3.tgz",
@ -3522,9 +3485,9 @@
} }
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.3.0", "version": "4.3.1",
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.0.tgz", "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -3798,6 +3761,19 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmmirror.com/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "11.5.2", "version": "11.5.2",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.2.tgz", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.2.tgz",
@ -3808,22 +3784,22 @@
} }
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "1.1.0", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-1.2.2.tgz",
"integrity": "sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==", "integrity": "sha512-veT/+7iXrXzT39XnEN4lOxtNl72dMgJ8Lp+5Bd6YcMSWpb0n0MjBM8Uuooi6jgJr8dhUW2swQgBmoZVMni5SVg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/magicast": { "node_modules/magicast": {
"version": "0.5.3", "version": "0.5.4",
"resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.5.3.tgz", "resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.5.4.tgz",
"integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.3", "@babel/parser": "^7.29.7",
"@babel/types": "^7.29.0", "@babel/types": "^7.29.7",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
} }
}, },
@ -4081,9 +4057,9 @@
} }
}, },
"node_modules/ohash": { "node_modules/ohash": {
"version": "2.0.11", "version": "2.0.12",
"resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.12.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/on-finished": { "node_modules/on-finished": {
@ -4164,18 +4140,6 @@
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/piccolore": { "node_modules/piccolore": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmmirror.com/piccolore/-/piccolore-0.1.3.tgz", "resolved": "https://registry.npmmirror.com/piccolore/-/piccolore-0.1.3.tgz",
@ -4276,9 +4240,9 @@
} }
}, },
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "5.0.0", "version": "5.1.1",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.1.1.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 20.19.0" "node": ">= 20.19.0"
@ -4370,26 +4334,26 @@
} }
}, },
"node_modules/satteri": { "node_modules/satteri": {
"version": "0.9.5", "version": "0.10.5",
"resolved": "https://registry.npmmirror.com/satteri/-/satteri-0.9.5.tgz", "resolved": "https://registry.npmmirror.com/satteri/-/satteri-0.10.5.tgz",
"integrity": "sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w==", "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree-jsx": "^1.0.5", "@types/estree-jsx": "^1.0.5",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.5",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@types/unist": "^3.0.3" "@types/unist": "^3.0.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"@bruits/satteri-darwin-arm64": "0.9.5", "@bruits/satteri-darwin-arm64": "0.10.5",
"@bruits/satteri-darwin-x64": "0.9.5", "@bruits/satteri-darwin-x64": "0.10.5",
"@bruits/satteri-linux-arm64-gnu": "0.9.5", "@bruits/satteri-linux-arm64-gnu": "0.10.5",
"@bruits/satteri-linux-arm64-musl": "0.9.5", "@bruits/satteri-linux-arm64-musl": "0.10.5",
"@bruits/satteri-linux-x64-gnu": "0.9.5", "@bruits/satteri-linux-x64-gnu": "0.10.5",
"@bruits/satteri-linux-x64-musl": "0.9.5", "@bruits/satteri-linux-x64-musl": "0.10.5",
"@bruits/satteri-wasm32-wasi": "0.9.5", "@bruits/satteri-wasm32-wasi": "0.10.5",
"@bruits/satteri-win32-arm64-msvc": "0.9.5", "@bruits/satteri-win32-arm64-msvc": "0.10.5",
"@bruits/satteri-win32-x64-msvc": "0.9.5" "@bruits/satteri-win32-x64-msvc": "0.10.5"
} }
}, },
"node_modules/sax": { "node_modules/sax": {
@ -4502,19 +4466,19 @@
} }
}, },
"node_modules/shiki": { "node_modules/shiki": {
"version": "4.3.1", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/shiki/-/shiki-4.4.3.tgz",
"integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==", "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@shikijs/core": "4.3.1", "@shikijs/core": "4.4.3",
"@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-javascript": "4.4.3",
"@shikijs/engine-oniguruma": "4.3.1", "@shikijs/engine-oniguruma": "4.4.3",
"@shikijs/langs": "4.3.1", "@shikijs/langs": "4.4.3",
"@shikijs/themes": "4.3.1", "@shikijs/themes": "4.4.3",
"@shikijs/types": "4.3.1", "@shikijs/types": "4.4.3",
"@shikijs/vscode-textmate": "^10.0.2", "@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4" "@types/hast": "^3.0.5"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
@ -4527,9 +4491,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/smol-toml": { "node_modules/smol-toml": {
"version": "1.7.1", "version": "1.8.0",
"resolved": "https://registry.npmmirror.com/smol-toml/-/smol-toml-1.7.1.tgz", "resolved": "https://registry.npmmirror.com/smol-toml/-/smol-toml-1.8.0.tgz",
"integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
@ -4580,6 +4544,18 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/svgo": { "node_modules/svgo": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmmirror.com/svgo/-/svgo-4.0.2.tgz", "resolved": "https://registry.npmmirror.com/svgo/-/svgo-4.0.2.tgz",
@ -4605,6 +4581,15 @@
"url": "https://opencollective.com/svgo" "url": "https://opencollective.com/svgo"
} }
}, },
"node_modules/svgo/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/swiper": { "node_modules/swiper": {
"version": "11.2.10", "version": "11.2.10",
"resolved": "https://registry.npmmirror.com/swiper/-/swiper-11.2.10.tgz", "resolved": "https://registry.npmmirror.com/swiper/-/swiper-11.2.10.tgz",
@ -4659,9 +4644,9 @@
} }
}, },
"node_modules/tinyexec": { "node_modules/tinyexec": {
"version": "1.2.4", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz", "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.3.0.tgz",
"integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -4737,6 +4722,15 @@
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici": {
"version": "8.10.0",
"resolved": "https://registry.npmmirror.com/undici/-/undici-8.10.0.tgz",
"integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
}
},
"node_modules/unified": { "node_modules/unified": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", "resolved": "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz",
@ -4757,14 +4751,14 @@
} }
}, },
"node_modules/unifont": { "node_modules/unifont": {
"version": "0.7.4", "version": "0.7.5",
"resolved": "https://registry.npmmirror.com/unifont/-/unifont-0.7.4.tgz", "resolved": "https://registry.npmmirror.com/unifont/-/unifont-0.7.5.tgz",
"integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"css-tree": "^3.1.0", "css-tree": "^3.1.0",
"ofetch": "^1.5.1", "ohash": "^2.0.11",
"ohash": "^2.0.11" "undici": "^8.0.0"
} }
}, },
"node_modules/unist-util-is": { "node_modules/unist-util-is": {
@ -4945,20 +4939,6 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/vfile-location": {
"version": "5.0.3",
"resolved": "https://registry.npmmirror.com/vfile-location/-/vfile-location-5.0.3.tgz",
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": { "node_modules/vfile-message": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", "resolved": "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz",
@ -5070,16 +5050,6 @@
} }
} }
}, },
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz",
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/xxhash-wasm": { "node_modules/xxhash-wasm": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",

View File

@ -13,11 +13,11 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/alpinejs": "^1.0.0", "@astrojs/alpinejs": "^1.0.0",
"@astrojs/node": "^11.1.2", "@astrojs/node": "^11.1.4",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@types/alpinejs": "^3.13.11", "@types/alpinejs": "^3.13.11",
"alpinejs": "^3.16.1", "alpinejs": "^3.16.1",
"astro": "^7.1.6", "astro": "^7.2.4",
"swiper": "^11.2.10", "swiper": "^11.2.10",
"tailwindcss": "^4.3.3" "tailwindcss": "^4.3.3"
} }

View File

@ -1,14 +1,12 @@
--- ---
import Breadcrumb from "@/components/Breadcrumb.astro" import Breadcrumb from "@/components/Breadcrumb.astro"
import { getBrands, getYears, type BrandEntry } from "@/lib/api" import { getBrands, getSsgPopularBrands, type BrandEntry } from "@/lib/api"
import { clientHref, ROUTES } from '@/lib/routes'
import { getI18n } from '@/i18n/utils' import { getI18n } from '@/i18n/utils'
const { locale, t, getRelativeLocaleUrl } = getI18n(Astro) const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
let sidebarBrands: BrandEntry[] = [] let sidebarBrands: BrandEntry[] = []
let yearChips: number[] = []
try { try {
sidebarBrands = (await getBrands({ size: 300, only_with_articles: 1, featured: 1 })) sidebarBrands = (await getBrands({ size: 300, only_with_articles: 1, featured: 1 }))
@ -16,12 +14,55 @@ try {
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
} catch {} } catch {}
let yearSeed = [2026, 2025, 2024, 2023, 2022, 2021] // 热门品牌:构建期走 SSG 内部接口(/api/ssg/brands/popular),不可用时回落假数据先看效果
const FAKE_HOT_BRANDS = [
{ id: "hot-chanel", name: "Chanel", article_count: 342 },
{ id: "hot-dior", name: "Dior", article_count: 318 },
{ id: "hot-gucci", name: "Gucci", article_count: 287 },
{ id: "hot-prada", name: "Prada", article_count: 254 },
{ id: "hot-louisvuitton", name: "Louis Vuitton", article_count: 241 },
{ id: "hot-saintlaurent", name: "Saint Laurent", article_count: 219 },
{ id: "hot-balenciaga", name: "Balenciaga", article_count: 198 },
{ id: "hot-valentino", name: "Valentino", article_count: 176 },
{ id: "hot-givenchy", name: "Givenchy", article_count: 162 },
{ id: "hot-burberry", name: "Burberry", article_count: 153 },
{ id: "hot-fendi", name: "Fendi", article_count: 141 },
{ id: "hot-bottega", name: "Bottega Veneta", article_count: 128 },
]
let hotBrands: { id: string; name: string; article_count: number }[] = []
try { try {
const ys = await getYears(30) const ssg = await getSsgPopularBrands(12)
if (ys.length) yearChips = ys if (ssg.length) hotBrands = ssg.map((b) => ({ id: String(b.id), name: b.brand || "", article_count: 0 }))
} catch {} } catch {}
if (!yearChips.length) yearChips = yearSeed if (!hotBrands.length) hotBrands = FAKE_HOT_BRANDS
// 年份筛选:写死,按需增删(数据最早约 2005 年,如需更老年份往数组里加即可)
// 年份:从当今年份往前推 15 年(含),末项加"X年前"兜底
const currentYear = new Date().getFullYear()
const yearFloor = currentYear - 15
const yearOptions: { value: number | string; label: string }[] = []
for (let y = currentYear; y >= yearFloor; y--) {
yearOptions.push({ value: y, label: String(y) })
}
yearOptions.push({ value: `before${yearFloor}`, label: `${yearFloor}年前` })
// 系列类型 / 季节:写死静态数据(先用示例/假数据预览,确认后直接改这两个数组即可)
const collectionOptions: { value: string; label: string }[] = [
{ value: "rtw", label: "Ready-to-Wear" },
{ value: "menswear", label: "Menswear" },
{ value: "womenswear", label: "Womenswear" },
{ value: "couture", label: "Couture" },
{ value: "resort", label: "Resort" },
{ value: "pre_fall", label: "Pre-Fall" },
{ value: "bridal", label: "Bridal" },
{ value: "swimwear", label: "Swimwear" },
]
const seasonOptions: { value: string; label: string }[] = [
{ value: "spring", label: "Spring / Summer" },
{ value: "fall", label: "Fall / Winter" },
{ value: "cruise", label: "Cruise" },
{ value: "holiday", label: "Holiday" },
]
--- ---
@ -69,7 +110,6 @@ if (!yearChips.length) yearChips = yearSeed
</span> </span>
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.designers"> <div class="showsg-b" x-show="!collapsedGroups.designers">
<input class="showssearch" placeholder="Search designer…" autocomplete="off" x-model.debounce.150ms="keyword" />
<ul class="showschecks"> <ul class="showschecks">
<!-- <li> <!-- <li>
<template x-if="item.type === 'letter'"> <template x-if="item.type === 'letter'">
@ -104,11 +144,9 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.collection"> <div class="showsg-b" x-show="!collapsedGroups.collection">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
<li><label><input type="checkbox" value="rtw" x-model="selectedCt" @change="refreshFilter()" /> <span>Ready-to-Wear</span></label></li> {collectionOptions.map((o) => (
<li><label><input type="checkbox" value="menswear" x-model="selectedCt" @change="refreshFilter()" /> <span>Menswear</span></label></li> <li><label><input type="checkbox" value={o.value} x-model="selectedCt" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
<li><label><input type="checkbox" value="couture" x-model="selectedCt" @change="refreshFilter()" /> <span>Couture</span></label></li> ))}
<li><label><input type="checkbox" value="resort" x-model="selectedCt" @change="refreshFilter()" /> <span>Resort</span></label></li>
<li><label><input type="checkbox" value="pre_fall" x-model="selectedCt" @change="refreshFilter()" /> <span>Pre-Fall</span></label></li>
</ul> </ul>
</div> </div>
</section> </section>
@ -123,8 +161,9 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.season"> <div class="showsg-b" x-show="!collapsedGroups.season">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
<li><label><input type="checkbox" value="spring" x-model="selectedSeason" @change="refreshFilter()" /> <span>Spring / Summer</span></label></li> {seasonOptions.map((o) => (
<li><label><input type="checkbox" value="fall" x-model="selectedSeason" @change="refreshFilter()" /> <span>Fall / Winter</span></label></li> <li><label><input type="checkbox" value={o.value} x-model="selectedSeason" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
))}
</ul> </ul>
</div> </div>
</section> </section>
@ -139,8 +178,8 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.year"> <div class="showsg-b" x-show="!collapsedGroups.year">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
{yearChips.map((y) => ( {yearOptions.map((o) => (
<li><label><input type="checkbox" value={String(y)} x-model="selectedYear" @change="refreshFilter()" /> <span>{y}</span></label></li> <li><label><input type="checkbox" value={String(o.value)} x-model="selectedYear" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
))} ))}
</ul> </ul>
</div> </div>
@ -236,12 +275,13 @@ if (!yearChips.length) yearChips = yearSeed
/> />
</div> </div>
<div class="showsmodal-az"> <div class="showsmodal-az">
<button class="az az-hot" type="button" :class="{ active: modalLetter === 'HOT' }" @click="modalPickLetter('HOT')">HOT</button>
<template x-for="l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('')" :key="l"> <template x-for="l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('')" :key="l">
<button class="az" type="button" :class="{ active: modalLetter === l }" @click="modalPickLetter(l)" x-text="l"></button> <button class="az" type="button" :class="{ active: modalLetter === l }" @click="modalPickLetter(l)" x-text="l"></button>
</template> </template>
</div> </div>
<div class="showsmodal-body"> <div class="showsmodal-body">
<div class="showsmodal-hint" x-show="!modalKeyword && !modalLetter">热门推荐</div> <div class="showsmodal-hint" x-show="!modalKeyword && modalLetter === 'HOT'">热门推荐</div>
<div class="showsmodal-loading" x-show="modalLoading" x-html="SPIN_SVG"></div> <div class="showsmodal-loading" x-show="modalLoading" x-html="SPIN_SVG"></div>
<ul class="showsmodal-list"> <ul class="showsmodal-list">
<template x-for="b in modalResults" :key="b.id"> <template x-for="b in modalResults" :key="b.id">
@ -263,7 +303,7 @@ if (!yearChips.length) yearChips = yearSeed
</ul> </ul>
</div> </div>
<footer class="showsmodal-foot"> <footer class="showsmodal-foot">
<span class="showsmodal-counter" x-text="`已选 ${modalSelected.length} / 最多 5 个`"></span> <!-- <span class="showsmodal-counter" x-text="`已选 ${modalSelected.length} / 最多 5 个`"></span> -->
<button class="showsmodal-ok" type="button" @click="confirmBrandModal()">确定</button> <button class="showsmodal-ok" type="button" @click="confirmBrandModal()">确定</button>
</footer> </footer>
</div> </div>
@ -272,6 +312,9 @@ if (!yearChips.length) yearChips = yearSeed
</div> </div>
<script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script>
<script id="showshotbrands-data" type="application/json" set:html={JSON.stringify(hotBrands)}></script>
<style> <style>
@reference "tailwindcss"; @reference "tailwindcss";
[x-cloak] { display: none !important; } [x-cloak] { display: none !important; }
@ -337,9 +380,7 @@ if (!yearChips.length) yearChips = yearSeed
@apply text-[#999] font-normal text-[10px] tracking-[0.14em]; @apply text-[#999] font-normal text-[10px] tracking-[0.14em];
} }
.showssearch {
@apply w-full border border-[#d4d4d4] p-[7px_10px] text-[11px] uppercase tracking-[0.08em] text-black rounded-none outline-none bg-[#fafafa] focus:border-black focus:bg-white;
}
.showschecks { .showschecks {
@apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin] overscroll-contain; @apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin] overscroll-contain;
@ -489,7 +530,7 @@ if (!yearChips.length) yearChips = yearSeed
background: transparent; background: transparent;
} }
.showsmodal-panel { .showsmodal-panel {
@apply relative w-full max-w-[460px] max-h-[72vh] bg-white flex flex-col border border-black/10 pointer-events-auto; @apply relative w-full max-w-[680px] max-h-[72vh] bg-white flex flex-col border border-black/10 pointer-events-auto;
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.15); box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.15);
} }
/* 顶部小三角箭头(语言切换器同款:白底 + 浅边框) */ /* 顶部小三角箭头(语言切换器同款:白底 + 浅边框) */
@ -529,6 +570,9 @@ if (!yearChips.length) yearChips = yearSeed
.showsmodal-az .az.active { .showsmodal-az .az.active {
@apply bg-black text-white border-black; @apply bg-black text-white border-black;
} }
.showsmodal-az .az-hot {
@apply w-auto px-2 font-sans font-bold tracking-[0.12em] text-[9px];
}
.showsmodal-body { .showsmodal-body {
@apply flex-1 min-h-0 overflow-y-auto px-2 py-1 overscroll-contain; @apply flex-1 min-h-0 overflow-y-auto px-2 py-1 overscroll-contain;
} }
@ -569,21 +613,20 @@ if (!yearChips.length) yearChips = yearSeed
<script> <script>
import { getArticles, getBrands, toAbs, type ArticleItem, type BrandEntry } from "@/lib/api" import { getArticles, getBrands, toAbs, type ArticleItem, type BrandEntry } from "@/lib/api"
import { clientHref, ROUTES } from "@/lib/routes"
const ICON_USER = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><circle cx="6" cy="4" r="2" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 10c0-2 1.8-3 4-3s4 1 4 3" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
const ICON_DATE = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><rect x="2" y="3" width="8" height="7" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 6h8M4 2v2M8 2v2" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
const SPIN_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true" class="w-[22px] h-[22px] block"><g class="ring origin-[12px_12px] animate-spin" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="40 22"><circle cx="12" cy="12" r="9"></circle></g><g opacity="0.55" fill="none" stroke="currentColor" stroke-width="1"><rect x="8" y="9.6" width="8" height="5.4"></rect><circle cx="12" cy="12.3" r="1.6"></circle></g></svg>` const SPIN_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true" class="w-[22px] h-[22px] block"><g class="ring origin-[12px_12px] animate-spin" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="40 22"><circle cx="12" cy="12" r="9"></circle></g><g opacity="0.55" fill="none" stroke="currentColor" stroke-width="1"><rect x="8" y="9.6" width="8" height="5.4"></rect><circle cx="12" cy="12.3" r="1.6"></circle></g></svg>`
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("showsPage", () => { Alpine.data("showsPage", () => {
const node = document.getElementById("showsbrands-data") const node = document.getElementById("showsbrands-data")
const allBrands: BrandEntry[] = node?.textContent ? JSON.parse(node.textContent) : [] const allBrands: BrandEntry[] = node?.textContent ? JSON.parse(node.textContent) : []
const hotNode = document.getElementById("showshotbrands-data")
const hotBrands = hotNode?.textContent ? JSON.parse(hotNode.textContent) : []
return { return {
locale: '', locale: '',
allBrands, allBrands,
ICON_USER, hotBrands,
ICON_DATE,
SPIN_SVG, SPIN_SVG,
collapsedGroups: { collapsedGroups: {
designers: localStorage.getItem("showsg-designers") === "1", designers: localStorage.getItem("showsg-designers") === "1",
@ -595,7 +638,6 @@ if (!yearChips.length) yearChips = yearSeed
selectedCt: [] as string[], selectedCt: [] as string[],
selectedSeason: [] as string[], selectedSeason: [] as string[],
selectedYear: [] as string[], selectedYear: [] as string[],
keyword: "",
brandsExpanded: false, brandsExpanded: false,
// 品牌筛选弹窗状态 // 品牌筛选弹窗状态
@ -606,7 +648,7 @@ if (!yearChips.length) yearChips = yearSeed
modalLoading: false, modalLoading: false,
modalSelected: [] as number[], modalSelected: [] as number[],
get modalMaxReached() { get modalMaxReached() {
return this.modalSelected.length >= 5 return this.modalSelected.length >= 1
}, },
items: [] as (ArticleItem & { _imgLoaded?: boolean })[], items: [] as (ArticleItem & { _imgLoaded?: boolean })[],
@ -625,34 +667,24 @@ if (!yearChips.length) yearChips = yearSeed
// 计算属性:侧边栏设计师列表的分组渲染 // 计算属性:侧边栏设计师列表的分组渲染
get visibleBrandItems() { get visibleBrandItems() {
const kw = this.keyword.trim().toLowerCase()
const result: any[] = [] const result: any[] = []
const map = new Map<string, BrandEntry[]>()
for (const b of this.allBrands) {
const letter = (b.name[0] || "?").toUpperCase()
if (!map.has(letter)) map.set(letter, [])
map.get(letter)!.push(b)
}
const grouped = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
if (kw) { let shown = 0
this.allBrands.forEach((b) => { const MAX_INITIAL = 60
if (b.name.toLowerCase().includes(kw)) { for (const [letter, list] of grouped) {
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id }) if (!this.brandsExpanded && shown >= MAX_INITIAL) break
} result.push({ type: "letter", letter, key: `letter-${letter}` })
}) for (const b of list) {
} else {
const map = new Map<string, BrandEntry[]>()
for (const b of this.allBrands) {
const letter = (b.name[0] || "?").toUpperCase()
if (!map.has(letter)) map.set(letter, [])
map.get(letter)!.push(b)
}
const grouped = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
let shown = 0
const MAX_INITIAL = 60
for (const [letter, list] of grouped) {
if (!this.brandsExpanded && shown >= MAX_INITIAL) break if (!this.brandsExpanded && shown >= MAX_INITIAL) break
result.push({ type: "letter", letter, key: `letter-${letter}` }) result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
for (const b of list) { shown++
if (!this.brandsExpanded && shown >= MAX_INITIAL) break
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
shown++
}
} }
} }
return result return result
@ -671,8 +703,9 @@ if (!yearChips.length) yearChips = yearSeed
get pills() { get pills() {
const list: { label: string; clear: () => void }[] = [] const list: { label: string; clear: () => void }[] = []
if (this.selectedBrands.length) { if (this.selectedBrands.length) {
const findName = (id: any) => this.allBrands.find((b) => b.id === id)?.name ?? this.hotBrands.find((b) => b.id === id)?.name ?? `#${id}`
const labels = this.selectedBrands const labels = this.selectedBrands
.map((id) => this.allBrands.find((b) => b.id === id)?.name ?? `#${id}`) .map((id) => findName(id))
.slice(0, 4) .slice(0, 4)
const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : "" const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : ""
list.push({ list.push({
@ -728,7 +761,7 @@ if (!yearChips.length) yearChips = yearSeed
async openBrandModal() { async openBrandModal() {
this.modalSelected = [...this.selectedBrands] this.modalSelected = [...this.selectedBrands]
this.modalKeyword = "" this.modalKeyword = ""
this.modalLetter = "" this.modalLetter = "HOT"
this.modalOpen = true this.modalOpen = true
document.body.style.overflow = "hidden" document.body.style.overflow = "hidden"
await this.modalLoadHot() await this.modalLoadHot()
@ -738,12 +771,12 @@ if (!yearChips.length) yearChips = yearSeed
document.body.style.overflow = "" document.body.style.overflow = ""
}, },
async modalLoadHot() { async modalLoadHot() {
this.modalLetter = "HOT"
this.modalLoading = true this.modalLoading = true
try { try {
const list = await getBrands({ featured: 1, size: 50 }) this.modalResults = this.hotBrands.length
this.modalResults = list.length ? list : this.allBrands.slice(0, 50) ? this.hotBrands.map((b) => ({ id: b.id, name: b.name, article_count: b.article_count || 0 }))
} catch { : this.allBrands.slice(0, 50)
this.modalResults = this.allBrands.slice(0, 50)
} finally { } finally {
this.modalLoading = false this.modalLoading = false
} }
@ -768,9 +801,15 @@ if (!yearChips.length) yearChips = yearSeed
} }
}, },
async modalPickLetter(l: string) { async modalPickLetter(l: string) {
if (l === "HOT") {
this.modalKeyword = ""
await this.modalLoadHot()
return
}
this.modalLetter = l this.modalLetter = l
this.modalKeyword = "" this.modalKeyword = ""
this.modalLoading = true this.modalLoading = true
this.modalResults = []
try { try {
const letter = l === "#" ? "OTHER" : l const letter = l === "#" ? "OTHER" : l
this.modalResults = await getBrands({ letter, size: 200 }) this.modalResults = await getBrands({ letter, size: 200 })
@ -800,7 +839,6 @@ if (!yearChips.length) yearChips = yearSeed
this.selectedCt = [] this.selectedCt = []
this.selectedSeason = [] this.selectedSeason = []
this.selectedYear = [] this.selectedYear = []
this.keyword = ""
this.refreshFilter() this.refreshFilter()
}, },
@ -847,11 +885,11 @@ if (!yearChips.length) yearChips = yearSeed
// 辅助辅助工具函数 // 辅助辅助工具函数
toAbsUrl(url: string) { return toAbs(url) }, toAbsUrl(url: string) { return toAbs(url) },
itemHref(it: { id: string | number }) { return clientHref(this.locale, ROUTES.runwayLooks(it.id)) },
ctLabel(ct?: string) { // 渲染文章url
const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall" } itemHref(it: { id: string | number }) { return clientHref(this.locale, ROUTES.item(it.id)) },
return ct && map[ct] ? map[ct] : (ct || "")
}, // 渲染thumb图片列表
getCardSlots(it: ArticleItem) { getCardSlots(it: ArticleItem) {
const gallery = it.gallery || [] const gallery = it.gallery || []
const totalCount = it.image_count || 0 const totalCount = it.image_count || 0

View File

@ -70,12 +70,13 @@ const headers = {
</button> </button>
</div> </div>
<div x-show="open" x-transition.opacity.duration.150ms class="absolute left-0 top-full mt-2 z-50 min-w-[240px] bg-white border border-black/10 shadow-sm py-1"> <div x-show="open" x-transition.opacity.duration.150ms class="absolute left-0 top-full mt-2 z-50 min-w-[240px] bg-white border border-black/10 shadow-sm py-1">
{headers.runways.map((item) => ( {
<a href={item.href} class:list={[ headers.runways.map((item) => (
'block px-4 py-2 text-[12px] uppercase tracking-[0.4px] hover:bg-black/5 transition-colors', <a href={item.href} class="block px-4 py-2 text-[12px] uppercase tracking-[0.4px] hover:bg-black/5 transition-colors">
item.current ? 'font-bold' : '' {item.label}
]}>{item.label}</a> </a>
))} ))
}
</div> </div>
</div> </div>
<a href={href(locale, ROUTES.latestProjects)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a> <a href={href(locale, ROUTES.latestProjects)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
@ -85,23 +86,8 @@ const headers = {
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<!-- 语言切换器:恢复为 Alpine.js 控制开合(x-data + x-show + x-transition) --> <!-- 语言切换器:恢复为 Alpine.js 控制开合(x-data + x-show + x-transition) -->
<div <div class="relative" id="lang-switch" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
class="relative" <button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="flex items-center gap-1.5 cursor-pointer hover:opacity-60 transition-opacity duration-200 p-1 -m-1 normal-case text-[13px] tracking-[0.5px]" aria-label={t('Language')} aria-haspopup="listbox" :aria-expanded="open">
id="lang-switch"
x-data="{ open: false }"
@mouseenter="open = true"
@mouseleave="open = false"
>
<button
type="button"
@click="open = !open"
@click.outside="open = false"
@keydown.escape.window="open = false"
class="flex items-center gap-1.5 cursor-pointer hover:opacity-60 transition-opacity duration-200 p-1 -m-1 normal-case text-[13px] tracking-[0.5px]"
aria-label={t('Language')}
aria-haspopup="listbox"
:aria-expanded="open"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-[18px] h-[18px] transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-[18px] h-[18px] transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true">
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/> <line x1="2" y1="12" x2="22" y2="12"/>

View File

@ -18,10 +18,10 @@ export const ROUTES = {
register: '/register', register: '/register',
runwayLooks: '/runway-looks', runwayLooks: '/runway-looks',
// 动态路由(工厂函数) // 动态路由(工厂函数)
item: (id: string | number) => `/items/${id}`, item: (id: string | number) => `/item/${id}`,
article: (id: string | number) => `/article?id=${id}`, article: (id: string | number) => `/article?id=${id}`,
articlesByBrand: (id: string | number) => `/articles?brand_id=${id}`, articlesByBrand: (id: string | number) => `/articles?brand_id=${id}`,
// runwayLooks: (id: string | number) => `/runway-looks/${id}`, // runwayLooksDetail: (id: string | number) => `/runway-looks/${id}`,
} as const; } as const;
/** 服务端:生成带 locale 前缀的完整 URL(依赖 astro:i18n,遵循 astro.config)。 */ /** 服务端:生成带 locale 前缀的完整 URL(依赖 astro:i18n,遵循 astro.config)。 */

View File

@ -1,13 +1,12 @@
--- ---
import Layout from "@/layouts/Layout.astro" import Layout from "@/layouts/Layout.astro"
import Breadcrumb from "@/components/Breadcrumb.astro" import Breadcrumb from "@/components/Breadcrumb.astro"
import { getBrands, getYears, type BrandEntry } from "@/lib/api" import { getBrands, getSsgPopularBrands, type BrandEntry } from "@/lib/api"
import { getI18n } from '@/i18n/utils' import { getI18n } from '@/i18n/utils'
import { clientHref, ROUTES, href } from '@/lib/routes' import { clientHref, ROUTES, href } from '@/lib/routes'
const { locale } = getI18n(Astro) const { locale } = getI18n(Astro)
let sidebarBrands: BrandEntry[] = [] let sidebarBrands: BrandEntry[] = []
let yearChips: number[] = []
try { try {
sidebarBrands = (await getBrands({ size: 300, only_with_articles: 1, featured: 1 })) sidebarBrands = (await getBrands({ size: 300, only_with_articles: 1, featured: 1 }))
@ -15,12 +14,55 @@ try {
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
} catch {} } catch {}
let yearSeed = [2026, 2025, 2024, 2023, 2022, 2021] // 热门品牌:构建期走 SSG 内部接口(/api/ssg/brands/popular),不可用时回落假数据先看效果
const FAKE_HOT_BRANDS = [
{ id: "hot-chanel", name: "Chanel", article_count: 342 },
{ id: "hot-dior", name: "Dior", article_count: 318 },
{ id: "hot-gucci", name: "Gucci", article_count: 287 },
{ id: "hot-prada", name: "Prada", article_count: 254 },
{ id: "hot-louisvuitton", name: "Louis Vuitton", article_count: 241 },
{ id: "hot-saintlaurent", name: "Saint Laurent", article_count: 219 },
{ id: "hot-balenciaga", name: "Balenciaga", article_count: 198 },
{ id: "hot-valentino", name: "Valentino", article_count: 176 },
{ id: "hot-givenchy", name: "Givenchy", article_count: 162 },
{ id: "hot-burberry", name: "Burberry", article_count: 153 },
{ id: "hot-fendi", name: "Fendi", article_count: 141 },
{ id: "hot-bottega", name: "Bottega Veneta", article_count: 128 },
]
let hotBrands: { id: string; name: string; article_count: number }[] = []
try { try {
const ys = await getYears(30) const ssg = await getSsgPopularBrands(12)
if (ys.length) yearChips = ys if (ssg.length) hotBrands = ssg.map((b) => ({ id: String(b.id), name: b.brand || "", article_count: 0 }))
} catch {} } catch {}
if (!yearChips.length) yearChips = yearSeed if (!hotBrands.length) hotBrands = FAKE_HOT_BRANDS
// 年份筛选:写死,按需增删(数据最早约 2005 年,如需更老年份往数组里加即可)
// 年份:从当今年份往前推 15 年(含),末项加"X年前"兜底
const currentYear = new Date().getFullYear()
const yearFloor = currentYear - 15
const yearOptions: { value: number | string; label: string }[] = []
for (let y = currentYear; y >= yearFloor; y--) {
yearOptions.push({ value: y, label: String(y) })
}
yearOptions.push({ value: `before${yearFloor}`, label: `${yearFloor}年前` })
// 系列类型 / 季节:写死静态数据(先用示例/假数据预览,确认后直接改这两个数组即可)
const collectionOptions: { value: string; label: string }[] = [
{ value: "rtw", label: "Ready-to-Wear" },
{ value: "menswear", label: "Menswear" },
{ value: "womenswear", label: "Womenswear" },
{ value: "couture", label: "Couture" },
{ value: "resort", label: "Resort" },
{ value: "pre_fall", label: "Pre-Fall" },
{ value: "bridal", label: "Bridal" },
{ value: "swimwear", label: "Swimwear" },
]
const seasonOptions: { value: string; label: string }[] = [
{ value: "spring", label: "Spring / Summer" },
{ value: "fall", label: "Fall / Winter" },
{ value: "cruise", label: "Cruise" },
{ value: "holiday", label: "Holiday" },
]
--- ---
<Layout title="全部的 runway — Portfolio Studio"> <Layout title="全部的 runway — Portfolio Studio">
@ -72,28 +114,40 @@ if (!yearChips.length) yearChips = yearSeed
</span> </span>
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.designers"> <div class="showsg-b" x-show="!collapsedGroups.designers">
<input class="showssearch" placeholder="Search designer…" autocomplete="off" x-model.debounce.150ms="keyword" /> <div class="showshot" x-show="hotBrands.length">
<ul class="showschecks"> <div class="mb-2 text-[10px] uppercase tracking-[0.18em] text-black/45">热门品牌 · HOT</div>
<li> <div class="flex flex-wrap gap-1.5">
<!-- Letter Header --> <template x-for="b in hotBrands" :key="b.id">
<template x-if="item.type === 'letter'"> <label class="inline-flex items-center gap-1.5 border border-black/15 px-2 py-1 text-[11px] cursor-pointer transition-colors has-[:checked]:border-black has-[:checked]:bg-black has-[:checked]:text-white">
<div class="letter-head" x-text="item.letter"></div> <input type="checkbox" :value="b.id" :checked="selectedBrands.includes(b.id)" @change="toggleBrand(b.id)" />
</template> <span x-text="b.name"></span>
<!-- Brand Item -->
<template x-if="item.type === 'brand'">
<label class="flex items-center gap-2 w-full cursor-pointer">
<input
type="checkbox"
:value="item.id"
:checked="selectedBrands.includes(item.id)"
@change="toggleBrand(item.id)"
>
<span x-text="item.name"></span>
<span class="ct" x-text="item.count ? item.count : ''"></span>
</label> </label>
</template> </template>
</li> </div>
</div>
<ul class="showschecks">
<template x-for="item in visibleBrandItems" :key="item.key">
<li>
<!-- Letter Header -->
<template x-if="item.type === 'letter'">
<div class="letter-head" x-text="item.letter"></div>
</template>
<!-- Brand Item -->
<template x-if="item.type === 'brand'">
<label class="flex items-center gap-2 w-full cursor-pointer">
<input
type="checkbox"
:value="item.id"
:checked="selectedBrands.includes(item.id)"
@change="toggleBrand(item.id)"
>
<span x-text="item.name"></span>
<span class="ct" x-text="item.count ? item.count : ''"></span>
</label>
</template>
</li>
</template>
</ul> </ul>
<a class="showsmore" @click="openBrandModal()" x-text="'浏览全部品牌 +'"></a> <a class="showsmore" @click="openBrandModal()" x-text="'浏览全部品牌 +'"></a>
</div> </div>
@ -109,11 +163,9 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.collection"> <div class="showsg-b" x-show="!collapsedGroups.collection">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
<li><label><input type="checkbox" value="rtw" x-model="selectedCt" @change="refreshFilter()" /> <span>Ready-to-Wear</span></label></li> {collectionOptions.map((o) => (
<li><label><input type="checkbox" value="menswear" x-model="selectedCt" @change="refreshFilter()" /> <span>Menswear</span></label></li> <li><label><input type="checkbox" value={o.value} x-model="selectedCt" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
<li><label><input type="checkbox" value="couture" x-model="selectedCt" @change="refreshFilter()" /> <span>Couture</span></label></li> ))}
<li><label><input type="checkbox" value="resort" x-model="selectedCt" @change="refreshFilter()" /> <span>Resort</span></label></li>
<li><label><input type="checkbox" value="pre_fall" x-model="selectedCt" @change="refreshFilter()" /> <span>Pre-Fall</span></label></li>
</ul> </ul>
</div> </div>
</section> </section>
@ -128,8 +180,9 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.season"> <div class="showsg-b" x-show="!collapsedGroups.season">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
<li><label><input type="checkbox" value="spring" x-model="selectedSeason" @change="refreshFilter()" /> <span>Spring / Summer</span></label></li> {seasonOptions.map((o) => (
<li><label><input type="checkbox" value="fall" x-model="selectedSeason" @change="refreshFilter()" /> <span>Fall / Winter</span></label></li> <li><label><input type="checkbox" value={o.value} x-model="selectedSeason" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
))}
</ul> </ul>
</div> </div>
</section> </section>
@ -144,8 +197,8 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.year"> <div class="showsg-b" x-show="!collapsedGroups.year">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
{yearChips.map((y) => ( {yearOptions.map((o) => (
<li><label><input type="checkbox" value={String(y)} x-model="selectedYear" @change="refreshFilter()" /> <span>{y}</span></label></li> <li><label><input type="checkbox" value={String(o.value)} x-model="selectedYear" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
))} ))}
</ul> </ul>
</div> </div>
@ -247,12 +300,13 @@ if (!yearChips.length) yearChips = yearSeed
/> />
</div> </div>
<div class="showsmodal-az"> <div class="showsmodal-az">
<button class="az az-hot" type="button" :class="{ active: modalLetter === 'HOT' }" @click="modalPickLetter('HOT')">HOT</button>
<template x-for="l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('')" :key="l"> <template x-for="l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('')" :key="l">
<button class="az" type="button" :class="{ active: modalLetter === l }" @click="modalPickLetter(l)" x-text="l"></button> <button class="az" type="button" :class="{ active: modalLetter === l }" @click="modalPickLetter(l)" x-text="l"></button>
</template> </template>
</div> </div>
<div class="showsmodal-body"> <div class="showsmodal-body">
<div class="showsmodal-hint" x-show="!modalKeyword && !modalLetter">热门推荐</div> <div class="showsmodal-hint" x-show="!modalKeyword && modalLetter === 'HOT'">热门推荐</div>
<div class="showsmodal-loading" x-show="modalLoading" x-html="SPIN_SVG"></div> <div class="showsmodal-loading" x-show="modalLoading" x-html="SPIN_SVG"></div>
<ul class="showsmodal-list"> <ul class="showsmodal-list">
<template x-for="b in modalResults" :key="b.id"> <template x-for="b in modalResults" :key="b.id">
@ -262,7 +316,6 @@ if (!yearChips.length) yearChips = yearSeed
type="checkbox" type="checkbox"
:value="b.id" :value="b.id"
:checked="modalSelected.includes(b.id)" :checked="modalSelected.includes(b.id)"
:disabled="!modalSelected.includes(b.id) && modalMaxReached"
@change="toggleModalBrand(b.id)" @change="toggleModalBrand(b.id)"
/> />
<span class="showsmodal-name" x-text="b.name"></span> <span class="showsmodal-name" x-text="b.name"></span>
@ -274,7 +327,7 @@ if (!yearChips.length) yearChips = yearSeed
</ul> </ul>
</div> </div>
<footer class="showsmodal-foot"> <footer class="showsmodal-foot">
<span class="showsmodal-counter" x-text="`已选 ${modalSelected.length} / 最多 5 个`"></span> <span class="showsmodal-counter" x-text="`已选 ${modalSelected.length} / 1 个品牌`"></span>
<button class="showsmodal-ok" type="button" @click="confirmBrandModal()">确定</button> <button class="showsmodal-ok" type="button" @click="confirmBrandModal()">确定</button>
</footer> </footer>
</div> </div>
@ -284,6 +337,7 @@ if (!yearChips.length) yearChips = yearSeed
</Layout> </Layout>
<script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script> <script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script>
<script id="showshotbrands-data" type="application/json" set:html={JSON.stringify(hotBrands)}></script>
<style> <style>
@reference "tailwindcss"; @reference "tailwindcss";
@ -350,9 +404,7 @@ if (!yearChips.length) yearChips = yearSeed
@apply text-[#999] font-normal text-[10px] tracking-[0.14em]; @apply text-[#999] font-normal text-[10px] tracking-[0.14em];
} }
.showssearch {
@apply w-full border border-[#d4d4d4] p-[7px_10px] text-[11px] uppercase tracking-[0.08em] text-black rounded-none outline-none bg-[#fafafa] focus:border-black focus:bg-white;
}
.showschecks { .showschecks {
@apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin]; @apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin];
@ -542,6 +594,9 @@ if (!yearChips.length) yearChips = yearSeed
.showsmodal-az .az.active { .showsmodal-az .az.active {
@apply bg-black text-white border-black; @apply bg-black text-white border-black;
} }
.showsmodal-az .az-hot {
@apply w-auto px-2 font-sans font-bold tracking-[0.12em] text-[9px];
}
.showsmodal-body { .showsmodal-body {
@apply flex-1 min-h-0 overflow-y-auto px-2 py-1; @apply flex-1 min-h-0 overflow-y-auto px-2 py-1;
} }
@ -582,6 +637,7 @@ if (!yearChips.length) yearChips = yearSeed
<script> <script>
import Alpine from "alpinejs" import Alpine from "alpinejs"
import { getArticles, getBrands, toAbs, type ArticleItem, type BrandEntry } from "@/lib/api" import { getArticles, getBrands, toAbs, type ArticleItem, type BrandEntry } from "@/lib/api"
import { clientHref, ROUTES } from "@/lib/routes"
const ICON_USER = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><circle cx="6" cy="4" r="2" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 10c0-2 1.8-3 4-3s4 1 4 3" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>` const ICON_USER = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><circle cx="6" cy="4" r="2" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 10c0-2 1.8-3 4-3s4 1 4 3" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
const ICON_DATE = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><rect x="2" y="3" width="8" height="7" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 6h8M4 2v2M8 2v2" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>` const ICON_DATE = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><rect x="2" y="3" width="8" height="7" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 6h8M4 2v2M8 2v2" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
@ -591,9 +647,12 @@ if (!yearChips.length) yearChips = yearSeed
Alpine.data("showsPage", () => { Alpine.data("showsPage", () => {
const node = document.getElementById("showsbrands-data") const node = document.getElementById("showsbrands-data")
const allBrands: BrandEntry[] = node?.textContent ? JSON.parse(node.textContent) : [] const allBrands: BrandEntry[] = node?.textContent ? JSON.parse(node.textContent) : []
const hotNode = document.getElementById("showshotbrands-data")
const hotBrands = hotNode?.textContent ? JSON.parse(hotNode.textContent) : []
return { return {
allBrands, allBrands,
hotBrands,
locale: '', locale: '',
ICON_USER, ICON_USER,
ICON_DATE, ICON_DATE,
@ -608,7 +667,6 @@ if (!yearChips.length) yearChips = yearSeed
selectedCt: [] as string[], selectedCt: [] as string[],
selectedSeason: [] as string[], selectedSeason: [] as string[],
selectedYear: [] as string[], selectedYear: [] as string[],
keyword: "",
brandsExpanded: false, brandsExpanded: false,
// 品牌筛选弹窗状态 // 品牌筛选弹窗状态
@ -618,9 +676,6 @@ if (!yearChips.length) yearChips = yearSeed
modalResults: [] as BrandEntry[], modalResults: [] as BrandEntry[],
modalLoading: false, modalLoading: false,
modalSelected: [] as number[], modalSelected: [] as number[],
get modalMaxReached() {
return this.modalSelected.length >= 5
},
items: [] as (ArticleItem & { _imgLoaded?: boolean })[], items: [] as (ArticleItem & { _imgLoaded?: boolean })[],
loading: false, loading: false,
@ -638,34 +693,24 @@ if (!yearChips.length) yearChips = yearSeed
// 计算属性:侧边栏设计师列表的分组渲染 // 计算属性:侧边栏设计师列表的分组渲染
get visibleBrandItems() { get visibleBrandItems() {
const kw = this.keyword.trim().toLowerCase()
const result: any[] = [] const result: any[] = []
const map = new Map<string, BrandEntry[]>()
for (const b of this.allBrands) {
const letter = (b.name[0] || "?").toUpperCase()
if (!map.has(letter)) map.set(letter, [])
map.get(letter)!.push(b)
}
const grouped = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
if (kw) { let shown = 0
this.allBrands.forEach((b) => { const MAX_INITIAL = 60
if (b.name.toLowerCase().includes(kw)) { for (const [letter, list] of grouped) {
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id }) if (!this.brandsExpanded && shown >= MAX_INITIAL) break
} result.push({ type: "letter", letter, key: `letter-${letter}` })
}) for (const b of list) {
} else {
const map = new Map<string, BrandEntry[]>()
for (const b of this.allBrands) {
const letter = (b.name[0] || "?").toUpperCase()
if (!map.has(letter)) map.set(letter, [])
map.get(letter)!.push(b)
}
const grouped = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
let shown = 0
const MAX_INITIAL = 60
for (const [letter, list] of grouped) {
if (!this.brandsExpanded && shown >= MAX_INITIAL) break if (!this.brandsExpanded && shown >= MAX_INITIAL) break
result.push({ type: "letter", letter, key: `letter-${letter}` }) result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
for (const b of list) { shown++
if (!this.brandsExpanded && shown >= MAX_INITIAL) break
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
shown++
}
} }
} }
return result return result
@ -693,8 +738,9 @@ if (!yearChips.length) yearChips = yearSeed
get pills() { get pills() {
const list: { label: string; clear: () => void }[] = [] const list: { label: string; clear: () => void }[] = []
if (this.selectedBrands.length) { if (this.selectedBrands.length) {
const findName = (id: any) => this.allBrands.find((b) => b.id === id)?.name ?? this.hotBrands.find((b) => b.id === id)?.name ?? `#${id}`
const labels = this.selectedBrands const labels = this.selectedBrands
.map((id) => this.allBrands.find((b) => b.id === id)?.name ?? `#${id}`) .map((id) => findName(id))
.slice(0, 4) .slice(0, 4)
const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : "" const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : ""
list.push({ list.push({
@ -736,9 +782,8 @@ if (!yearChips.length) yearChips = yearSeed
}, },
toggleBrand(id: number) { toggleBrand(id: number) {
const idx = this.selectedBrands.indexOf(id) if (this.selectedBrands.includes(id)) this.selectedBrands = []
if (idx >= 0) this.selectedBrands.splice(idx, 1) else this.selectedBrands = [id]
else this.selectedBrands.push(id)
this.refreshFilter() this.refreshFilter()
}, },
@ -750,7 +795,7 @@ if (!yearChips.length) yearChips = yearSeed
async openBrandModal() { async openBrandModal() {
this.modalSelected = [...this.selectedBrands] this.modalSelected = [...this.selectedBrands]
this.modalKeyword = "" this.modalKeyword = ""
this.modalLetter = "" this.modalLetter = "HOT"
this.modalOpen = true this.modalOpen = true
await this.modalLoadHot() await this.modalLoadHot()
}, },
@ -758,12 +803,12 @@ if (!yearChips.length) yearChips = yearSeed
this.modalOpen = false this.modalOpen = false
}, },
async modalLoadHot() { async modalLoadHot() {
this.modalLetter = "HOT"
this.modalLoading = true this.modalLoading = true
try { try {
const list = await getBrands({ featured: 1, size: 50 }) this.modalResults = this.hotBrands.length
this.modalResults = list.length ? list : this.allBrands.slice(0, 50) ? this.hotBrands.map((b) => ({ id: b.id, name: b.name, article_count: b.article_count || 0 }))
} catch { : this.allBrands.slice(0, 50)
this.modalResults = this.allBrands.slice(0, 50)
} finally { } finally {
this.modalLoading = false this.modalLoading = false
} }
@ -787,6 +832,11 @@ if (!yearChips.length) yearChips = yearSeed
} }
}, },
async modalPickLetter(l: string) { async modalPickLetter(l: string) {
if (l === "HOT") {
this.modalKeyword = ""
await this.modalLoadHot()
return
}
this.modalLetter = l this.modalLetter = l
this.modalKeyword = "" this.modalKeyword = ""
this.modalLoading = true this.modalLoading = true
@ -800,12 +850,8 @@ if (!yearChips.length) yearChips = yearSeed
} }
}, },
toggleModalBrand(id: number) { toggleModalBrand(id: number) {
const idx = this.modalSelected.indexOf(id) if (this.modalSelected.includes(id)) this.modalSelected = []
if (idx >= 0) this.modalSelected.splice(idx, 1) else this.modalSelected = [id]
else {
if (this.modalSelected.length >= 5) return
this.modalSelected.push(id)
}
}, },
confirmBrandModal() { confirmBrandModal() {
this.selectedBrands = [...this.modalSelected] this.selectedBrands = [...this.modalSelected]
@ -818,7 +864,6 @@ if (!yearChips.length) yearChips = yearSeed
this.selectedCt = [] this.selectedCt = []
this.selectedSeason = [] this.selectedSeason = []
this.selectedYear = [] this.selectedYear = []
this.keyword = ""
this.refreshFilter() this.refreshFilter()
}, },
@ -873,7 +918,7 @@ if (!yearChips.length) yearChips = yearSeed
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}` return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`
}, },
ctLabel(ct?: string) { ctLabel(ct?: string) {
const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall" } const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", womenswear: "Womenswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall", bridal: "Bridal", swimwear: "Swimwear" }
return ct && map[ct] ? map[ct] : (ct || "") return ct && map[ct] ? map[ct] : (ct || "")
}, },
getCardSlots(it: ArticleItem) { getCardSlots(it: ArticleItem) {

View File

@ -1,13 +1,12 @@
--- ---
import Layout from "@/layouts/Layout.astro" import Layout from "@/layouts/Layout.astro"
import Breadcrumb from "@/components/Breadcrumb.astro" import Breadcrumb from "@/components/Breadcrumb.astro"
import { getBrands, getYears, type BrandEntry } from "@/lib/api" import { getBrands, getSsgPopularBrands, type BrandEntry } from "@/lib/api"
import { getI18n } from '@/i18n/utils' import { getI18n } from '@/i18n/utils'
import { clientHref, ROUTES, href } from '@/lib/routes' import { clientHref, ROUTES, href } from '@/lib/routes'
const { locale } = getI18n(Astro) const { locale } = getI18n(Astro)
let sidebarBrands: BrandEntry[] = [] let sidebarBrands: BrandEntry[] = []
let yearChips: number[] = []
try { try {
sidebarBrands = (await getBrands({ size: 300, only_with_articles: 1, featured: 1 })) sidebarBrands = (await getBrands({ size: 300, only_with_articles: 1, featured: 1 }))
@ -15,12 +14,55 @@ try {
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
} catch {} } catch {}
let yearSeed = [2026, 2025, 2024, 2023, 2022, 2021] // 热门品牌:构建期走 SSG 内部接口(/api/ssg/brands/popular),不可用时回落假数据先看效果
const FAKE_HOT_BRANDS = [
{ id: "hot-chanel", name: "Chanel", article_count: 342 },
{ id: "hot-dior", name: "Dior", article_count: 318 },
{ id: "hot-gucci", name: "Gucci", article_count: 287 },
{ id: "hot-prada", name: "Prada", article_count: 254 },
{ id: "hot-louisvuitton", name: "Louis Vuitton", article_count: 241 },
{ id: "hot-saintlaurent", name: "Saint Laurent", article_count: 219 },
{ id: "hot-balenciaga", name: "Balenciaga", article_count: 198 },
{ id: "hot-valentino", name: "Valentino", article_count: 176 },
{ id: "hot-givenchy", name: "Givenchy", article_count: 162 },
{ id: "hot-burberry", name: "Burberry", article_count: 153 },
{ id: "hot-fendi", name: "Fendi", article_count: 141 },
{ id: "hot-bottega", name: "Bottega Veneta", article_count: 128 },
]
let hotBrands: { id: string; name: string; article_count: number }[] = []
try { try {
const ys = await getYears(30) const ssg = await getSsgPopularBrands(12)
if (ys.length) yearChips = ys if (ssg.length) hotBrands = ssg.map((b) => ({ id: String(b.id), name: b.brand || "", article_count: 0 }))
} catch {} } catch {}
if (!yearChips.length) yearChips = yearSeed if (!hotBrands.length) hotBrands = FAKE_HOT_BRANDS
// 年份筛选:写死,按需增删(数据最早约 2005 年,如需更老年份往数组里加即可)
// 年份:从当今年份往前推 15 年(含),末项加"X年前"兜底
const currentYear = new Date().getFullYear()
const yearFloor = currentYear - 15
const yearOptions: { value: number | string; label: string }[] = []
for (let y = currentYear; y >= yearFloor; y--) {
yearOptions.push({ value: y, label: String(y) })
}
yearOptions.push({ value: `before${yearFloor}`, label: `${yearFloor}年前` })
// 系列类型 / 季节:写死静态数据(先用示例/假数据预览,确认后直接改这两个数组即可)
const collectionOptions: { value: string; label: string }[] = [
{ value: "rtw", label: "Ready-to-Wear" },
{ value: "menswear", label: "Menswear" },
{ value: "womenswear", label: "Womenswear" },
{ value: "couture", label: "Couture" },
{ value: "resort", label: "Resort" },
{ value: "pre_fall", label: "Pre-Fall" },
{ value: "bridal", label: "Bridal" },
{ value: "swimwear", label: "Swimwear" },
]
const seasonOptions: { value: string; label: string }[] = [
{ value: "spring", label: "Spring / Summer" },
{ value: "fall", label: "Fall / Winter" },
{ value: "cruise", label: "Cruise" },
{ value: "holiday", label: "Holiday" },
]
--- ---
<Layout title="全部的 runway — Portfolio Studio"> <Layout title="全部的 runway — Portfolio Studio">
@ -86,30 +128,42 @@ if (!yearChips.length) yearChips = yearSeed
</span> </span>
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.designers"> <div class="showsg-b" x-show="!collapsedGroups.designers">
<input class="showssearch" placeholder="Search designer…" autocomplete="off" x-model.debounce.150ms="keyword" /> <div class="showshot" x-show="hotBrands.length">
<ul class="showschecks"> <div class="mb-2 text-[10px] uppercase tracking-[0.18em] text-black/45">HOT · Popular</div>
<li> <div class="flex flex-wrap gap-1.5">
<!-- Letter Header --> <template x-for="b in hotBrands" :key="b.id">
<template x-if="item.type === 'letter'"> <label class="inline-flex items-center gap-1.5 border border-black/15 px-2 py-1 text-[11px] cursor-pointer transition-colors has-[:checked]:border-black has-[:checked]:bg-black has-[:checked]:text-white">
<div class="letter-head" x-text="item.letter"></div> <input type="checkbox" :value="b.id" :checked="selectedBrands.includes(b.id)" @change="toggleBrand(b.id)" />
</template> <span x-text="b.name"></span>
<!-- Brand Item -->
<template x-if="item.type === 'brand'">
<label class="flex items-center gap-2 w-full cursor-pointer">
<input
type="checkbox"
:value="item.id"
:checked="selectedBrands.includes(item.id)"
@change="toggleBrand(item.id)"
>
<span x-text="item.name"></span>
<span class="ct" x-text="item.count ? item.count : ''"></span>
</label> </label>
</template> </template>
</li> </div>
</div>
<ul class="showschecks">
<template x-for="item in visibleBrandItems" :key="item.key">
<li>
<!-- Letter Header -->
<template x-if="item.type === 'letter'">
<div class="letter-head" x-text="item.letter"></div>
</template>
<!-- Brand Item -->
<template x-if="item.type === 'brand'">
<label class="flex items-center gap-2 w-full cursor-pointer">
<input
type="checkbox"
:value="item.id"
:checked="selectedBrands.includes(item.id)"
@change="toggleBrand(item.id)"
>
<span x-text="item.name"></span>
<span class="ct" x-text="item.count ? item.count : ''"></span>
</label>
</template>
</li>
</template>
</ul> </ul>
<a class="showsmore" x-show="hasMoreBrands && !keyword" @click="brandsExpanded = !brandsExpanded" x-text="brandsExpanded ? 'Show fewer ▴' : `Show all ${allBrands.length} designers ▾`"></a> <a class="showsmore" x-show="hasMoreBrands" @click="brandsExpanded = !brandsExpanded" x-text="brandsExpanded ? 'Show fewer ▴' : `Show all ${allBrands.length} designers ▾`"></a>
</div> </div>
</section> </section>
@ -123,11 +177,9 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.collection"> <div class="showsg-b" x-show="!collapsedGroups.collection">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
<li><label><input type="checkbox" value="rtw" x-model="selectedCt" @change="refreshFilter()" /> <span>Ready-to-Wear</span></label></li> {collectionOptions.map((o) => (
<li><label><input type="checkbox" value="menswear" x-model="selectedCt" @change="refreshFilter()" /> <span>Menswear</span></label></li> <li><label><input type="checkbox" value={o.value} x-model="selectedCt" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
<li><label><input type="checkbox" value="couture" x-model="selectedCt" @change="refreshFilter()" /> <span>Couture</span></label></li> ))}
<li><label><input type="checkbox" value="resort" x-model="selectedCt" @change="refreshFilter()" /> <span>Resort</span></label></li>
<li><label><input type="checkbox" value="pre_fall" x-model="selectedCt" @change="refreshFilter()" /> <span>Pre-Fall</span></label></li>
</ul> </ul>
</div> </div>
</section> </section>
@ -142,8 +194,9 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.season"> <div class="showsg-b" x-show="!collapsedGroups.season">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
<li><label><input type="checkbox" value="spring" x-model="selectedSeason" @change="refreshFilter()" /> <span>Spring / Summer</span></label></li> {seasonOptions.map((o) => (
<li><label><input type="checkbox" value="fall" x-model="selectedSeason" @change="refreshFilter()" /> <span>Fall / Winter</span></label></li> <li><label><input type="checkbox" value={o.value} x-model="selectedSeason" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
))}
</ul> </ul>
</div> </div>
</section> </section>
@ -158,8 +211,8 @@ if (!yearChips.length) yearChips = yearSeed
</header> </header>
<div class="showsg-b" x-show="!collapsedGroups.year"> <div class="showsg-b" x-show="!collapsedGroups.year">
<ul class="showschecks showschecks--flat"> <ul class="showschecks showschecks--flat">
{yearChips.map((y) => ( {yearOptions.map((o) => (
<li><label><input type="checkbox" value={String(y)} x-model="selectedYear" @change="refreshFilter()" /> <span>{y}</span></label></li> <li><label><input type="checkbox" value={String(o.value)} x-model="selectedYear" @change="refreshFilter()" /> <span>{o.label}</span></label></li>
))} ))}
</ul> </ul>
</div> </div>
@ -245,6 +298,7 @@ if (!yearChips.length) yearChips = yearSeed
</Layout> </Layout>
<script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script> <script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script>
<script id="showshotbrands-data" type="application/json" set:html={JSON.stringify(hotBrands)}></script>
<style> <style>
@reference "tailwindcss"; @reference "tailwindcss";
@ -330,9 +384,7 @@ if (!yearChips.length) yearChips = yearSeed
@apply text-[#999] font-normal text-[10px] tracking-[0.14em]; @apply text-[#999] font-normal text-[10px] tracking-[0.14em];
} }
.showssearch {
@apply w-full border border-[#d4d4d4] p-[7px_10px] text-[11px] uppercase tracking-[0.08em] text-black rounded-none outline-none bg-[#fafafa] focus:border-black focus:bg-white;
}
.showschecks { .showschecks {
@apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin]; @apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin];
@ -479,6 +531,7 @@ if (!yearChips.length) yearChips = yearSeed
<script> <script>
import Alpine from "alpinejs" import Alpine from "alpinejs"
import { getArticles, toAbs, type ArticleItem, type BrandEntry } from "@/lib/api" import { getArticles, toAbs, type ArticleItem, type BrandEntry } from "@/lib/api"
import { clientHref, ROUTES } from "@/lib/routes"
const ICON_USER = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><circle cx="6" cy="4" r="2" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 10c0-2 1.8-3 4-3s4 1 4 3" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>` const ICON_USER = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><circle cx="6" cy="4" r="2" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 10c0-2 1.8-3 4-3s4 1 4 3" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
const ICON_DATE = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><rect x="2" y="3" width="8" height="7" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 6h8M4 2v2M8 2v2" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>` const ICON_DATE = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><rect x="2" y="3" width="8" height="7" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 6h8M4 2v2M8 2v2" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
@ -488,9 +541,12 @@ if (!yearChips.length) yearChips = yearSeed
Alpine.data("showsPage", () => { Alpine.data("showsPage", () => {
const node = document.getElementById("showsbrands-data") const node = document.getElementById("showsbrands-data")
const allBrands: BrandEntry[] = node?.textContent ? JSON.parse(node.textContent) : [] const allBrands: BrandEntry[] = node?.textContent ? JSON.parse(node.textContent) : []
const hotNode = document.getElementById("showshotbrands-data")
const hotBrands = hotNode?.textContent ? JSON.parse(hotNode.textContent) : []
return { return {
allBrands, allBrands,
hotBrands,
locale: '', locale: '',
ICON_USER, ICON_USER,
ICON_DATE, ICON_DATE,
@ -506,7 +562,6 @@ if (!yearChips.length) yearChips = yearSeed
selectedCt: [] as string[], selectedCt: [] as string[],
selectedSeason: [] as string[], selectedSeason: [] as string[],
selectedYear: [] as string[], selectedYear: [] as string[],
keyword: "",
brandsExpanded: false, brandsExpanded: false,
items: [] as (ArticleItem & { _imgLoaded?: boolean })[], items: [] as (ArticleItem & { _imgLoaded?: boolean })[],
@ -525,34 +580,24 @@ if (!yearChips.length) yearChips = yearSeed
// 计算属性:侧边栏设计师列表的分组渲染 // 计算属性:侧边栏设计师列表的分组渲染
get visibleBrandItems() { get visibleBrandItems() {
const kw = this.keyword.trim().toLowerCase()
const result: any[] = [] const result: any[] = []
const map = new Map<string, BrandEntry[]>()
for (const b of this.allBrands) {
const letter = (b.name[0] || "?").toUpperCase()
if (!map.has(letter)) map.set(letter, [])
map.get(letter)!.push(b)
}
const grouped = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
if (kw) { let shown = 0
this.allBrands.forEach((b) => { const MAX_INITIAL = 60
if (b.name.toLowerCase().includes(kw)) { for (const [letter, list] of grouped) {
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id }) if (!this.brandsExpanded && shown >= MAX_INITIAL) break
} result.push({ type: "letter", letter, key: `letter-${letter}` })
}) for (const b of list) {
} else {
const map = new Map<string, BrandEntry[]>()
for (const b of this.allBrands) {
const letter = (b.name[0] || "?").toUpperCase()
if (!map.has(letter)) map.set(letter, [])
map.get(letter)!.push(b)
}
const grouped = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
let shown = 0
const MAX_INITIAL = 60
for (const [letter, list] of grouped) {
if (!this.brandsExpanded && shown >= MAX_INITIAL) break if (!this.brandsExpanded && shown >= MAX_INITIAL) break
result.push({ type: "letter", letter, key: `letter-${letter}` }) result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
for (const b of list) { shown++
if (!this.brandsExpanded && shown >= MAX_INITIAL) break
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
shown++
}
} }
} }
return result return result
@ -580,8 +625,9 @@ if (!yearChips.length) yearChips = yearSeed
get pills() { get pills() {
const list: { label: string; clear: () => void }[] = [] const list: { label: string; clear: () => void }[] = []
if (this.selectedBrands.length) { if (this.selectedBrands.length) {
const findName = (id: any) => this.allBrands.find((b) => b.id === id)?.name ?? this.hotBrands.find((b) => b.id === id)?.name ?? `#${id}`
const labels = this.selectedBrands const labels = this.selectedBrands
.map((id) => this.allBrands.find((b) => b.id === id)?.name ?? `#${id}`) .map((id) => findName(id))
.slice(0, 4) .slice(0, 4)
const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : "" const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : ""
list.push({ list.push({
@ -628,10 +674,10 @@ if (!yearChips.length) yearChips = yearSeed
localStorage.setItem(`showsg-${group}`, this.collapsedGroups[group] ? "1" : "") localStorage.setItem(`showsg-${group}`, this.collapsedGroups[group] ? "1" : "")
}, },
// 单选:每次只保留一个品牌,再次点击已选项则取消
toggleBrand(id: number) { toggleBrand(id: number) {
const idx = this.selectedBrands.indexOf(id) if (this.selectedBrands.includes(id)) this.selectedBrands = []
if (idx >= 0) this.selectedBrands.splice(idx, 1) else this.selectedBrands = [id]
else this.selectedBrands.push(id)
this.refreshFilter() this.refreshFilter()
}, },
@ -644,7 +690,6 @@ if (!yearChips.length) yearChips = yearSeed
this.selectedCt = [] this.selectedCt = []
this.selectedSeason = [] this.selectedSeason = []
this.selectedYear = [] this.selectedYear = []
this.keyword = ""
this.refreshFilter() this.refreshFilter()
}, },
@ -699,7 +744,7 @@ if (!yearChips.length) yearChips = yearSeed
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}` return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`
}, },
ctLabel(ct?: string) { ctLabel(ct?: string) {
const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall" } const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", womenswear: "Womenswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall", bridal: "Bridal", swimwear: "Swimwear" }
return ct && map[ct] ? map[ct] : (ct || "") return ct && map[ct] ? map[ct] : (ct || "")
}, },
getCardSlots(it: ArticleItem) { getCardSlots(it: ArticleItem) {