From 9754d61ffeb50d2daca24787efe468aa3140e61e Mon Sep 17 00:00:00 2001 From: Spirale <2635131263@qq.com> Date: Wed, 2 Sep 2026 23:53:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(ts):=20=E5=AE=9E=E7=8E=B0=20Polaris=20?= =?UTF-8?q?=E5=8F=AF=E4=BA=A4=E4=BA=92=E9=A6=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 8 + ts/web/nextjs/polaris/.prettierignore | 5 + ts/web/nextjs/polaris/AGENTS.md | 9 + ts/web/nextjs/polaris/CLAUDE.md | 1 + ts/web/nextjs/polaris/components.json | 21 + .../{ => docs/assets}/Polaris页面设计图.png | Bin .../design/2026-08-26-Polaris-首页实现规格.md | 453 + .../design}/Polaris 分类页设计文档.md | 34 +- .../{ => docs/design}/Polaris 项目说明文档.md | 39 +- .../{ => docs/design}/Polaris 首页设计文档.md | 26 +- .../plans/2026-08-26-polaris-homepage.md | 3355 +++++++ ts/web/nextjs/polaris/eslint.config.mjs | 9 + ts/web/nextjs/polaris/next-env.d.ts | 7 + ts/web/nextjs/polaris/next.config.ts | 11 + ts/web/nextjs/polaris/package.json | 52 + ts/web/nextjs/polaris/pnpm-lock.yaml | 7945 +++++++++++++++++ ts/web/nextjs/polaris/pnpm-workspace.yaml | 5 + ts/web/nextjs/polaris/postcss.config.mjs | 7 + ts/web/nextjs/polaris/prettier.config.mjs | 9 + .../polaris/{ => public/brand}/logo.svg | 0 ts/web/nextjs/polaris/src/app/globals.css | 635 ++ ts/web/nextjs/polaris/src/app/layout.tsx | 26 + ts/web/nextjs/polaris/src/app/page.tsx | 27 + .../src/components/shared/glass-panel.tsx | 18 + .../components/shared/icon-candidates.test.ts | 76 + .../src/components/shared/icon-candidates.ts | 62 + .../src/components/shared/simple-icon-map.ts | 44 + .../src/components/shared/site-icon.test.tsx | 84 + .../src/components/shared/site-icon.tsx | 111 + .../polaris/src/components/ui/button.tsx | 50 + .../src/components/ui/dropdown-menu.tsx | 194 + .../polaris/src/components/ui/tooltip.tsx | 48 + .../nextjs/polaris/src/data/categories.json | 51 + ts/web/nextjs/polaris/src/data/data.test.ts | 89 + ts/web/nextjs/polaris/src/data/links.json | 230 + ts/web/nextjs/polaris/src/data/settings.json | 17 + .../home/components/clock-greeting.test.tsx | 60 + .../home/components/clock-greeting.tsx | 34 + .../features/home/components/home-content.tsx | 34 + .../home/components/local-search-results.tsx | 53 + .../home/components/quick-links.test.tsx | 55 + .../features/home/components/quick-links.tsx | 26 + .../home/components/search-box.test.tsx | 172 + .../features/home/components/search-box.tsx | 217 + .../home/components/search-provider-menu.tsx | 85 + .../src/features/home/hooks/use-clock.ts | 61 + .../src/features/home/lib/date-time.test.ts | 43 + .../src/features/home/lib/date-time.ts | 34 + .../features/home/lib/local-search.test.ts | 112 + .../src/features/home/lib/local-search.ts | 74 + .../home/lib/provider-preference.test.ts | 14 + .../features/home/lib/provider-preference.ts | 11 + .../home/lib/search-providers.test.ts | 34 + .../src/features/home/lib/search-providers.ts | 30 + .../navigation/components/app-shell.test.tsx | 162 + .../navigation/components/app-shell.tsx | 127 + .../navigation/components/category-icon.tsx | 25 + .../navigation/components/sidebar.tsx | 105 + .../navigation/lib/sidebar-preference.test.ts | 15 + .../navigation/lib/sidebar-preference.ts | 10 + ts/web/nextjs/polaris/src/lib/utils.ts | 6 + .../nextjs/polaris/src/services/categories.ts | 10 + ts/web/nextjs/polaris/src/services/links.ts | 25 + .../polaris/src/services/services.test.ts | 81 + .../nextjs/polaris/src/services/settings.ts | 79 + ts/web/nextjs/polaris/src/test/setup.ts | 7 + ts/web/nextjs/polaris/src/types/navigation.ts | 21 + ts/web/nextjs/polaris/src/types/search.ts | 12 + ts/web/nextjs/polaris/src/types/settings.ts | 19 + ts/web/nextjs/polaris/tsconfig.json | 33 + ts/web/nextjs/polaris/vitest.config.ts | 15 + 71 files changed, 15613 insertions(+), 46 deletions(-) create mode 100644 ts/web/nextjs/polaris/.prettierignore create mode 100644 ts/web/nextjs/polaris/AGENTS.md create mode 100644 ts/web/nextjs/polaris/CLAUDE.md create mode 100644 ts/web/nextjs/polaris/components.json rename ts/web/nextjs/polaris/{ => docs/assets}/Polaris页面设计图.png (100%) create mode 100644 ts/web/nextjs/polaris/docs/design/2026-08-26-Polaris-首页实现规格.md rename ts/web/nextjs/polaris/{ => docs/design}/Polaris 分类页设计文档.md (98%) rename ts/web/nextjs/polaris/{ => docs/design}/Polaris 项目说明文档.md (77%) rename ts/web/nextjs/polaris/{ => docs/design}/Polaris 首页设计文档.md (95%) create mode 100644 ts/web/nextjs/polaris/docs/superpowers/plans/2026-08-26-polaris-homepage.md create mode 100644 ts/web/nextjs/polaris/eslint.config.mjs create mode 100644 ts/web/nextjs/polaris/next-env.d.ts create mode 100644 ts/web/nextjs/polaris/next.config.ts create mode 100644 ts/web/nextjs/polaris/package.json create mode 100644 ts/web/nextjs/polaris/pnpm-lock.yaml create mode 100644 ts/web/nextjs/polaris/pnpm-workspace.yaml create mode 100644 ts/web/nextjs/polaris/postcss.config.mjs create mode 100644 ts/web/nextjs/polaris/prettier.config.mjs rename ts/web/nextjs/polaris/{ => public/brand}/logo.svg (100%) create mode 100644 ts/web/nextjs/polaris/src/app/globals.css create mode 100644 ts/web/nextjs/polaris/src/app/layout.tsx create mode 100644 ts/web/nextjs/polaris/src/app/page.tsx create mode 100644 ts/web/nextjs/polaris/src/components/shared/glass-panel.tsx create mode 100644 ts/web/nextjs/polaris/src/components/shared/icon-candidates.test.ts create mode 100644 ts/web/nextjs/polaris/src/components/shared/icon-candidates.ts create mode 100644 ts/web/nextjs/polaris/src/components/shared/simple-icon-map.ts create mode 100644 ts/web/nextjs/polaris/src/components/shared/site-icon.test.tsx create mode 100644 ts/web/nextjs/polaris/src/components/shared/site-icon.tsx create mode 100644 ts/web/nextjs/polaris/src/components/ui/button.tsx create mode 100644 ts/web/nextjs/polaris/src/components/ui/dropdown-menu.tsx create mode 100644 ts/web/nextjs/polaris/src/components/ui/tooltip.tsx create mode 100644 ts/web/nextjs/polaris/src/data/categories.json create mode 100644 ts/web/nextjs/polaris/src/data/data.test.ts create mode 100644 ts/web/nextjs/polaris/src/data/links.json create mode 100644 ts/web/nextjs/polaris/src/data/settings.json create mode 100644 ts/web/nextjs/polaris/src/features/home/components/clock-greeting.test.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/components/clock-greeting.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/components/home-content.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/components/local-search-results.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/components/quick-links.test.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/components/quick-links.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/components/search-box.test.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/components/search-box.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/components/search-provider-menu.tsx create mode 100644 ts/web/nextjs/polaris/src/features/home/hooks/use-clock.ts create mode 100644 ts/web/nextjs/polaris/src/features/home/lib/date-time.test.ts create mode 100644 ts/web/nextjs/polaris/src/features/home/lib/date-time.ts create mode 100644 ts/web/nextjs/polaris/src/features/home/lib/local-search.test.ts create mode 100644 ts/web/nextjs/polaris/src/features/home/lib/local-search.ts create mode 100644 ts/web/nextjs/polaris/src/features/home/lib/provider-preference.test.ts create mode 100644 ts/web/nextjs/polaris/src/features/home/lib/provider-preference.ts create mode 100644 ts/web/nextjs/polaris/src/features/home/lib/search-providers.test.ts create mode 100644 ts/web/nextjs/polaris/src/features/home/lib/search-providers.ts create mode 100644 ts/web/nextjs/polaris/src/features/navigation/components/app-shell.test.tsx create mode 100644 ts/web/nextjs/polaris/src/features/navigation/components/app-shell.tsx create mode 100644 ts/web/nextjs/polaris/src/features/navigation/components/category-icon.tsx create mode 100644 ts/web/nextjs/polaris/src/features/navigation/components/sidebar.tsx create mode 100644 ts/web/nextjs/polaris/src/features/navigation/lib/sidebar-preference.test.ts create mode 100644 ts/web/nextjs/polaris/src/features/navigation/lib/sidebar-preference.ts create mode 100644 ts/web/nextjs/polaris/src/lib/utils.ts create mode 100644 ts/web/nextjs/polaris/src/services/categories.ts create mode 100644 ts/web/nextjs/polaris/src/services/links.ts create mode 100644 ts/web/nextjs/polaris/src/services/services.test.ts create mode 100644 ts/web/nextjs/polaris/src/services/settings.ts create mode 100644 ts/web/nextjs/polaris/src/test/setup.ts create mode 100644 ts/web/nextjs/polaris/src/types/navigation.ts create mode 100644 ts/web/nextjs/polaris/src/types/search.ts create mode 100644 ts/web/nextjs/polaris/src/types/settings.ts create mode 100644 ts/web/nextjs/polaris/tsconfig.json create mode 100644 ts/web/nextjs/polaris/vitest.config.ts diff --git a/.gitignore b/.gitignore index 52abedb..73ea2d0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,14 @@ node_modules/ dist/ .astro/ *.tsbuildinfo +.next/ +out/ +coverage/ +.env* +!.env.example + +# ---- Claude Code ---- +.claude/worktrees/ # ---- IDE / 编辑器 ---- .idea/ diff --git a/ts/web/nextjs/polaris/.prettierignore b/ts/web/nextjs/polaris/.prettierignore new file mode 100644 index 0000000..f56cafb --- /dev/null +++ b/ts/web/nextjs/polaris/.prettierignore @@ -0,0 +1,5 @@ +.next/ +out/ +node_modules/ +coverage/ +next-env.d.ts diff --git a/ts/web/nextjs/polaris/AGENTS.md b/ts/web/nextjs/polaris/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/ts/web/nextjs/polaris/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/ts/web/nextjs/polaris/CLAUDE.md b/ts/web/nextjs/polaris/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/ts/web/nextjs/polaris/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/ts/web/nextjs/polaris/components.json b/ts/web/nextjs/polaris/components.json new file mode 100644 index 0000000..30fbc61 --- /dev/null +++ b/ts/web/nextjs/polaris/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/ts/web/nextjs/polaris/Polaris页面设计图.png b/ts/web/nextjs/polaris/docs/assets/Polaris页面设计图.png similarity index 100% rename from ts/web/nextjs/polaris/Polaris页面设计图.png rename to ts/web/nextjs/polaris/docs/assets/Polaris页面设计图.png diff --git a/ts/web/nextjs/polaris/docs/design/2026-08-26-Polaris-首页实现规格.md b/ts/web/nextjs/polaris/docs/design/2026-08-26-Polaris-首页实现规格.md new file mode 100644 index 0000000..b6a605d --- /dev/null +++ b/ts/web/nextjs/polaris/docs/design/2026-08-26-Polaris-首页实现规格.md @@ -0,0 +1,453 @@ +# Polaris 首页实现规格 + +> 日期:2026-08-26 +> +> 状态:用户已批准,可进入实施 +> +> 实现分支:`feat/polaris-homepage` + +## 1. 背景与目标 + +Polaris 是一个轻量、安静、高效的个人浏览器起始页,不是后台管理系统或信息密集型 Dashboard。 + +本次工作只建立推荐的项目结构并实现完整可交互首页。用户验收首页视觉和交互后,才会明确决定是否继续实现分类页及第一阶段其余目标。 + +## 2. 范围 + +### 2.1 本次实现 + +- 初始化 Next.js App Router 项目; +- 建立 Feature-first 混合目录; +- 整理现有设计文档和资产位置; +- 实现低饱和青蓝 CSS 渐变背景与浅色玻璃视觉; +- 实现可折叠且持久化的桌面侧栏; +- 实现实时时钟、日期和分时问候语; +- 实现 Google、Bing、GitHub、Local 四种严格分离的搜索模式; +- 实现 Local 搜索结果、键盘导航和空状态; +- 实现 8 个常用链接; +- 实现统一图标 fallback; +- 建立本地 JSON、service、类型与数据校验边界; +- 完成单元测试、组件交互测试、静态构建和真实页面视觉检查。 + +### 2.2 本次不实现 + +- 正式分类页; +- Go API 与 PostgreSQL; +- 管理页或隐藏管理入口; +- 深色模式; +- 移动端与平板布局; +- 自定义背景上传; +- 自定义搜索引擎、URL 直达、AI 搜索; +- 加载骨架和后端错误重试。 + +## 3. 已确认决策 + +| 决策项 | 结论 | +| ---------- | ----------------------------------------------------------------- | +| 首页范围 | 完整可交互首页 | +| 目录方案 | Feature-first + 共享基础层 | +| 分支粒度 | 项目骨架与首页位于同一个 `feat/polaris-homepage` 分支 | +| 文档整理 | 三份文档迁入 `docs/design/`,设计图迁入 `docs/assets/` | +| Logo | 现有占位 Logo 迁入 `public/brand/`,不追加品牌设计 | +| 运行时背景 | CSS 低饱和青蓝渐变;设计图只作构图参考 | +| 外部搜索 | Google、Bing、GitHub 在当前标签页跳转 | +| 本地链接 | Local 结果与常用链接在新标签页打开 | +| 排序模型 | `sort` 管理分类内顺序;可选 `pinnedSort` 独立管理首页常用链接顺序 | +| 分类导航 | 首页验收阶段展示但禁用,避免进入未实现路由;分类页完成后启用 | +| 注释 | 关键规则、非显然决策、边界条件和阶段性限制使用简洁中文注释 | +| 提交时机 | 分支全部目标完成并验证后再提交 | + +## 4. 技术基线 + +- Next.js App Router; +- React; +- TypeScript strict; +- Tailwind CSS v4,使用 CSS-first 配置; +- shadcn/ui,只引入实际使用的基础原语; +- lucide-react; +- simple-icons; +- pnpm; +- Vitest; +- Testing Library; +- ESLint; +- Prettier。 + +`next.config.ts` 使用 `output: "export"`,构建产物必须能作为纯静态文件部署。 + +不引入 Redux、Zustand、React Query、Zod、Storybook、Repository interface 或依赖注入容器。当前规模下这些抽象没有必要。 + +## 5. 推荐目录 + +```text +polaris/ +├─ docs/ +│ ├─ design/ +│ │ ├─ Polaris 项目说明文档.md +│ │ ├─ Polaris 首页设计文档.md +│ │ ├─ Polaris 分类页设计文档.md +│ │ └─ 2026-08-26-Polaris-首页实现规格.md +│ └─ assets/ +│ └─ Polaris页面设计图.png +├─ public/ +│ ├─ brand/ +│ │ └─ logo.svg +│ └─ icons/ +├─ src/ +│ ├─ app/ +│ │ ├─ globals.css +│ │ ├─ layout.tsx +│ │ └─ page.tsx +│ ├─ components/ +│ │ ├─ ui/ +│ │ └─ shared/ +│ ├─ features/ +│ │ ├─ home/ +│ │ │ ├─ components/ +│ │ │ ├─ hooks/ +│ │ │ └─ lib/ +│ │ └─ navigation/ +│ │ └─ components/ +│ ├─ data/ +│ │ ├─ categories.json +│ │ ├─ links.json +│ │ └─ settings.json +│ ├─ services/ +│ │ ├─ categories.ts +│ │ ├─ links.ts +│ │ └─ settings.ts +│ ├─ types/ +│ │ ├─ navigation.ts +│ │ ├─ search.ts +│ │ └─ settings.ts +│ ├─ lib/ +│ │ └─ utils.ts +│ └─ test/ +│ └─ setup.ts +├─ components.json +├─ eslint.config.mjs +├─ next.config.ts +├─ package.json +├─ pnpm-lock.yaml +├─ postcss.config.mjs +├─ prettier.config.mjs +├─ tsconfig.json +└─ vitest.config.ts +``` + +### 5.1 目录职责 + +- `app/`:路由、metadata、根布局、全局样式和页面组合; +- `features/`:按功能聚合组件、hooks 和专属纯函数; +- `components/ui/`:shadcn/ui 基础原语; +- `components/shared/`:跨功能复用组件,例如 `GlassPanel`、`SiteIcon`; +- `data/`:第一版本地 JSON; +- `services/`:页面与数据源之间的唯一访问边界; +- `types/`:跨功能共享的数据契约; +- `lib/`:真正通用且不依赖业务功能的工具; +- `test/`:全局测试环境与共享测试辅助代码。 + +测试文件与被测试实现就近放置,`src/test/` 不作为所有测试的集中目录。 + +## 6. 渲染与组件边界 + +`src/app/page.tsx` 保持为 Server Component,在构建时调用 service 层读取数据,再将可序列化数据传给页面组件。 + +```text +app/page.tsx +└─ AppShell + ├─ Sidebar + │ ├─ SidebarHeader + │ └─ SidebarNav + └─ HomeContent + ├─ ClockGreeting + ├─ SearchBox + │ ├─ SearchProviderMenu + │ └─ LocalSearchResults + └─ QuickLinks + └─ QuickLinkCard + └─ SiteIcon +``` + +Client Component 边界仅用于浏览器状态: + +- `AppShell`:侧栏折叠与持久化; +- `ClockGreeting`:当前时间; +- `SearchBox`:Provider、query、结果选中项和快捷键; +- `SiteIcon`:图标加载失败后的 fallback。 + +把 Server Component 作为 `AppShell` 的 `children` 传入,不因为外层存在 Client Component 就将全部展示代码改成客户端代码。 + +不为每个小标签或图标创建组件。只有存在独立状态、复用价值或清晰职责时才拆分。 + +## 7. 数据模型与数据流 + +### 7.1 数据规模 + +- 7 个分类; +- 开发工具 15 条; +- ChatGPT、Gmail、Notion、YouTube、Bilibili 5 条跨分类链接; +- 共 20 条链接; +- 8 条 `pinned` 常用链接; +- `sort` 只表示分类内顺序,可选 `pinnedSort` 只表示首页常用链接顺序。 + +首页常用链接固定按以下 `pinnedSort` 顺序展示:GitHub、ChatGPT、Gmail、Notion、Figma、YouTube、Vercel、Bilibili。该字段解决开发工具分类中 Vercel 位于 Figma 之前、但首页中 Figma 位于 Vercel 之前的双重排序需求。 + +设计资源、学习资料、生活服务保持无链接;AI 工具、效率工具和娱乐媒体因常用链接而有少量数据。 + +### 7.2 访问路径 + +1. `page.tsx` 调用 `getCategories()`、`getLinks()` 和 `getSettings()`; +2. service 层复制、排序和筛选数据; +3. 页面不直接 import JSON; +4. 首页只把所需的分类、全部搜索候选、8 条常用链接和设置传给客户端; +5. 未来接 Go API 时保持 service 的对外职责不变,只替换其内部数据来源。 + +### 7.3 数据校验 + +Vitest 必须校验: + +- 必填字段类型; +- 分类和链接 ID 唯一; +- URL 合法; +- `categoryId` 必须指向现有分类; +- `sort` 与可选 `pinnedSort` 合法; +- Provider 配置有效; +- `pinned` 数量为 8,且每条 pinned 链接都有唯一 `pinnedSort`。 + +## 8. 视觉系统 + +### 8.1 背景 + +使用多层 CSS 渐变和柔和光斑模拟晨雾、天空和水面氛围,不把 `Polaris页面设计图.png` 作为运行时背景。 + +增加浅色遮罩和适度模糊,保证未来替换背景时仍具有可读性。 + +### 8.2 布局 + +- 侧栏外边距:`20px`; +- 展开宽度:`240px`; +- 折叠宽度:`72px`; +- 主内容视觉重心位于中上方; +- 使用 `minmax()` 与 `clamp()` 适配 1024px 以上桌面窗口; +- 搜索框最大宽度约 `760px`、高度约 `68px`; +- 常用链接最大四列、共两行,缩窄窗口时允许减少列数; +- 第一版不实现移动端抽屉侧栏。 + +### 8.3 Design tokens + +在 `globals.css` 中定义 Polaris 语义变量,并通过 Tailwind v4 `@theme inline` 映射: + +- 主文本、次级文本、弱文本; +- 主色、hover 色、柔和主色; +- 侧栏、搜索框、卡片表面; +- 玻璃边框; +- 软阴影、中等阴影; +- 圆角与模糊强度。 + +第一版只实现浅色值,但变量命名允许以后通过主题选择器覆盖。 + +### 8.4 动效与无障碍 + +- hover、focus 和折叠过渡约为 180ms; +- 不使用夸张缩放或弹跳; +- 尊重 `prefers-reduced-motion`; +- 所有图标按钮有可访问名称; +- 键盘焦点清晰; +- 搜索输入与结果使用合理的 combobox/listbox 语义; +- 禁用分类项使用 `aria-disabled` 并提供“分类页将在下一阶段提供”的轻量提示。 + +## 9. 首页交互 + +### 9.1 时钟与问候 + +问候规则: + +```text +05:00 - 11:59 早上好 +12:00 - 13:59 中午好 +14:00 - 17:59 下午好 +18:00 - 23:59 晚上好 +00:00 - 04:59 夜深了 +``` + +客户端按分钟更新;跨午夜后日期、星期和问候语同步变化。 + +### 9.2 Provider 状态 + +- 初始值来自 `settings.search.defaultProvider`; +- 用户切换后保存到 `localStorage`; +- 无效值或存储不可用时回退到设置默认值; +- 外部 Provider 与 Local 模式严格分离。 + +### 9.3 外部搜索 + +Google、Bing、GitHub: + +- 输入先 `trim()`; +- 空内容不提交; +- 使用 `URLSearchParams` 编码; +- 在当前标签页跳转; +- 不显示 Local 结果下拉。 + +### 9.4 Local 搜索 + +匹配字段: + +- 链接标题; +- 关键词; +- 描述; +- 分类名称; +- 分类 ID。 + +优先级: + +```text +标题完全匹配 +标题前缀或包含 +关键词匹配 +描述匹配 +分类匹配 +``` + +同分时按链接 `sort` 和标题稳定排序,最多返回 8 条。 + +交互: + +- 默认选中第一项; +- `↑` / `↓` 移动; +- `Enter` 打开选中项; +- 鼠标点击直接打开; +- 结果在新标签页打开; +- `Esc` 清空输入并关闭面板; +- 无结果显示“没有匹配的网站”。 + +### 9.5 快捷键 + +- 首页加载后自动聚焦搜索框; +- Windows/Linux:`Ctrl+K`; +- macOS:`⌘K`; +- 快捷键阻止浏览器默认行为后聚焦; +- 监听器只存在于首页生命周期内。 + +### 9.6 侧栏 + +- 折叠状态保存到 `localStorage`; +- 存储不可用或值损坏时回退到设置默认值; +- hydration 后同步持久化值; +- 同步完成前禁用布局过渡,减少闪动; +- 首页项为当前选中态; +- 分类项本次显示但禁用,不进入 404。 + +### 9.7 常用链接 + +- 对 `pinned` 链接按 `pinnedSort` 排序取前 8 条; +- `pinned: true` 时必须提供唯一的 `pinnedSort`; +- 使用真实 ``; +- `target="_blank"`; +- `rel="noopener noreferrer"`; +- 卡片仅显示图标与名称。 + +## 10. 图标策略 + +运行时优先级: + +```text +自定义本地图标 +明确映射的 Simple Icons +favicon +标题首字符文字占位 +``` + +当前 20 条数据尽量命中显式 Simple Icons 映射,避免正常页面依赖远程 favicon。远程图标失败时不得显示破图。 + +侧栏分类图标使用 lucide-react,不参与站点图标 fallback。 + +## 11. 状态与故障处理 + +- 静态 JSON 无 Loading UI; +- 数据结构错误由测试和构建阶段阻止; +- `localStorage` 读写使用容错封装; +- favicon 失败显示文字占位; +- 空搜索不跳转; +- Local 无结果显示轻量空状态; +- 非法 Provider 回退到配置默认值; +- Server 初始值与 Client 首次渲染保持一致,避免 hydration error。 + +## 12. 注释规范 + +必须为以下非显然逻辑添加简洁中文注释,解释“为什么”: + +- Local 搜索权重及稳定排序; +- Server/Client 边界选择; +- 图标 fallback 顺序; +- hydration 后同步 `localStorage`; +- 分类导航临时禁用; +- 数据校验中的跨分类引用规则。 + +普通 JSX、显然的变量赋值和框架惯用代码不添加复述式注释。 + +## 13. 测试设计 + +### 13.1 纯函数 + +- 问候语全部时间边界; +- 日期格式; +- 搜索 URL 与特殊字符编码; +- Local 搜索字段、权重、排序和数量上限; +- 图标引用解析; +- 数据结构及交叉引用。 + +### 13.2 组件交互 + +- 自动聚焦; +- Provider 严格分离; +- Provider 持久化与非法值回退; +- Local 键盘导航、Enter、Esc、点击和无结果; +- `Ctrl+K` / `⌘K`; +- 侧栏折叠、恢复和异常存储回退; +- 新标签页链接安全属性; +- 分类项禁用状态。 + +## 14. 验证与视觉验收 + +完成后必须运行: + +```text +pnpm lint +pnpm typecheck +pnpm test +pnpm build +``` + +随后实际启动页面并检查: + +- `1440 × 900`; +- `1920 × 1080`; +- `1024 × 768`。 + +检查项: + +- 无横向溢出; +- 背景渐变层次和文本对比度; +- 玻璃透明度、边框与阴影一致; +- 侧栏展开/折叠比例; +- 搜索聚焦与 Provider 交互; +- Local 结果定位和键盘交互; +- 卡片排列、hover 和 focus; +- `prefers-reduced-motion`; +- 控制台无 hydration error 或运行时错误。 + +设计图用于构图和氛围参照,不要求像素级复制图片背景。 + +## 15. 完成定义 + +只有以下条件全部满足,`feat/polaris-homepage` 才完成: + +1. 推荐目录和工程配置建立; +2. 文档及资产迁移完成,内部链接有效; +3. 完整可交互首页实现; +4. 自动化检查全部通过; +5. 真实页面完成三个尺寸的视觉检查; +6. 实际结果、视觉观察和已知偏差如实报告; +7. 未提前实现正式分类页; +8. 完成后再执行提交操作。 diff --git a/ts/web/nextjs/polaris/Polaris 分类页设计文档.md b/ts/web/nextjs/polaris/docs/design/Polaris 分类页设计文档.md similarity index 98% rename from ts/web/nextjs/polaris/Polaris 分类页设计文档.md rename to ts/web/nextjs/polaris/docs/design/Polaris 分类页设计文档.md index 16f1118..eac6e87 100644 --- a/ts/web/nextjs/polaris/Polaris 分类页设计文档.md +++ b/ts/web/nextjs/polaris/docs/design/Polaris 分类页设计文档.md @@ -194,8 +194,8 @@ AI 工具 推荐样式: ```css -background: rgba(99, 102, 241, 0.10); -color: #2563EB; +background: rgba(99, 102, 241, 0.1); +color: #2563eb; ``` 图标和文字同时高亮。 @@ -276,7 +276,7 @@ background: rgba(255, 255, 255, 0.56); backdrop-filter: blur(24px); border: 1px solid rgba(255, 255, 255, 0.48); border-radius: 24px; -box-shadow: 0 24px 80px rgba(15, 23, 42, 0.10); +box-shadow: 0 24px 80px rgba(15, 23, 42, 0.1); ``` 容器与页面边距: @@ -328,7 +328,7 @@ padding: 32px 24px; ```css font-size: 40px; font-weight: 700; -color: #0F172A; +color: #0f172a; letter-spacing: -0.02em; ``` @@ -355,7 +355,7 @@ font-size: 44px; ```css font-size: 16px; line-height: 1.7; -color: #64748B; +color: #64748b; margin-top: 12px; ``` @@ -402,12 +402,12 @@ AI 工具:精选常用 AI 工具与服务,辅助创作、开发和效率提 .section-title { font-size: 18px; font-weight: 700; - color: #0F172A; + color: #0f172a; } .section-count { font-size: 14px; - color: #94A3B8; + color: #94a3b8; background: rgba(255, 255, 255, 0.42); border-radius: 999px; padding: 4px 10px; @@ -640,7 +640,7 @@ height: 44px; ```css font-size: 17px; font-weight: 700; -color: #0F172A; +color: #0f172a; ``` 工具描述: @@ -648,7 +648,7 @@ color: #0F172A; ```css font-size: 14px; line-height: 1.5; -color: #64748B; +color: #64748b; margin-top: 4px; ``` @@ -704,6 +704,7 @@ JavaScript 包管理器 "icon": "si:github", "categoryId": "dev", "pinned": true, + "pinnedSort": 10, "keywords": ["代码", "仓库", "开源", "git"], "sort": 10 }, @@ -803,6 +804,7 @@ JavaScript 包管理器 "icon": "si:vercel", "categoryId": "dev", "pinned": true, + "pinnedSort": 70, "keywords": ["部署", "前端", "托管"], "sort": 100 }, @@ -858,6 +860,7 @@ JavaScript 包管理器 "icon": "si:figma", "categoryId": "dev", "pinned": true, + "pinnedSort": 50, "keywords": ["设计", "ui", "协作"], "sort": 150 } @@ -897,14 +900,14 @@ window.open(url, "_blank", "noopener,noreferrer") ```css width: 16px; height: 16px; -color: #64748B; +color: #64748b; opacity: 0.75; ``` Hover 时: ```css -color: #2563EB; +color: #2563eb; opacity: 1; ``` @@ -992,7 +995,7 @@ height: 280px; display: flex; align-items: center; justify-content: center; -color: #64748B; +color: #64748b; ``` --- @@ -1147,11 +1150,12 @@ export type Category = { export type NavLink = { id: string title: string - description?: string + description: string url: string - icon: string + icon?: string categoryId: string pinned: boolean + pinnedSort?: number keywords: string[] sort: number } @@ -1240,4 +1244,4 @@ type CategoryPageData = { 侧边栏负责导航 管理功能不进入主视觉 视觉风格保持轻玻璃和极简 -``` \ No newline at end of file +``` diff --git a/ts/web/nextjs/polaris/Polaris 项目说明文档.md b/ts/web/nextjs/polaris/docs/design/Polaris 项目说明文档.md similarity index 77% rename from ts/web/nextjs/polaris/Polaris 项目说明文档.md rename to ts/web/nextjs/polaris/docs/design/Polaris 项目说明文档.md index 2301e7f..7231ad3 100644 --- a/ts/web/nextjs/polaris/Polaris 项目说明文档.md +++ b/ts/web/nextjs/polaris/docs/design/Polaris 项目说明文档.md @@ -4,7 +4,7 @@ > [Polaris 首页设计文档](./Polaris%20首页设计文档.md) —— 首页布局、搜索交互、视觉规范 > [Polaris 分类页设计文档](./Polaris%20分类页设计文档.md) —— 分类页布局、卡片、状态设计 > -> 配套资产:`logo.svg`(占位 Logo,后续替换)、`Polaris页面设计图.png` +> 配套资产:[占位 Logo](../../public/brand/logo.svg)(后续替换)、[Polaris 页面设计图](../assets/Polaris页面设计图.png) ## 1. 项目名称 @@ -54,22 +54,22 @@ Polaris 不是后台管理系统,也不是信息密集型 Dashboard。 以下决策已于 2026-08-21 确认,作为实施基线: -| # | 决策项 | 结论 | -|---|--------|------| -| 1 | 技术栈 | 以本文档为准:Next.js + TypeScript + Tailwind CSS + shadcn/ui + lucide-react(早期 README 中 Astro 设想作废) | -| 2 | 渲染策略 | 静态导出(`output: 'export'`),时钟/搜索/侧边栏折叠为 client 岛屿 | -| 3 | 包管理器 | pnpm | -| 4 | 项目位置 | `ts/web/nextjs/polaris/` | -| 5 | Logo | 现有 `logo.svg` 仅作占位,后续替换,不投入设计精力 | -| 6 | 数据载体 | 本地 JSON 数据文件(`links.json` / `categories.json` / `settings.json`),结构以 Vitest 校验测试兜底 | -| 7 | 第一版数据范围 | 「开发工具」15 条 + 首页常用链接 8 条(跨分类条目归入真实分类,见首页设计文档 8.1);设计资源、学习资料、生活服务三个分类展示空状态 | -| 8 | 默认背景 | CSS 低饱和青蓝渐变,不依赖图片资产;`settings.background.type` 预留 `image` | -| 9 | 深色模式 | 第一版不做;CSS 变量按双主题友好组织,后续以 `[data-theme="dark"]` 覆盖 | -| 10 | 移动端 | 第一版不适配,仅桌面端 | -| 11 | 快捷键 | Ctrl+K(macOS ⌘K)聚焦搜索框,仅首页生效;提示按平台检测显示 | -| 12 | 图标方案 | 四级 fallback:自定义图标 → Simple Icons / 本地图标库 → favicon 自动抓取 → 文字占位(见 6.4) | -| 13 | 后端数据库 | PostgreSQL(后续阶段);表结构不锁死 PG 特有语法(JSON 存通用 TEXT) | -| 14 | 搜索行为 | 按模式严格分离:选中哪个引擎就只用哪个引擎搜索(回车直跳);仅 Local 模式显示本地结果下拉 | +| # | 决策项 | 结论 | +| --- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| 1 | 技术栈 | 以本文档为准:Next.js + TypeScript + Tailwind CSS + shadcn/ui + lucide-react(早期 README 中 Astro 设想作废) | +| 2 | 渲染策略 | 静态导出(`output: 'export'`),时钟/搜索/侧边栏折叠为 client 岛屿 | +| 3 | 包管理器 | pnpm | +| 4 | 项目位置 | `ts/web/nextjs/polaris/` | +| 5 | Logo | 现有 `logo.svg` 仅作占位,后续替换,不投入设计精力 | +| 6 | 数据载体 | 本地 JSON 数据文件(`links.json` / `categories.json` / `settings.json`),结构以 Vitest 校验测试兜底 | +| 7 | 第一版数据范围 | 「开发工具」15 条 + 首页常用链接 8 条(跨分类条目归入真实分类,见首页设计文档 8.1);设计资源、学习资料、生活服务三个分类展示空状态 | +| 8 | 默认背景 | CSS 低饱和青蓝渐变,不依赖图片资产;`settings.background.type` 预留 `image` | +| 9 | 深色模式 | 第一版不做;CSS 变量按双主题友好组织,后续以 `[data-theme="dark"]` 覆盖 | +| 10 | 移动端 | 第一版不适配,仅桌面端 | +| 11 | 快捷键 | Ctrl+K(macOS ⌘K)聚焦搜索框,仅首页生效;提示按平台检测显示 | +| 12 | 图标方案 | 四级 fallback:自定义图标 → Simple Icons / 本地图标库 → favicon 自动抓取 → 文字占位(见 6.4) | +| 13 | 后端数据库 | PostgreSQL(后续阶段);表结构不锁死 PG 特有语法(JSON 存通用 TEXT) | +| 14 | 搜索行为 | 按模式严格分离:选中哪个引擎就只用哪个引擎搜索(回车直跳);仅 Local 模式显示本地结果下拉 | --- @@ -189,6 +189,7 @@ sort: 排序值 icon: "si:github", categoryId: "dev", pinned: true, + pinnedSort: 10, keywords: ["代码", "仓库", "开源", "git"], sort: 10 } @@ -204,8 +205,9 @@ url: 链接地址 icon: 图标引用,格式见 6.4 categoryId: 所属分类 pinned: 是否展示在首页常用链接区域 +pinnedSort: 首页常用链接顺序(仅 pinned 为 true 时必填) keywords: 本地搜索关键词 -sort: 排序值 +sort: 分类内排序值 ``` ### 6.3 设置数据 @@ -309,6 +311,7 @@ CREATE TABLE links ( icon TEXT, category_id TEXT NOT NULL, pinned BOOLEAN NOT NULL DEFAULT FALSE, + pinned_sort INTEGER, keywords TEXT NOT NULL DEFAULT '[]', -- JSON 数组字符串 sort INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/ts/web/nextjs/polaris/Polaris 首页设计文档.md b/ts/web/nextjs/polaris/docs/design/Polaris 首页设计文档.md similarity index 95% rename from ts/web/nextjs/polaris/Polaris 首页设计文档.md rename to ts/web/nextjs/polaris/docs/design/Polaris 首页设计文档.md index b6a8bc6..beb04d4 100644 --- a/ts/web/nextjs/polaris/Polaris 首页设计文档.md +++ b/ts/web/nextjs/polaris/docs/design/Polaris 首页设计文档.md @@ -4,7 +4,7 @@ > [Polaris 项目说明文档](./Polaris%20项目说明文档.md) —— 项目定位、技术选型、数据结构、开发阶段 > [Polaris 分类页设计文档](./Polaris%20分类页设计文档.md) —— 分类页布局与交互 > -> 配套设计图:`Polaris页面设计图.png`(示意图,视觉细节以本文档文字规范为准) +> 配套设计图:[Polaris 页面设计图](../assets/Polaris页面设计图.png)(示意图,视觉细节以本文档文字规范为准) ## 1. 页面定位 @@ -200,7 +200,7 @@ AI 工具 background: rgba(255, 255, 255, 0.68); backdrop-filter: blur(24px); border: 1px solid rgba(255, 255, 255, 0.45); -box-shadow: 0 24px 80px rgba(15, 23, 42, 0.10); +box-shadow: 0 24px 80px rgba(15, 23, 42, 0.1); border-radius: 24px; ``` @@ -242,14 +242,14 @@ bottom: 20px; ```css font-size: 56px; font-weight: 700; -color: #0F172A; +color: #0f172a; ``` 日期样式: ```css font-size: 16px; -color: #64748B; +color: #64748b; ``` 时间为客户端实时时钟,每分钟更新。 @@ -525,7 +525,7 @@ Bilibili 实际项目中根据个人习惯调整。 -数据来源:`pinned: true` 的链接,按 `sort` 排序取前 8 个;超出 8 个时按 4 列网格换行展示。 +数据来源:`pinned: true` 的链接,按 `pinnedSort` 排序取前 8 个;超出 8 个时按 4 列网格换行展示。`sort` 仅用于分类内排序,避免首页顺序与分类页顺序相互约束。 第一版数据构成(8 条 pinned 分布于各分类,归入真实分类): @@ -573,7 +573,7 @@ background: rgba(255, 255, 255, 0.62); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.56); border-radius: 20px; -box-shadow: 0 14px 40px rgba(15, 23, 42, 0.10); +box-shadow: 0 14px 40px rgba(15, 23, 42, 0.1); ``` 推荐尺寸: @@ -674,13 +674,13 @@ backdrop-filter: blur(8px); ```css :root { - --color-text-primary: #0F172A; - --color-text-secondary: #64748B; - --color-text-muted: #94A3B8; + --color-text-primary: #0f172a; + --color-text-secondary: #64748b; + --color-text-muted: #94a3b8; - --color-primary: #3B82F6; - --color-primary-hover: #2563EB; - --color-primary-soft: rgba(59, 130, 246, 0.10); + --color-primary: #3b82f6; + --color-primary-hover: #2563eb; + --color-primary-soft: rgba(59, 130, 246, 0.1); --color-surface-sidebar: rgba(255, 255, 255, 0.68); --color-surface-card: rgba(255, 255, 255, 0.62); @@ -689,7 +689,7 @@ backdrop-filter: blur(8px); --color-border-light: rgba(255, 255, 255, 0.56); --color-border-subtle: rgba(15, 23, 42, 0.08); - --shadow-soft: 0 14px 40px rgba(15, 23, 42, 0.10); + --shadow-soft: 0 14px 40px rgba(15, 23, 42, 0.1); --shadow-medium: 0 20px 60px rgba(15, 23, 42, 0.12); } ``` diff --git a/ts/web/nextjs/polaris/docs/superpowers/plans/2026-08-26-polaris-homepage.md b/ts/web/nextjs/polaris/docs/superpowers/plans/2026-08-26-polaris-homepage.md new file mode 100644 index 0000000..08470c1 --- /dev/null +++ b/ts/web/nextjs/polaris/docs/superpowers/plans/2026-08-26-polaris-homepage.md @@ -0,0 +1,3355 @@ +# Polaris Interactive Homepage Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 `feat/polaris-homepage` 分支中建立可维护的 Next.js 项目骨架,并交付符合已批准设计的完整可交互 Polaris 首页。 + +**Architecture:** `src/app` 只负责路由和组合,首页与导航逻辑分别聚合在 `src/features/home`、`src/features/navigation`,跨功能 UI 放在 `src/components/shared`,本地 JSON 只能通过 `src/services` 访问。页面保持 Server Component,只有侧栏、时钟、搜索和图标 fallback 使用最小 Client Component 边界。 + +**Tech Stack:** Next.js App Router、React、TypeScript strict、Tailwind CSS v4、shadcn/ui、lucide-react、simple-icons、Vitest、Testing Library、ESLint、Prettier、pnpm。 + +**Spec:** `ts/web/nextjs/polaris/docs/design/2026-08-26-Polaris-首页实现规格.md` + +## Global Constraints + +- 当前工作分支必须是 `feat/polaris-homepage`。 +- 采用 `output: "export"`,`pnpm build` 必须生成纯静态 `out/` 产物。 +- 第一版只实现浅色桌面首页;不得创建正式分类页、管理页、深色模式或 Go API。 +- 运行时背景必须是 CSS 低饱和青蓝渐变;设计图仅作构图参考。 +- Google、Bing、GitHub 搜索在当前标签页跳转;Local 结果和常用链接在新标签页打开。 +- 分类导航本阶段可见但禁用,不得进入 404。 +- `sort` 管分类内顺序;可选 `pinnedSort` 管首页常用链接顺序。 +- 关键业务规则、fallback、hydration 和阶段性限制使用简洁中文注释;不写复述代码的噪声注释。 +- 遵循 TDD:行为任务先写失败测试,确认失败,再写最小实现。 +- 用户要求整个分支目标完成后再提交,因此 Task 1–9 不创建中间 commit;仅 Task 10 在全部验证通过后提交一次。 +- 不引入 Redux、Zustand、React Query、Zod、Storybook、Repository interface 或依赖注入容器。 + +--- + +## File Map + +### Project and documentation + +- Modify: `.gitignore` — 增加 Next.js、测试覆盖率和环境文件忽略规则。 +- Create: `ts/web/nextjs/polaris/package.json` — 脚本、依赖和 Node 版本约束。 +- Create: `ts/web/nextjs/polaris/pnpm-lock.yaml` — pnpm 锁文件。 +- Create: `ts/web/nextjs/polaris/next.config.ts` — 静态导出配置。 +- Create: `ts/web/nextjs/polaris/tsconfig.json` — strict TypeScript 与 `@/*` alias。 +- Create: `ts/web/nextjs/polaris/next-env.d.ts` — Next.js 类型声明。 +- Create: `ts/web/nextjs/polaris/postcss.config.mjs` — Tailwind v4 PostCSS 插件。 +- Create: `ts/web/nextjs/polaris/eslint.config.mjs` — Next.js Core Web Vitals 与 TypeScript lint。 +- Create: `ts/web/nextjs/polaris/prettier.config.mjs` — Prettier 与 Tailwind class 排序。 +- Create: `ts/web/nextjs/polaris/vitest.config.ts` — jsdom、React 和 alias 测试配置。 +- Create: `ts/web/nextjs/polaris/components.json` — shadcn/ui 配置。 +- Move: 三份 Polaris 文档到 `ts/web/nextjs/polaris/docs/design/`。 +- Move: `Polaris页面设计图.png` 到 `ts/web/nextjs/polaris/docs/assets/`。 +- Move: `logo.svg` 到 `ts/web/nextjs/polaris/public/brand/logo.svg`。 + +### Application shell + +- Create: `src/app/layout.tsx` — 根 HTML、metadata 与全局样式。 +- Create: `src/app/page.tsx` — Server Component 首页组合入口。 +- Create: `src/app/globals.css` — Tailwind、design tokens、背景与全局动效。 +- Create: `src/lib/utils.ts` — shadcn `cn()` 工具。 +- Create: `src/test/setup.ts` — Testing Library 全局清理与 jest-dom matcher。 +- Create: `src/components/ui/button.tsx` — shadcn Button。 +- Create: `src/components/ui/dropdown-menu.tsx` — shadcn DropdownMenu。 +- Create: `src/components/ui/tooltip.tsx` — shadcn Tooltip。 + +### Data and contracts + +- Create: `src/types/navigation.ts` — `Category`、`NavLink`。 +- Create: `src/types/search.ts` — Provider 与 Local 结果类型。 +- Create: `src/types/settings.ts` — `AppSettings`。 +- Create: `src/data/categories.json` — 7 个分类。 +- Create: `src/data/links.json` — 20 条链接及 8 个 `pinnedSort`。 +- Create: `src/data/settings.json` — 搜索、背景和侧栏默认设置。 +- Create: `src/data/data.test.ts` — JSON 结构与交叉引用测试。 +- Create: `src/services/categories.ts` — 分类读取和排序。 +- Create: `src/services/links.ts` — 链接读取与常用链接排序。 +- Create: `src/services/settings.ts` — 设置读取。 +- Create: `src/services/services.test.ts` — service 排序和不可变性测试。 + +### Home domain + +- Create: `src/features/home/lib/date-time.ts` — 问候、时间和日期纯函数。 +- Create: `src/features/home/lib/date-time.test.ts` — 时间边界测试。 +- Create: `src/features/home/hooks/use-clock.ts` — 对齐分钟边界的客户端时钟。 +- Create: `src/features/home/components/clock-greeting.tsx` — 时钟展示。 +- Create: `src/features/home/components/clock-greeting.test.tsx` — 时钟交互测试。 +- Create: `src/features/home/lib/search-providers.ts` — 外部搜索 URL 与 Provider 校验。 +- Create: `src/features/home/lib/search-providers.test.ts` — URL 编码测试。 +- Create: `src/features/home/lib/local-search.ts` — 本地匹配、评分和排序。 +- Create: `src/features/home/lib/local-search.test.ts` — 本地搜索权重测试。 +- Create: `src/features/home/lib/provider-preference.ts` — Provider 存储解析与容错。 +- Create: `src/features/home/lib/provider-preference.test.ts` — 非法存储值测试。 +- Create: `src/features/home/components/search-provider-menu.tsx` — Provider 菜单。 +- Create: `src/features/home/components/local-search-results.tsx` — 结果 listbox。 +- Create: `src/features/home/components/search-box.tsx` — 搜索状态和键盘交互。 +- Create: `src/features/home/components/search-box.test.tsx` — 完整搜索交互测试。 +- Create: `src/features/home/components/quick-links.tsx` — 常用链接网格。 +- Create: `src/features/home/components/quick-links.test.tsx` — 顺序与安全属性测试。 +- Create: `src/features/home/components/home-content.tsx` — 首页主内容组合。 + +### Navigation and shared UI + +- Create: `src/features/navigation/lib/sidebar-preference.ts` — 折叠状态解析和存储。 +- Create: `src/features/navigation/lib/sidebar-preference.test.ts` — 存储容错测试。 +- Create: `src/features/navigation/components/category-icon.tsx` — 分类 icon 白名单映射。 +- Create: `src/features/navigation/components/sidebar.tsx` — Logo、首页选中态和禁用分类项。 +- Create: `src/features/navigation/components/app-shell.tsx` — 折叠状态与布局容器。 +- Create: `src/features/navigation/components/app-shell.test.tsx` — 折叠、持久化和禁用导航测试。 +- Create: `src/components/shared/glass-panel.tsx` — 共享玻璃容器。 +- Create: `src/components/shared/icon-candidates.ts` — 图标候选链纯函数。 +- Create: `src/components/shared/icon-candidates.test.ts` — fallback 顺序测试。 +- Create: `src/components/shared/simple-icon-map.ts` — 当前 20 个品牌 icon 的显式映射。 +- Create: `src/components/shared/site-icon.tsx` — SVG、favicon 和文字占位渲染。 +- Create: `src/components/shared/site-icon.test.tsx` — 图片失败 fallback 测试。 + +--- + +### Task 1: Establish the project foundation and move documentation + +**Files:** + +- Modify: `.gitignore` +- Create: all project configuration files listed under “Project and documentation” +- Create: `src/app/layout.tsx` +- Create: `src/app/page.tsx` +- Create: `src/app/globals.css` +- Create: `src/lib/utils.ts` +- Create: `src/test/setup.ts` +- Move: existing docs and assets to approved locations + +**Interfaces:** + +- Produces: a runnable static-export Next.js application with `@/*` resolving to `src/*`. +- Produces scripts: `dev`, `build`, `lint`, `typecheck`, `test`, `test:watch`, `format`, `format:check`. +- Consumes: no application code. + +- [ ] **Step 1: Verify branch and baseline** + +Run from repository root: + +```bash +git branch --show-current +git status --short +``` + +Expected: + +```text +feat/polaris-homepage +?? ts/web/nextjs/polaris/docs/ +``` + +The only pre-existing uncommitted file should be the approved implementation spec. + +- [ ] **Step 2: Move approved documentation and assets** + +Run: + +```bash +cd ts/web/nextjs/polaris +git mv 'Polaris 项目说明文档.md' docs/design/ +git mv 'Polaris 首页设计文档.md' docs/design/ +git mv 'Polaris 分类页设计文档.md' docs/design/ +mkdir -p docs/assets public/brand public/icons +git mv 'Polaris页面设计图.png' docs/assets/ +git mv logo.svg public/brand/logo.svg +``` + +Update links after the move: + +- In `docs/design/Polaris 项目说明文档.md`, replace the asset note with links to `../assets/Polaris页面设计图.png` and `../../public/brand/logo.svg`. +- In `docs/design/Polaris 首页设计文档.md`, point the design image link to `../assets/Polaris页面设计图.png`. +- Keep cross-document links as `./Polaris%20...md`, because all three documents remain siblings. + +- [ ] **Step 3: Create the initial package manifest** + +Create `package.json`: + +```json +{ + "name": "polaris", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "engines": { + "node": ">=20.9.0" + } +} +``` + +Install runtime dependencies: + +```bash +pnpm add next@latest react@latest react-dom@latest clsx tailwind-merge lucide-react simple-icons +``` + +Install development dependencies: + +```bash +pnpm add -D typescript @types/node @types/react @types/react-dom tailwindcss @tailwindcss/postcss postcss eslint eslint-config-next prettier prettier-plugin-tailwindcss vitest jsdom @vitejs/plugin-react vite-tsconfig-paths @testing-library/react @testing-library/user-event @testing-library/jest-dom +``` + +Expected: `package.json` contains pinned resolved ranges and `pnpm-lock.yaml` exists. + +- [ ] **Step 4: Create TypeScript and Next.js configuration** + +Create `next.config.ts`: + +```ts +import type { NextConfig } from "next" + +const nextConfig: NextConfig = { + output: "export", + trailingSlash: true, + images: { + unoptimized: true, + }, +} + +export default nextConfig +``` + +Create `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} +``` + +Create `next-env.d.ts`: + +```ts +/// +/// + +// 该文件由 Next.js 类型系统使用,不应手动加入业务声明。 +``` + +Create `postcss.config.mjs`: + +```js +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +} + +export default config +``` + +- [ ] **Step 5: Create lint, format and test configuration** + +Create `eslint.config.mjs`: + +```js +import { defineConfig, globalIgnores } from "eslint/config" +import nextVitals from "eslint-config-next/core-web-vitals" +import nextTypeScript from "eslint-config-next/typescript" + +export default defineConfig([ + ...nextVitals, + ...nextTypeScript, + globalIgnores([".next/**", "out/**", "coverage/**", "next-env.d.ts"]), +]) +``` + +Create `prettier.config.mjs`: + +```js +/** @type {import("prettier").Config} */ +const config = { + plugins: ["prettier-plugin-tailwindcss"], + semi: false, + singleQuote: false, + trailingComma: "all", +} + +export default config +``` + +Create `vitest.config.ts`: + +```ts +import react from "@vitejs/plugin-react" +import tsconfigPaths from "vite-tsconfig-paths" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + test: { + environment: "jsdom", + setupFiles: ["./src/test/setup.ts"], + clearMocks: true, + css: true, + }, +}) +``` + +Create `src/test/setup.ts`: + +```ts +import "@testing-library/jest-dom/vitest" +import { cleanup } from "@testing-library/react" +import { afterEach } from "vitest" + +afterEach(() => { + cleanup() +}) +``` + +- [ ] **Step 6: Configure shadcn/ui and generate only required primitives** + +Create `components.json`: + +```json +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} +``` + +Create `src/lib/utils.ts`: + +```ts +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} +``` + +Run: + +```bash +pnpm dlx shadcn@latest add button dropdown-menu tooltip --yes +``` + +Expected: only `button.tsx`, `dropdown-menu.tsx`, `tooltip.tsx` and their required packages are added. + +- [ ] **Step 7: Create the minimal App Router smoke page** + +Create `src/app/globals.css`: + +```css +@import "tailwindcss"; + +:root { + color-scheme: light; +} + +* { + box-sizing: border-box; +} + +html, +body { + min-height: 100%; +} + +body { + margin: 0; + color: #0f172a; + background: #e9f2f8; +} +``` + +Create `src/app/layout.tsx`: + +```tsx +import type { Metadata } from "next" + +import "./globals.css" + +export const metadata: Metadata = { + title: "Polaris", + description: "安静、高效的个人导航起始页", +} + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ) +} +``` + +Create `src/app/page.tsx`: + +```tsx +export default function HomePage() { + return
Polaris
+} +``` + +- [ ] **Step 8: Extend the repository ignore rules** + +Append under the Node/TypeScript section in repository-root `.gitignore`: + +```gitignore +.next/ +out/ +coverage/ +.env* +!.env.example +``` + +- [ ] **Step 9: Run the foundation smoke checks** + +Run from `ts/web/nextjs/polaris`: + +```bash +pnpm format +pnpm lint +pnpm typecheck +pnpm build +``` + +Expected: all commands exit `0`; `out/index.html` exists. + +- [ ] **Step 10: Record an uncommitted checkpoint** + +Run: + +```bash +git status --short +git diff --check +``` + +Expected: project foundation and moved docs are visible; no whitespace errors. Do not commit. + +--- + +### Task 2: Define typed data, independent pinned ordering, and service boundaries + +**Files:** + +- Create: `src/types/navigation.ts` +- Create: `src/types/search.ts` +- Create: `src/types/settings.ts` +- Create: `src/data/categories.json` +- Create: `src/data/links.json` +- Create: `src/data/settings.json` +- Create: `src/data/data.test.ts` +- Create: `src/services/categories.ts` +- Create: `src/services/links.ts` +- Create: `src/services/settings.ts` +- Create: `src/services/services.test.ts` +- Modify: moved project, home and category design documents to describe `pinnedSort` + +**Interfaces:** + +- Produces: `Category`, `NavLink`, `SearchProvider`, `AppSettings`. +- Produces: `getCategories(): Category[]`. +- Produces: `getLinks(): NavLink[]`. +- Produces: `getPinnedLinks(limit?: number): NavLink[]`. +- Produces: `getSettings(): AppSettings`. +- Consumes: JSON module support from Task 1. + +- [ ] **Step 1: Create the shared contracts** + +Create `src/types/navigation.ts`: + +```ts +export type Category = { + id: string + title: string + description: string + icon: string + sort: number +} + +export type NavLink = { + id: string + title: string + description: string + url: string + icon?: string + categoryId: string + pinned: boolean + /** 首页顺序与分类内顺序职责不同,避免复用 sort 造成顺序冲突。 */ + pinnedSort?: number + keywords: string[] + sort: number +} +``` + +Create `src/types/search.ts`: + +```ts +import type { NavLink } from "@/types/navigation" + +export const SEARCH_PROVIDERS = ["google", "bing", "github", "local"] as const + +export type SearchProvider = (typeof SEARCH_PROVIDERS)[number] +export type ExternalSearchProvider = Exclude + +export type LocalSearchResult = { + link: NavLink + categoryTitle: string + score: number +} +``` + +Create `src/types/settings.ts`: + +```ts +import type { SearchProvider } from "@/types/search" + +export type AppSettings = { + siteName: string + theme: "light-glass" + search: { + defaultProvider: SearchProvider + enabledProviders: SearchProvider[] + } + background: { + type: "gradient" | "image" + url: string | null + blur: number + overlay: number + } + sidebar: { + defaultCollapsed: boolean + } +} +``` + +- [ ] **Step 2: Write failing data validation tests before creating JSON** + +Create `src/data/data.test.ts` with tests that import all three missing JSON files and assert the contracts: + +```ts +import { describe, expect, it } from "vitest" + +import categories from "@/data/categories.json" +import links from "@/data/links.json" +import settings from "@/data/settings.json" +import { SEARCH_PROVIDERS } from "@/types/search" + +const hasUniqueValues = (values: string[]) => + new Set(values).size === values.length + +describe("Polaris 静态数据", () => { + it("包含 7 个唯一且结构完整的分类", () => { + expect(categories).toHaveLength(7) + expect(hasUniqueValues(categories.map(({ id }) => id))).toBe(true) + for (const category of categories) { + expect(category).toEqual({ + id: expect.any(String), + title: expect.any(String), + description: expect.any(String), + icon: expect.any(String), + sort: expect.any(Number), + }) + } + }) + + it("包含 20 条合法链接且分类引用均存在", () => { + const categoryIds = new Set(categories.map(({ id }) => id)) + expect(links).toHaveLength(20) + expect(hasUniqueValues(links.map(({ id }) => id))).toBe(true) + + for (const link of links) { + expect(() => new URL(link.url)).not.toThrow() + expect(categoryIds.has(link.categoryId)).toBe(true) + expect(Array.isArray(link.keywords)).toBe(true) + expect(Number.isFinite(link.sort)).toBe(true) + } + }) + + it("8 条常用链接拥有唯一 pinnedSort", () => { + const pinned = links.filter(({ pinned }) => pinned) + const pinnedSorts = pinned.map(({ pinnedSort }) => pinnedSort) + + expect(pinned).toHaveLength(8) + expect(pinnedSorts.every(Number.isFinite)).toBe(true) + expect(new Set(pinnedSorts).size).toBe(8) + }) + + it("搜索设置只使用受支持的 Provider", () => { + expect(SEARCH_PROVIDERS).toContain(settings.search.defaultProvider) + expect(settings.search.enabledProviders).toEqual([...SEARCH_PROVIDERS]) + }) +}) +``` + +- [ ] **Step 3: Run the data test and verify the expected failure** + +Run: + +```bash +pnpm test -- src/data/data.test.ts +``` + +Expected: FAIL because `categories.json`, `links.json`, and `settings.json` do not exist. + +- [ ] **Step 4: Create categories and settings data** + +Create `src/data/categories.json` with this exact order and content: + +```json +[ + { + "id": "ai", + "title": "AI 工具", + "description": "精选常用 AI 工具与服务,辅助创作、开发和效率提升。", + "icon": "sparkles", + "sort": 10 + }, + { + "id": "dev", + "title": "开发工具", + "description": "高效的开发工具和资源集合,提升你的开发体验和效率。", + "icon": "code", + "sort": 20 + }, + { + "id": "design", + "title": "设计资源", + "description": "常用设计工具、素材与灵感资源集合。", + "icon": "palette", + "sort": 30 + }, + { + "id": "learning", + "title": "学习资料", + "description": "整理常用学习平台、文档、课程和知识资源。", + "icon": "book-open", + "sort": 40 + }, + { + "id": "efficiency", + "title": "效率工具", + "description": "提升日常工作流效率的工具入口。", + "icon": "zap", + "sort": 50 + }, + { + "id": "media", + "title": "娱乐媒体", + "description": "常用影音、社区和内容平台。", + "icon": "circle-play", + "sort": 60 + }, + { + "id": "life", + "title": "生活服务", + "description": "日常生活、服务查询和实用网站入口。", + "icon": "coffee", + "sort": 70 + } +] +``` + +Create `src/data/settings.json`: + +```json +{ + "siteName": "Polaris", + "theme": "light-glass", + "search": { + "defaultProvider": "google", + "enabledProviders": ["google", "bing", "github", "local"] + }, + "background": { + "type": "gradient", + "url": null, + "blur": 8, + "overlay": 0.32 + }, + "sidebar": { + "defaultCollapsed": false + } +} +``` + +- [ ] **Step 5: Create all 20 links with separate `sort` and `pinnedSort`** + +Create `src/data/links.json`. Use the 15 development links exactly as specified in `docs/design/Polaris 分类页设计文档.md`; retain their category `sort` values `10` through `150`. + +Add these `pinnedSort` values to the three pinned development links: + +```text +GitHub -> sort 10, pinnedSort 10 +Vercel -> sort 100, pinnedSort 70 +Figma -> sort 150, pinnedSort 50 +``` + +Add the five cross-category records: + +```json +[ + { + "id": "chatgpt", + "title": "ChatGPT", + "description": "智能对话与创作助手", + "url": "https://chatgpt.com", + "icon": "si:openai", + "categoryId": "ai", + "pinned": true, + "pinnedSort": 20, + "keywords": ["ai", "人工智能", "对话", "写作"], + "sort": 10 + }, + { + "id": "gmail", + "title": "Gmail", + "description": "Google 电子邮件服务", + "url": "https://mail.google.com", + "icon": "si:gmail", + "categoryId": "efficiency", + "pinned": true, + "pinnedSort": 30, + "keywords": ["邮件", "邮箱", "google", "mail"], + "sort": 10 + }, + { + "id": "notion", + "title": "Notion", + "description": "一体化知识与协作空间", + "url": "https://www.notion.so", + "icon": "si:notion", + "categoryId": "efficiency", + "pinned": true, + "pinnedSort": 40, + "keywords": ["笔记", "知识库", "协作", "文档"], + "sort": 20 + }, + { + "id": "youtube", + "title": "YouTube", + "description": "全球视频内容平台", + "url": "https://www.youtube.com", + "icon": "si:youtube", + "categoryId": "media", + "pinned": true, + "pinnedSort": 60, + "keywords": ["视频", "影音", "直播", "google"], + "sort": 10 + }, + { + "id": "bilibili", + "title": "Bilibili", + "description": "年轻人的视频社区", + "url": "https://www.bilibili.com", + "icon": "si:bilibili", + "categoryId": "media", + "pinned": true, + "pinnedSort": 80, + "keywords": ["哔哩哔哩", "视频", "动漫", "社区"], + "sort": 20 + } +] +``` + +The final file must contain one JSON array of 20 objects, not two concatenated arrays. + +- [ ] **Step 6: Run the data tests and make them pass** + +Run: + +```bash +pnpm test -- src/data/data.test.ts +``` + +Expected: all 4 tests PASS. + +- [ ] **Step 7: Write failing service behavior tests** + +Create `src/services/services.test.ts`: + +```ts +import { describe, expect, it } from "vitest" + +import { getCategories } from "@/services/categories" +import { getLinks, getPinnedLinks } from "@/services/links" +import { getSettings } from "@/services/settings" + +describe("静态数据 service", () => { + it("按 sort 返回分类并提供新数组", () => { + const first = getCategories() + const second = getCategories() + + expect(first.map(({ id }) => id)).toEqual([ + "ai", + "dev", + "design", + "learning", + "efficiency", + "media", + "life", + ]) + expect(first).not.toBe(second) + }) + + it("按 pinnedSort 返回首页 8 个常用链接", () => { + expect(getPinnedLinks().map(({ id }) => id)).toEqual([ + "github", + "chatgpt", + "gmail", + "notion", + "figma", + "youtube", + "vercel", + "bilibili", + ]) + }) + + it("限制常用链接数量且不修改原数据", () => { + const allLinks = getLinks() + expect(getPinnedLinks(3)).toHaveLength(3) + expect(getLinks()).toEqual(allLinks) + }) + + it("返回设置对象的独立副本", () => { + expect(getSettings()).not.toBe(getSettings()) + }) +}) +``` + +- [ ] **Step 8: Run service tests and verify the expected failure** + +Run: + +```bash +pnpm test -- src/services/services.test.ts +``` + +Expected: FAIL because service modules do not exist. + +- [ ] **Step 9: Implement service functions** + +Create `src/services/categories.ts`: + +```ts +import categoriesData from "@/data/categories.json" +import type { Category } from "@/types/navigation" + +const categories = categoriesData satisfies Category[] + +export function getCategories(): Category[] { + return categories + .map((category) => ({ ...category })) + .sort((a, b) => a.sort - b.sort) +} +``` + +Create `src/services/links.ts`: + +```ts +import linksData from "@/data/links.json" +import type { NavLink } from "@/types/navigation" + +const links = linksData satisfies NavLink[] + +function cloneLink(link: NavLink): NavLink { + return { ...link, keywords: [...link.keywords] } +} + +export function getLinks(): NavLink[] { + return links.map(cloneLink) +} + +export function getPinnedLinks(limit = 8): NavLink[] { + // 首页顺序必须独立于分类内 sort,否则 Vercel 与 Figma 无法同时满足两处设计。 + return links + .filter((link) => link.pinned) + .map(cloneLink) + .sort( + (a, b) => + (a.pinnedSort ?? Number.MAX_SAFE_INTEGER) - + (b.pinnedSort ?? Number.MAX_SAFE_INTEGER), + ) + .slice(0, limit) +} +``` + +Create `src/services/settings.ts`: + +```ts +import settingsData from "@/data/settings.json" +import type { AppSettings } from "@/types/settings" + +// JSON 的联合字面量由数据校验测试兜底,此处收窄为应用设置契约。 +const settings = settingsData as AppSettings + +export function getSettings(): AppSettings { + return { + ...settings, + search: { + ...settings.search, + enabledProviders: [...settings.search.enabledProviders], + }, + background: { ...settings.background }, + sidebar: { ...settings.sidebar }, + } +} +``` + +- [ ] **Step 10: Run service and data tests** + +Run: + +```bash +pnpm test -- src/data/data.test.ts src/services/services.test.ts +pnpm typecheck +``` + +Expected: all tests PASS and TypeScript exits `0`. + +- [ ] **Step 11: Update design documents for `pinnedSort`** + +Update all moved documents consistently: + +- `Polaris 项目说明文档.md`: add optional `pinnedSort` to the `NavLink` example and field table; state `sort` is category order and `pinnedSort` is homepage order; add nullable `pinned_sort INTEGER` to the future `links` table. +- `Polaris 首页设计文档.md`: change “按 `sort` 排序取前 8 个” to “按 `pinnedSort` 排序取前 8 个”. +- `Polaris 分类页设计文档.md`: add `pinnedSort?: number` to the TypeScript type and add `pinnedSort` only to GitHub, Vercel and Figma sample records. + +Use this exact homepage order in prose: + +```text +GitHub → ChatGPT → Gmail → Notion → Figma → YouTube → Vercel → Bilibili +``` + +- [ ] **Step 12: Record an uncommitted checkpoint** + +Run: + +```bash +pnpm format +pnpm test -- src/data/data.test.ts src/services/services.test.ts +git diff --check +``` + +Expected: PASS; do not commit. + +--- + +### Task 3: Implement deterministic greeting and minute-aligned clock + +**Files:** + +- Create: `src/features/home/lib/date-time.ts` +- Create: `src/features/home/lib/date-time.test.ts` +- Create: `src/features/home/hooks/use-clock.ts` +- Create: `src/features/home/components/clock-greeting.tsx` +- Create: `src/features/home/components/clock-greeting.test.tsx` + +**Interfaces:** + +- Produces: `getGreeting(hour: number): string`. +- Produces: `formatTime(date: Date): string`. +- Produces: `formatDate(date: Date): string`. +- Produces: `getDelayUntilNextMinute(date: Date): number`. +- Produces: `` with no props. +- Consumes: React client effects. + +- [ ] **Step 1: Write failing pure-function tests** + +Create `src/features/home/lib/date-time.test.ts`: + +```ts +import { describe, expect, it } from "vitest" + +import { + formatDate, + formatTime, + getDelayUntilNextMinute, + getGreeting, +} from "./date-time" + +describe("首页日期与问候", () => { + it.each([ + [0, "夜深了"], + [4, "夜深了"], + [5, "早上好"], + [11, "早上好"], + [12, "中午好"], + [13, "中午好"], + [14, "下午好"], + [17, "下午好"], + [18, "晚上好"], + [23, "晚上好"], + ])("%i 时显示 %s", (hour, expected) => { + expect(getGreeting(hour)).toBe(expected) + }) + + it("拒绝无效小时", () => { + expect(() => getGreeting(-1)).toThrow("hour must be between 0 and 23") + expect(() => getGreeting(24)).toThrow("hour must be between 0 and 23") + }) + + it("格式化中文日期与两位时间", () => { + const date = new Date(2026, 7, 20, 9, 6, 30) + expect(formatTime(date)).toBe("09:06") + expect(formatDate(date)).toBe("2026 年 8 月 20 日 星期四") + }) + + it("计算到下一分钟边界的延迟", () => { + expect(getDelayUntilNextMinute(new Date(2026, 7, 20, 9, 6, 30, 250))).toBe( + 29_750, + ) + }) +}) +``` + +- [ ] **Step 2: Run and confirm failure** + +Run: + +```bash +pnpm test -- src/features/home/lib/date-time.test.ts +``` + +Expected: FAIL because `date-time.ts` does not exist. + +- [ ] **Step 3: Implement date-time pure functions** + +Create `src/features/home/lib/date-time.ts`: + +```ts +const WEEKDAYS = [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", +] as const + +export function getGreeting(hour: number): string { + if (!Number.isInteger(hour) || hour < 0 || hour > 23) { + throw new RangeError("hour must be between 0 and 23") + } + if (hour < 5) return "夜深了" + if (hour < 12) return "早上好" + if (hour < 14) return "中午好" + if (hour < 18) return "下午好" + return "晚上好" +} + +export function formatTime(date: Date): string { + const hours = String(date.getHours()).padStart(2, "0") + const minutes = String(date.getMinutes()).padStart(2, "0") + return `${hours}:${minutes}` +} + +export function formatDate(date: Date): string { + return `${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日 ${WEEKDAYS[date.getDay()]}` +} + +export function getDelayUntilNextMinute(date: Date): number { + return 60_000 - (date.getSeconds() * 1_000 + date.getMilliseconds()) +} +``` + +- [ ] **Step 4: Run pure-function tests** + +Run: + +```bash +pnpm test -- src/features/home/lib/date-time.test.ts +``` + +Expected: all tests PASS. + +- [ ] **Step 5: Write the failing ClockGreeting component test** + +Create `src/features/home/components/clock-greeting.test.tsx`: + +```tsx +import { act, render, screen } from "@testing-library/react" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import { ClockGreeting } from "./clock-greeting" + +describe("ClockGreeting", () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 7, 20, 21, 56, 30)) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it("挂载后显示当前问候、时间和日期", () => { + render() + expect(screen.getByRole("timer")).toHaveTextContent("晚上好,21:56") + expect(screen.getByText("2026 年 8 月 20 日 星期四")).toBeInTheDocument() + }) + + it("在下一分钟边界刷新", () => { + render() + act(() => { + vi.advanceTimersByTime(30_000) + }) + expect(screen.getByRole("timer")).toHaveTextContent("21:57") + }) +}) +``` + +- [ ] **Step 6: Run and confirm component test failure** + +Run: + +```bash +pnpm test -- src/features/home/components/clock-greeting.test.tsx +``` + +Expected: FAIL because `ClockGreeting` does not exist. + +- [ ] **Step 7: Implement a minute-aligned clock hook and component** + +Create `src/features/home/hooks/use-clock.ts`: + +```ts +"use client" + +import { useEffect, useState } from "react" + +import { getDelayUntilNextMinute } from "@/features/home/lib/date-time" + +export function useClock(): Date | null { + const [now, setNow] = useState(null) + + useEffect(() => { + let intervalId: number | undefined + const current = new Date() + setNow(current) + + // 先对齐真实分钟边界,再每分钟更新,避免从挂载时刻累计漂移。 + const timeoutId = window.setTimeout(() => { + setNow(new Date()) + intervalId = window.setInterval(() => setNow(new Date()), 60_000) + }, getDelayUntilNextMinute(current)) + + return () => { + window.clearTimeout(timeoutId) + if (intervalId !== undefined) window.clearInterval(intervalId) + } + }, []) + + return now +} +``` + +Create `src/features/home/components/clock-greeting.tsx`: + +```tsx +"use client" + +import { useClock } from "@/features/home/hooks/use-clock" +import { + formatDate, + formatTime, + getGreeting, +} from "@/features/home/lib/date-time" + +export function ClockGreeting() { + const now = useClock() + + if (!now) { + return ( +
+

+ 正在同步时间… +

+ +
+ ) + } + + return ( +
+

+ {getGreeting(now.getHours())},{formatTime(now)} +

+

{formatDate(now)}

+
+ ) +} +``` + +- [ ] **Step 8: Run clock tests and checkpoint** + +Run: + +```bash +pnpm test -- src/features/home/lib/date-time.test.ts src/features/home/components/clock-greeting.test.tsx +pnpm typecheck +git diff --check +``` + +Expected: PASS; do not commit. + +--- + +### Task 4: Implement external search URLs and deterministic Local ranking + +**Files:** + +- Create: `src/features/home/lib/search-providers.ts` +- Create: `src/features/home/lib/search-providers.test.ts` +- Create: `src/features/home/lib/local-search.ts` +- Create: `src/features/home/lib/local-search.test.ts` +- Create: `src/features/home/lib/provider-preference.ts` +- Create: `src/features/home/lib/provider-preference.test.ts` + +**Interfaces:** + +- Produces: `isSearchProvider(value: unknown): value is SearchProvider`. +- Produces: `buildSearchUrl(provider, query): string | null`. +- Produces: `searchLocalLinks(query, links, categories, limit?): LocalSearchResult[]`. +- Produces: `parseStoredProvider(value, fallback): SearchProvider`. +- Consumes: types and data contracts from Task 2. + +- [ ] **Step 1: Write failing external Provider tests** + +Create `src/features/home/lib/search-providers.test.ts`: + +```ts +import { describe, expect, it } from "vitest" + +import { buildSearchUrl, isSearchProvider } from "./search-providers" + +describe("外部搜索 Provider", () => { + it.each([ + ["google", "https://www.google.com/search"], + ["bing", "https://www.bing.com/search"], + ["github", "https://github.com/search"], + ] as const)("为 %s 构建正确 URL", (provider, expectedBase) => { + const result = buildSearchUrl(provider, "Next.js 中文") + const url = new URL(result!) + expect(`${url.origin}${url.pathname}`).toBe(expectedBase) + expect(url.searchParams.get("q")).toBe("Next.js 中文") + }) + + it("空白查询不生成 URL", () => { + expect(buildSearchUrl("google", " ")).toBeNull() + }) + + it("识别合法 Provider", () => { + expect(isSearchProvider("local")).toBe(true) + expect(isSearchProvider("duckduckgo")).toBe(false) + expect(isSearchProvider(null)).toBe(false) + }) +}) +``` + +- [ ] **Step 2: Write failing Local search tests** + +Create `src/features/home/lib/local-search.test.ts` with compact fixtures and these assertions: + +```ts +import { describe, expect, it } from "vitest" + +import type { Category, NavLink } from "@/types/navigation" + +import { searchLocalLinks } from "./local-search" + +const categories: Category[] = [ + { id: "dev", title: "开发工具", description: "", icon: "code", sort: 10 }, + { id: "ai", title: "AI 工具", description: "", icon: "sparkles", sort: 20 }, +] + +const links: NavLink[] = [ + { + id: "github", + title: "GitHub", + description: "代码托管平台", + url: "https://github.com", + icon: "si:github", + categoryId: "dev", + pinned: true, + pinnedSort: 10, + keywords: ["git", "仓库"], + sort: 20, + }, + { + id: "gitlab", + title: "GitLab", + description: "DevOps 平台", + url: "https://gitlab.com", + icon: "si:gitlab", + categoryId: "dev", + pinned: false, + keywords: ["git", "ci"], + sort: 10, + }, + { + id: "chatgpt", + title: "ChatGPT", + description: "智能对话与写作", + url: "https://chatgpt.com", + icon: "si:openai", + categoryId: "ai", + pinned: true, + pinnedSort: 20, + keywords: ["人工智能", "写作"], + sort: 10, + }, +] + +describe("Local 搜索", () => { + it("标题完全匹配优先于标题包含和关键词", () => { + expect(searchLocalLinks("github", links, categories)[0].link.id).toBe( + "github", + ) + }) + + it("匹配关键词、描述、分类标题和分类 ID", () => { + expect(searchLocalLinks("人工智能", links, categories)[0].link.id).toBe( + "chatgpt", + ) + expect(searchLocalLinks("DevOps", links, categories)[0].link.id).toBe( + "gitlab", + ) + expect( + searchLocalLinks("开发工具", links, categories).map( + ({ link }) => link.id, + ), + ).toEqual(["gitlab", "github"]) + expect(searchLocalLinks("ai", links, categories)[0].link.id).toBe("chatgpt") + }) + + it("同分时按 sort 再按标题稳定排序", () => { + expect( + searchLocalLinks("git", links, categories).map(({ link }) => link.id), + ).toEqual(["gitlab", "github"]) + }) + + it("规范化大小写和首尾空白并限制结果数量", () => { + expect(searchLocalLinks(" GITHUB ", links, categories, 1)).toHaveLength(1) + }) + + it("空查询返回空数组", () => { + expect(searchLocalLinks(" ", links, categories)).toEqual([]) + }) +}) +``` + +- [ ] **Step 3: Run search tests and confirm failure** + +Run: + +```bash +pnpm test -- src/features/home/lib/search-providers.test.ts src/features/home/lib/local-search.test.ts +``` + +Expected: FAIL because both implementation modules are missing. + +- [ ] **Step 4: Implement external Provider helpers** + +Create `src/features/home/lib/search-providers.ts`: + +```ts +import { + SEARCH_PROVIDERS, + type ExternalSearchProvider, + type SearchProvider, +} from "@/types/search" + +const SEARCH_ENDPOINTS: Record = { + google: "https://www.google.com/search", + bing: "https://www.bing.com/search", + github: "https://github.com/search", +} + +export function isSearchProvider(value: unknown): value is SearchProvider { + return ( + typeof value === "string" && + SEARCH_PROVIDERS.includes(value as SearchProvider) + ) +} + +export function buildSearchUrl( + provider: ExternalSearchProvider, + query: string, +): string | null { + const normalizedQuery = query.trim() + if (!normalizedQuery) return null + + const url = new URL(SEARCH_ENDPOINTS[provider]) + url.searchParams.set("q", normalizedQuery) + return url.toString() +} +``` + +- [ ] **Step 5: Implement weighted Local search** + +Create `src/features/home/lib/local-search.ts`: + +```ts +import type { Category, NavLink } from "@/types/navigation" +import type { LocalSearchResult } from "@/types/search" + +function normalize(value: string): string { + return value.normalize("NFKC").trim().toLocaleLowerCase("zh-CN") +} + +function textScore( + value: string, + query: string, + exact: number, + prefix: number, + contains: number, +): number { + if (value === query) return exact + if (value.startsWith(query)) return prefix + if (value.includes(query)) return contains + return 0 +} + +function scoreLink( + link: NavLink, + category: Category | undefined, + query: string, +): number { + const title = normalize(link.title) + const keywordScore = Math.max( + 0, + ...link.keywords.map((keyword) => + textScore(normalize(keyword), query, 300, 260, 240), + ), + ) + + // 各区间不重叠,保证“标题 > 关键词 > 描述 > 分类”的规则不会被字段数量反转。 + return Math.max( + textScore(title, query, 500, 400, 350), + keywordScore, + normalize(link.description).includes(query) ? 180 : 0, + category ? textScore(normalize(category.title), query, 140, 130, 120) : 0, + category ? textScore(normalize(category.id), query, 100, 90, 80) : 0, + ) +} + +export function searchLocalLinks( + query: string, + links: NavLink[], + categories: Category[], + limit = 8, +): LocalSearchResult[] { + const normalizedQuery = normalize(query) + if (!normalizedQuery || limit <= 0) return [] + + const categoryById = new Map( + categories.map((category) => [category.id, category]), + ) + + return links + .map((link) => { + const category = categoryById.get(link.categoryId) + return { + link, + categoryTitle: category?.title ?? "未分类", + score: scoreLink(link, category, normalizedQuery), + } + }) + .filter(({ score }) => score > 0) + .sort( + (a, b) => + b.score - a.score || + a.link.sort - b.link.sort || + a.link.title.localeCompare(b.link.title, "zh-CN"), + ) + .slice(0, limit) +} +``` + +- [ ] **Step 6: Run pure search tests** + +Run: + +```bash +pnpm test -- src/features/home/lib/search-providers.test.ts src/features/home/lib/local-search.test.ts +``` + +Expected: all tests PASS. + +- [ ] **Step 7: Test and implement persisted Provider parsing** + +Create `src/features/home/lib/provider-preference.test.ts`: + +```ts +import { describe, expect, it } from "vitest" + +import { parseStoredProvider } from "./provider-preference" + +describe("Provider 持久化值", () => { + it("返回合法存储值", () => { + expect(parseStoredProvider("local", "google")).toBe("local") + }) + + it("非法或空值回退默认值", () => { + expect(parseStoredProvider("duckduckgo", "google")).toBe("google") + expect(parseStoredProvider(null, "bing")).toBe("bing") + }) +}) +``` + +Run and expect FAIL: + +```bash +pnpm test -- src/features/home/lib/provider-preference.test.ts +``` + +Create `src/features/home/lib/provider-preference.ts`: + +```ts +import { isSearchProvider } from "@/features/home/lib/search-providers" +import type { SearchProvider } from "@/types/search" + +export const SEARCH_PROVIDER_STORAGE_KEY = "polaris.search.provider" + +export function parseStoredProvider( + value: string | null, + fallback: SearchProvider, +): SearchProvider { + return isSearchProvider(value) ? value : fallback +} +``` + +Run: + +```bash +pnpm test -- src/features/home/lib/provider-preference.test.ts +``` + +Expected: PASS. + +- [ ] **Step 8: Record an uncommitted checkpoint** + +Run: + +```bash +pnpm format +pnpm test -- src/features/home/lib +pnpm typecheck +git diff --check +``` + +Expected: PASS; do not commit. + +--- + +### Task 5: Implement the shared glass primitive and icon fallback chain + +**Files:** + +- Create: `src/components/shared/glass-panel.tsx` +- Create: `src/components/shared/icon-candidates.ts` +- Create: `src/components/shared/icon-candidates.test.ts` +- Create: `src/components/shared/simple-icon-map.ts` +- Create: `src/components/shared/site-icon.tsx` +- Create: `src/components/shared/site-icon.test.tsx` + +**Interfaces:** + +- Produces: ``. +- Produces: `getIconCandidates(link): IconCandidate[]`. +- Produces: ``. +- Consumes: `NavLink`, `simple-icons`, and `cn()`. + +- [ ] **Step 1: Write failing icon candidate tests** + +Create `src/components/shared/icon-candidates.test.ts`: + +```ts +import { describe, expect, it } from "vitest" + +import type { NavLink } from "@/types/navigation" + +import { getIconCandidates } from "./icon-candidates" + +const baseLink: NavLink = { + id: "example", + title: "Example", + description: "Example site", + url: "https://example.com/path", + categoryId: "dev", + pinned: false, + keywords: [], + sort: 10, +} + +describe("站点图标候选链", () => { + it("local 图标后依次回退 favicon 和文字", () => { + expect(getIconCandidates({ ...baseLink, icon: "local:custom" })).toEqual([ + { kind: "image", src: "/icons/custom.svg" }, + { kind: "image", src: "https://example.com/favicon.ico" }, + { kind: "initial", value: "E" }, + ]) + }) + + it("已知 simple icon 优先使用本地 SVG 数据", () => { + expect( + getIconCandidates({ ...baseLink, icon: "si:github" })[0], + ).toMatchObject({ + kind: "simple", + slug: "github", + }) + }) + + it("未知 simple icon 跳过无效映射", () => { + expect(getIconCandidates({ ...baseLink, icon: "si:not-real" })[0]).toEqual({ + kind: "image", + src: "https://example.com/favicon.ico", + }) + }) + + it("显式 URL 图标失败后仍回退 favicon 和文字", () => { + expect( + getIconCandidates({ + ...baseLink, + icon: "url:https://cdn.example.com/icon.png", + }), + ).toEqual([ + { kind: "image", src: "https://cdn.example.com/icon.png" }, + { kind: "image", src: "https://example.com/favicon.ico" }, + { kind: "initial", value: "E" }, + ]) + }) +}) +``` + +- [ ] **Step 2: Run and confirm failure** + +Run: + +```bash +pnpm test -- src/components/shared/icon-candidates.test.ts +``` + +Expected: FAIL because the module does not exist. + +- [ ] **Step 3: Create an explicit tree-shakeable Simple Icons map** + +Create `src/components/shared/simple-icon-map.ts` importing these exact named exports from `simple-icons`: + +```ts +import { + siBilibili, + siBitbucket, + siDocker, + siFigma, + siGithub, + siGitlab, + siGmail, + siGoland, + siJetbrains, + siKubernetes, + siNetlify, + siNotion, + siNpm, + siOpenai, + siPnpm, + siPostman, + siRender, + siVercel, + siVisualstudiocode, + siYoutube, + type SimpleIcon, +} from "simple-icons" + +export const SIMPLE_ICON_MAP: Readonly> = { + bilibili: siBilibili, + bitbucket: siBitbucket, + docker: siDocker, + figma: siFigma, + github: siGithub, + gitlab: siGitlab, + gmail: siGmail, + goland: siGoland, + jetbrains: siJetbrains, + kubernetes: siKubernetes, + netlify: siNetlify, + notion: siNotion, + npm: siNpm, + openai: siOpenai, + pnpm: siPnpm, + postman: siPostman, + render: siRender, + vercel: siVercel, + vscode: siVisualstudiocode, + youtube: siYoutube, +} +``` + +- [ ] **Step 4: Implement the pure candidate chain** + +Create `src/components/shared/icon-candidates.ts`: + +```ts +import { SIMPLE_ICON_MAP } from "@/components/shared/simple-icon-map" +import type { NavLink } from "@/types/navigation" + +export type IconCandidate = + | { kind: "simple"; slug: string } + | { kind: "image"; src: string } + | { kind: "initial"; value: string } + +function getFaviconUrl(url: string): string | null { + try { + return new URL("/favicon.ico", url).toString() + } catch { + return null + } +} + +export function getIconCandidates( + link: Pick, +): IconCandidate[] { + const candidates: IconCandidate[] = [] + const icon = link.icon?.trim() + + if (icon?.startsWith("local:")) { + const name = icon.slice("local:".length) + if (name) candidates.push({ kind: "image", src: `/icons/${name}.svg` }) + } else if (icon?.startsWith("si:")) { + const slug = icon.slice("si:".length).toLowerCase() + if (SIMPLE_ICON_MAP[slug]) candidates.push({ kind: "simple", slug }) + } else if (icon?.startsWith("url:")) { + const src = icon.slice("url:".length) + if (src) candidates.push({ kind: "image", src }) + } + + const favicon = getFaviconUrl(link.url) + if ( + favicon && + !candidates.some( + (candidate) => candidate.kind === "image" && candidate.src === favicon, + ) + ) { + candidates.push({ kind: "image", src: favicon }) + } + + candidates.push({ + kind: "initial", + value: link.title.trim().charAt(0).toUpperCase() || "?", + }) + return candidates +} +``` + +- [ ] **Step 5: Run candidate tests** + +Run: + +```bash +pnpm test -- src/components/shared/icon-candidates.test.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Write failing SiteIcon component test** + +Create `src/components/shared/site-icon.test.tsx`: + +```tsx +import { fireEvent, render, screen } from "@testing-library/react" +import { describe, expect, it } from "vitest" + +import { SiteIcon } from "./site-icon" + +describe("SiteIcon", () => { + it("渲染已知 Simple Icon", () => { + const { container } = render( + , + ) + expect(container.querySelector("svg")).toBeInTheDocument() + }) + + it("远程图片连续失败后显示首字符", () => { + render( + , + ) + + fireEvent.error(screen.getByRole("img", { name: "Example 图标" })) + fireEvent.error(screen.getByRole("img", { name: "Example 图标" })) + expect(screen.getByText("E")).toBeInTheDocument() + }) +}) +``` + +- [ ] **Step 7: Implement SiteIcon and GlassPanel** + +Create `src/components/shared/site-icon.tsx`: + +```tsx +"use client" + +/* eslint-disable @next/next/no-img-element -- favicon 来源由数据动态决定,静态 Image 配置无法预枚举域名。 */ +import { useEffect, useMemo, useState } from "react" + +import { getIconCandidates } from "@/components/shared/icon-candidates" +import { SIMPLE_ICON_MAP } from "@/components/shared/simple-icon-map" +import { cn } from "@/lib/utils" +import type { NavLink } from "@/types/navigation" + +type SiteIconProps = { + link: Pick + size?: number + className?: string +} + +export function SiteIcon({ link, size = 36, className }: SiteIconProps) { + const candidates = useMemo( + () => getIconCandidates(link), + [link.icon, link.title, link.url], + ) + const [candidateIndex, setCandidateIndex] = useState(0) + + useEffect(() => setCandidateIndex(0), [candidates]) + + const candidate = candidates[candidateIndex] + const wrapperClassName = cn("site-icon", className) + const style = { width: size, height: size } + + if (candidate.kind === "simple") { + const icon = SIMPLE_ICON_MAP[candidate.slug] + return ( + + ) + } + + if (candidate.kind === "image") { + return ( + + {`${link.title} + setCandidateIndex((index) => + Math.min(index + 1, candidates.length - 1), + ) + } + /> + + ) + } + + return ( + + ) +} +``` + +Create `src/components/shared/glass-panel.tsx`: + +```tsx +import { createElement, type ComponentPropsWithoutRef } from "react" + +import { cn } from "@/lib/utils" + +type GlassPanelProps = ComponentPropsWithoutRef<"div"> & { + as?: "div" | "section" | "aside" +} + +export function GlassPanel({ + as = "div", + className, + ...props +}: GlassPanelProps) { + return createElement(as, { + className: cn("glass-panel", className), + ...props, + }) +} +``` + +- [ ] **Step 8: Run shared component checks** + +Run: + +```bash +pnpm test -- src/components/shared +pnpm typecheck +pnpm lint +``` + +Expected: PASS. + +- [ ] **Step 9: Record an uncommitted checkpoint** + +Run `git diff --check`; expected PASS. Do not commit. + +--- + +### Task 6: Implement the persistent desktop navigation shell + +**Files:** + +- Create: `src/features/navigation/lib/sidebar-preference.ts` +- Create: `src/features/navigation/lib/sidebar-preference.test.ts` +- Create: `src/features/navigation/components/category-icon.tsx` +- Create: `src/features/navigation/components/sidebar.tsx` +- Create: `src/features/navigation/components/app-shell.tsx` +- Create: `src/features/navigation/components/app-shell.test.tsx` + +**Interfaces:** + +- Produces: `parseSidebarPreference(value, fallback): boolean`. +- Produces: `AppShell({ categories, siteName, defaultCollapsed, children })`. +- Consumes: `Category[]`, shadcn Button/Tooltip, lucide icons, `/brand/logo.svg`. + +- [ ] **Step 1: Test sidebar preference parsing** + +Create `src/features/navigation/lib/sidebar-preference.test.ts`: + +```ts +import { describe, expect, it } from "vitest" + +import { parseSidebarPreference } from "./sidebar-preference" + +describe("侧栏持久化值", () => { + it("解析显式布尔字符串", () => { + expect(parseSidebarPreference("true", false)).toBe(true) + expect(parseSidebarPreference("false", true)).toBe(false) + }) + + it("损坏或缺失值回退设置默认值", () => { + expect(parseSidebarPreference("collapsed", false)).toBe(false) + expect(parseSidebarPreference(null, true)).toBe(true) + }) +}) +``` + +Run and expect FAIL: + +```bash +pnpm test -- src/features/navigation/lib/sidebar-preference.test.ts +``` + +Create `src/features/navigation/lib/sidebar-preference.ts`: + +```ts +export const SIDEBAR_STORAGE_KEY = "polaris.sidebar.collapsed" + +export function parseSidebarPreference( + value: string | null, + fallback: boolean, +): boolean { + if (value === "true") return true + if (value === "false") return false + return fallback +} +``` + +Run the test again; expected PASS. + +- [ ] **Step 2: Write failing AppShell interaction tests** + +Create `src/features/navigation/components/app-shell.test.tsx`: + +```tsx +import { fireEvent, render, screen, waitFor } from "@testing-library/react" +import { beforeEach, describe, expect, it } from "vitest" + +import type { Category } from "@/types/navigation" + +import { AppShell } from "./app-shell" + +const categories: Category[] = [ + { id: "dev", title: "开发工具", description: "开发", icon: "code", sort: 10 }, +] + +describe("AppShell", () => { + beforeEach(() => window.localStorage.clear()) + + it("切换侧栏并持久化状态", async () => { + render( + +
首页内容
+
, + ) + + const toggle = screen.getByRole("button", { name: "折叠侧边栏" }) + fireEvent.click(toggle) + + expect(screen.getByTestId("app-shell")).toHaveAttribute( + "data-sidebar-collapsed", + "true", + ) + expect(window.localStorage.getItem("polaris.sidebar.collapsed")).toBe( + "true", + ) + }) + + it("hydration 后恢复持久化状态", async () => { + window.localStorage.setItem("polaris.sidebar.collapsed", "true") + render( + +
首页内容
+
, + ) + + await waitFor(() => { + expect(screen.getByTestId("app-shell")).toHaveAttribute( + "data-sidebar-collapsed", + "true", + ) + }) + }) + + it("分类项可聚焦但保持禁用", () => { + render( + +
首页内容
+
, + ) + + expect( + screen.getByRole("button", { name: "开发工具,分类页将在下一阶段提供" }), + ).toHaveAttribute("aria-disabled", "true") + expect( + screen.queryByRole("link", { name: /开发工具/ }), + ).not.toBeInTheDocument() + }) +}) +``` + +- [ ] **Step 3: Run and confirm AppShell failure** + +Run: + +```bash +pnpm test -- src/features/navigation/components/app-shell.test.tsx +``` + +Expected: FAIL because `AppShell` does not exist. + +- [ ] **Step 4: Implement the category icon whitelist** + +Create `src/features/navigation/components/category-icon.tsx`: + +```tsx +import { + BookOpen, + CirclePlay, + Code2, + Coffee, + Palette, + Sparkles, + Zap, + type LucideIcon, +} from "lucide-react" + +const CATEGORY_ICONS: Readonly> = { + sparkles: Sparkles, + code: Code2, + palette: Palette, + "book-open": BookOpen, + zap: Zap, + "circle-play": CirclePlay, + coffee: Coffee, +} + +export function CategoryIcon({ name }: { name: string }) { + const Icon = CATEGORY_ICONS[name] ?? Sparkles + return
`; +- ID is `${listboxId}-option-${index}`; +- `aria-selected={index === activeIndex}`; +- mouse move updates active index without navigation; +- render `SiteIcon`, title and category title; +- empty array renders a panel containing `没有匹配的网站`. + +- [ ] **Step 5: Implement SearchBox state machine** + +Use this prop contract: + +```ts +type SearchBoxProps = { + links: NavLink[] + categories: Category[] + defaultProvider: SearchProvider + enabledProviders: SearchProvider[] + onExternalNavigate?: (url: string) => void +} +``` + +Implementation requirements: + +1. Initial state uses `defaultProvider`, empty query, active index `0`. +2. Mount effect: + - focuses input; + - reads `SEARCH_PROVIDER_STORAGE_KEY` in `try/catch`; + - applies `parseStoredProvider` only if the result is enabled. +3. Global key listener focuses input on `(ctrlKey || metaKey) && key.toLowerCase() === "k"` and calls `preventDefault()`. +4. Provider change persists inside `try/catch`, resets active index, and never opens Local results for external providers. +5. `results` is memoized from `searchLocalLinks(query, links, categories, 8)` only in Local mode. +6. External submit calls `buildSearchUrl` and then `onExternalNavigate ?? ((url) => window.location.assign(url))`. +7. Local `ArrowDown`/`ArrowUp` wraps around the result length. +8. Local `Enter` calls `.click()` on the selected result anchor ref. +9. `Escape` clears query and resets active index. +10. Input uses `role="combobox"`, exact accessible label `搜索网站、书签或关键词`, placeholder `搜索网站、书签或关键词...`, and correct `aria-expanded`, `aria-controls`, `aria-activedescendant`, `aria-autocomplete="list"`. +11. Platform hint uses `⌘K` when `navigator.userAgent` contains `Mac`, otherwise `Ctrl K`; detect after mount so SSR and hydration remain stable. + +Add concise Chinese comments only around strict Provider separation, Local keyboard delegation to the real anchor, and hydration-safe preference restoration. + +- [ ] **Step 6: Run SearchBox tests and fix only contract mismatches** + +Run: + +```bash +pnpm test -- src/features/home/components/search-box.test.tsx +``` + +Expected: all tests PASS. Do not weaken accessible names or replace real Local anchors with click-only `div` elements. + +- [ ] **Step 7: Run the full home-domain suite** + +Run: + +```bash +pnpm test -- src/features/home +pnpm typecheck +pnpm lint +``` + +Expected: PASS. + +- [ ] **Step 8: Record an uncommitted checkpoint** + +Run `git diff --check`; expected PASS. Do not commit. + +--- + +### Task 8: Compose quick links and the Server Component homepage + +**Files:** + +- Create: `src/features/home/components/quick-links.tsx` +- Create: `src/features/home/components/quick-links.test.tsx` +- Create: `src/features/home/components/home-content.tsx` +- Modify: `src/app/page.tsx` +- Modify: `src/app/layout.tsx` + +**Interfaces:** + +- Produces: ``. +- Produces: ``. +- Consumes: services from Task 2, AppShell from Task 6, and interactive components from Tasks 3 and 7. + +- [ ] **Step 1: Write failing QuickLinks tests** + +Create `src/features/home/components/quick-links.test.tsx`: + +```tsx +import { render, screen } from "@testing-library/react" +import { describe, expect, it } from "vitest" + +import type { NavLink } from "@/types/navigation" + +import { QuickLinks } from "./quick-links" + +const links: NavLink[] = [ + { + id: "github", + title: "GitHub", + description: "代码托管", + url: "https://github.com", + icon: "si:github", + categoryId: "dev", + pinned: true, + pinnedSort: 10, + keywords: [], + sort: 10, + }, + { + id: "chatgpt", + title: "ChatGPT", + description: "AI 助手", + url: "https://chatgpt.com", + icon: "si:openai", + categoryId: "ai", + pinned: true, + pinnedSort: 20, + keywords: [], + sort: 10, + }, +] + +describe("QuickLinks", () => { + it("按输入顺序渲染安全的新标签页链接", () => { + render() + const renderedLinks = screen.getAllByRole("link") + + expect(renderedLinks.map((link) => link.textContent)).toEqual([ + "GitHub", + "ChatGPT", + ]) + for (const link of renderedLinks) { + expect(link).toHaveAttribute("target", "_blank") + expect(link).toHaveAttribute("rel", "noopener noreferrer") + } + }) + + it("只展示图标和名称,不展示描述", () => { + render() + expect(screen.queryByText("代码托管")).not.toBeInTheDocument() + expect(screen.queryByText("AI 助手")).not.toBeInTheDocument() + }) +}) +``` + +- [ ] **Step 2: Run and confirm QuickLinks failure** + +Run: + +```bash +pnpm test -- src/features/home/components/quick-links.test.tsx +``` + +Expected: FAIL because `QuickLinks` does not exist. + +- [ ] **Step 3: Implement QuickLinks** + +Create `src/features/home/components/quick-links.tsx`: + +```tsx +import { SiteIcon } from "@/components/shared/site-icon" +import type { NavLink } from "@/types/navigation" + +export function QuickLinks({ links }: { links: NavLink[] }) { + return ( +
+ +
+ {links.map((link) => ( + + + {link.title} + + ))} +
+
+ ) +} +``` + +Run its test; expected PASS. + +- [ ] **Step 4: Implement HomeContent composition** + +Create `src/features/home/components/home-content.tsx`: + +```tsx +import { ClockGreeting } from "@/features/home/components/clock-greeting" +import { QuickLinks } from "@/features/home/components/quick-links" +import { SearchBox } from "@/features/home/components/search-box" +import type { Category, NavLink } from "@/types/navigation" +import type { AppSettings } from "@/types/settings" + +type HomeContentProps = { + categories: Category[] + links: NavLink[] + pinnedLinks: NavLink[] + settings: AppSettings +} + +export function HomeContent({ + categories, + links, + pinnedLinks, + settings, +}: HomeContentProps) { + return ( +
+
+ + + +
+
+ ) +} +``` + +- [ ] **Step 5: Replace the smoke page with service-backed Server composition** + +Replace `src/app/page.tsx`: + +```tsx +import { HomeContent } from "@/features/home/components/home-content" +import { AppShell } from "@/features/navigation/components/app-shell" +import { getCategories } from "@/services/categories" +import { getLinks, getPinnedLinks } from "@/services/links" +import { getSettings } from "@/services/settings" + +export default function HomePage() { + const categories = getCategories() + const links = getLinks() + const pinnedLinks = getPinnedLinks() + const settings = getSettings() + + return ( + + + + ) +} +``` + +This file must not contain `"use client"` and must not import JSON directly. + +- [ ] **Step 6: Finalize root metadata and viewport** + +In `src/app/layout.tsx`, extend the type import to `import type { Metadata, Viewport } from "next"`, then set: + +```ts +export const metadata: Metadata = { + title: "Polaris — 个人导航起始页", + description: "快速搜索并访问常用网站的个人导航起始页。", +} +``` + +Add: + +```ts +export const viewport: Viewport = { + colorScheme: "light", + themeColor: "#dcebf5", +} +``` + +Keep ``. + +- [ ] **Step 7: Run composition checks** + +Run: + +```bash +pnpm test -- src/features/home/components/quick-links.test.tsx +pnpm typecheck +pnpm lint +pnpm build +``` + +Expected: PASS and `out/index.html` exists. Do not commit. + +--- + +### Task 9: Apply the Polaris visual system, responsive desktop layout, and accessibility polish + +**Files:** + +- Modify: `src/app/globals.css` +- Modify: navigation, search, clock, quick-link and shared components only where class names or semantics need alignment +- Test: existing component suites + +**Interfaces:** + +- Consumes: all rendered class names from Tasks 3, 5, 6, 7 and 8. +- Produces: the final CSS-gradient glass homepage at 1024×768, 1440×900 and 1920×1080. + +- [ ] **Step 1: Replace global CSS with semantic Tailwind v4 tokens** + +At the top of `src/app/globals.css`, use: + +```css +@import "tailwindcss"; + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--text-primary); + --color-primary: var(--primary); + --color-primary-foreground: #ffffff; + --color-muted-foreground: var(--text-secondary); + --color-border: var(--border-subtle); + --radius-lg: var(--radius-card); + --radius-xl: var(--radius-panel); +} + +:root { + color-scheme: light; + --background: #dcebf5; + --text-primary: #0f172a; + --text-secondary: #64748b; + --text-muted: #94a3b8; + --primary: #3b82f6; + --primary-hover: #2563eb; + --primary-soft: rgba(59, 130, 246, 0.1); + --surface-sidebar: rgba(255, 255, 255, 0.68); + --surface-search: rgba(255, 255, 255, 0.72); + --surface-card: rgba(255, 255, 255, 0.62); + --surface-card-hover: rgba(255, 255, 255, 0.78); + --border-glass: rgba(255, 255, 255, 0.58); + --border-subtle: rgba(15, 23, 42, 0.08); + --shadow-soft: 0 14px 40px rgba(15, 23, 42, 0.1); + --shadow-medium: 0 20px 60px rgba(15, 23, 42, 0.12); + --shadow-panel: 0 24px 80px rgba(15, 23, 42, 0.1); + --radius-card: 20px; + --radius-panel: 24px; + --sidebar-expanded: 240px; + --sidebar-collapsed: 72px; + --layout-gap: 20px; +} +``` + +- [ ] **Step 2: Implement the CSS-only atmospheric background** + +Use exact global foundations: + +```css +* { + box-sizing: border-box; +} + +html, +body { + min-height: 100%; +} + +body { + margin: 0; + overflow-x: hidden; + color: var(--text-primary); + background: + radial-gradient( + circle at 12% 15%, + rgba(255, 245, 239, 0.92) 0, + transparent 34% + ), + radial-gradient( + circle at 82% 14%, + rgba(194, 222, 246, 0.92) 0, + transparent 38% + ), + radial-gradient( + circle at 48% 82%, + rgba(226, 239, 247, 0.72) 0, + transparent 44% + ), + linear-gradient(135deg, #eef5fa 0%, #dceaf4 48%, #cbdff0 100%); + font-family: + Inter, + "SF Pro Display", + "SF Pro Text", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + "PingFang SC", + "Hiragino Sans GB", + "Microsoft YaHei", + sans-serif; +} + +body::before { + position: fixed; + inset: 0; + z-index: -1; + content: ""; + pointer-events: none; + background: + linear-gradient( + 180deg, + rgba(255, 255, 255, 0.2), + rgba(255, 255, 255, 0.08) + ), + repeating-linear-gradient( + 165deg, + transparent 0 84px, + rgba(255, 255, 255, 0.08) 85px 86px + ); + backdrop-filter: blur(8px); +} + +button, +input, +a { + font: inherit; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +:focus-visible { + outline: 3px solid rgba(59, 130, 246, 0.34); + outline-offset: 3px; +} +``` + +Do not reference `docs/assets/Polaris页面设计图.png` from CSS. + +- [ ] **Step 3: Style the shell and sidebar** + +Implement these behavior-defining rules; values may only vary by at most 2px during visual tuning: + +```css +.app-shell { + --sidebar-width: var(--sidebar-expanded); + min-height: 100svh; +} + +.app-shell[data-sidebar-collapsed="true"] { + --sidebar-width: var(--sidebar-collapsed); +} + +.app-shell:not([data-hydrated="true"]) *, +.app-shell:not([data-hydrated="true"]) *::before, +.app-shell:not([data-hydrated="true"]) *::after { + transition: none !important; +} + +.sidebar { + position: fixed; + inset: 20px auto 20px 20px; + z-index: 20; + width: var(--sidebar-width); + overflow: visible; + border: 1px solid var(--border-glass); + border-radius: var(--radius-panel); + background: var(--surface-sidebar); + box-shadow: var(--shadow-panel); + backdrop-filter: blur(24px); + transition: width 180ms ease; +} + +.app-shell__content { + min-height: 100svh; + margin-left: calc(var(--sidebar-width) + 40px); + transition: margin-left 180ms ease; +} +``` + +Sidebar internal requirements: + +- header height `72px`, horizontal padding `16px`; +- logo `40px`; expanded name visible, collapsed name visually hidden; +- nav item height `48px`, radius `16px`, gap `12px`; +- active home background `var(--primary-soft)` and color `var(--primary-hover)`; +- disabled categories retain readable `#334155`, cursor `not-allowed`, opacity at least `0.78`; +- collapsed nav icons stay centered; +- tooltip must remain inside viewport and above the main content. + +- [ ] **Step 4: Style the main hierarchy** + +Use: + +```css +.home-content { + display: grid; + min-height: 100svh; + place-items: start center; + padding: clamp(108px, 14vh, 156px) 40px 64px; +} + +.home-content__inner { + width: min(100%, 880px); +} + +.clock-greeting { + text-align: center; +} + +.clock-greeting__time { + min-height: 1.2em; + margin: 0; + color: var(--text-primary); + font-size: clamp(44px, 4.2vw, 56px); + font-weight: 700; + letter-spacing: -0.035em; +} + +.clock-greeting__date { + margin: 12px 0 0; + color: var(--text-secondary); + font-size: 16px; + line-height: 1.6; +} +``` + +- [ ] **Step 5: Style SearchBox and Local results** + +Required geometry: + +- search wrapper width `min(100%, 760px)`, height `68px`, centered, margin-top `40px`; +- surface `var(--surface-search)`, blur `22px`, radius `24px`, `var(--shadow-medium)`; +- Provider trigger min width sufficient for current label; no layout jump between labels; +- input takes remaining width and has no native border/background; +- hint is a muted bordered capsule; +- Local panel top gap `8px`, same width, max height enough for 8 rows with internal scroll; +- row height at least `58px`; selected row uses `var(--primary-soft)`; +- empty state minimum height `96px`; +- Local panel z-index above quick links. + +Use `transition` only on background, color, transform, border-color and box-shadow; do not use `transition: all`. + +- [ ] **Step 6: Style QuickLinks and SiteIcon** + +Required geometry: + +```css +.quick-links { + width: min(100%, 880px); + margin-top: 44px; +} + +.quick-links__title { + margin: 0 0 18px; + color: var(--text-primary); + font-size: 16px; + font-weight: 650; +} + +.quick-links__grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px 18px; +} + +.quick-link-card { + display: flex; + min-width: 0; + height: 76px; + align-items: center; + gap: 14px; + padding: 0 20px; + color: var(--text-primary); + text-decoration: none; + border: 1px solid var(--border-glass); + border-radius: var(--radius-card); + background: var(--surface-card); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(20px); + transition: + transform 180ms ease, + background 180ms ease, + border-color 180ms ease, + box-shadow 180ms ease; +} + +.quick-link-card:hover { + transform: translateY(-2px); + border-color: rgba(255, 255, 255, 0.76); + background: var(--surface-card-hover); + box-shadow: 0 18px 50px rgba(15, 23, 42, 0.14); +} +``` + +`site-icon` must center SVG/image, prevent shrinking, round image corners, and render initials on a light translucent square. + +- [ ] **Step 7: Add desktop narrowing and reduced-motion rules** + +Use: + +```css +@media (max-width: 1199px) { + :root { + --sidebar-expanded: 220px; + } + + .home-content { + padding-inline: 28px; + } + + .quick-links__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} +``` + +At 1024px, all text and controls must remain visible with no horizontal scroll. Do not add mobile drawer behavior. + +- [ ] **Step 8: Run all automated checks after visual changes** + +Run: + +```bash +pnpm format +pnpm lint +pnpm typecheck +pnpm test +pnpm build +``` + +Expected: all commands exit `0`. + +- [ ] **Step 9: Record an uncommitted checkpoint** + +Run: + +```bash +git diff --check +git status --short +``` + +Expected: all homepage files remain uncommitted; no category route exists. + +--- + +### Task 10: Run real-browser acceptance, reconcile docs, and create the single final commit + +**Files:** + +- Modify only files needed to fix verified acceptance defects. +- Verify: all files created in Tasks 1–9. +- Commit: entire `feat/polaris-homepage` target after every gate passes. + +**Interfaces:** + +- Consumes: the complete homepage. +- Produces: verified static build and one local Git commit; no push. + +- [ ] **Step 1: Run the final clean automated gate** + +Run from `ts/web/nextjs/polaris`: + +```bash +pnpm format:check +pnpm lint +pnpm typecheck +pnpm test +pnpm build +``` + +Expected: five commands exit `0`; record test counts and build route summary for the final report. + +- [ ] **Step 2: Launch the real application using the project run workflow** + +Invoke the `run` skill, then launch `pnpm dev` on an available localhost port. Do not substitute static file inspection for a real rendered browser. + +Verify the console contains no hydration warning, uncaught exception, missing asset error, or React key warning. + +- [ ] **Step 3: Inspect 1440×900** + +Verify: + +- sidebar width is 240px and remains 20px from viewport edges; +- logo, title, selected home item and all seven categories are aligned; +- greeting is the first visual focus, search second, quick links third; +- search is centered and approximately 760×68px; +- quick links are 4 columns × 2 rows in the approved order; +- cards and search glass surfaces remain readable over the gradient; +- Local panel aligns with SearchBox and does not push content layout; +- no horizontal or vertical clipping. + +Capture a screenshot if the run environment supports it. + +- [ ] **Step 4: Inspect 1920×1080** + +Verify: + +- main content does not stretch beyond 880px; +- background gradients fill the viewport without bitmap scaling artifacts; +- vertical placement stays in the upper-middle region rather than dead center; +- shadows remain subtle and the large empty space feels intentional; +- sidebar height and fixed positioning remain correct. + +Capture a screenshot if supported. + +- [ ] **Step 5: Inspect 1024×768** + +Verify: + +- sidebar uses the 220px narrowed desktop width; +- main area has no horizontal overflow; +- search remains fully usable; +- quick links switch to 2 columns; +- all seven categories remain reachable within the viewport; +- no mobile drawer or unintended mobile layout appears. + +Capture a screenshot if supported. + +- [ ] **Step 6: Exercise every interactive acceptance path** + +Verify manually in the real app: + +1. Search input auto-focuses on load. +2. `Ctrl+K` or `⌘K` returns focus to search. +3. Provider can switch among all four values and survives refresh. +4. Google/Bing/GitHub submit correctly encoded URLs in the current tab; use a harmless query and return to Polaris after each check. +5. Local results appear only in Local mode. +6. Local `↑`, `↓`, `Enter`, mouse click and `Esc` work. +7. Local no-result text is exact. +8. Sidebar collapses to 72px, expands to 240px, and survives refresh. +9. Classification items show the temporary tooltip and never navigate. +10. Every quick link opens a new tab with the correct URL. +11. No broken brand icon appears; fallback can be verified by temporarily changing one icon in browser devtools only, without committing fixture corruption. +12. Focus rings are visible and reduced-motion mode removes perceptible transitions. + +- [ ] **Step 7: Fix verified defects with targeted regression tests** + +For each defect: + +1. Add or extend the nearest existing test so it fails for the observed behavior. +2. Run that specific test and record the expected failure. +3. Apply the smallest implementation or CSS correction. +4. Re-run the specific test. +5. Re-run `pnpm lint`, `pnpm typecheck`, `pnpm test`, and `pnpm build` after all corrections. + +Do not add unrelated features during this step. + +- [ ] **Step 8: Verify documentation and scope consistency** + +Confirm: + +- all three moved design documents link to one another; +- homepage document references `../assets/Polaris页面设计图.png`; +- project document references `../../public/brand/logo.svg`; +- all `NavLink` examples and schema text distinguish `sort` from `pinnedSort`; +- no root-level duplicate docs or assets remain; +- no `src/app/categories/` directory exists; +- implementation spec status remains “用户已批准,可进入实施”. + +- [ ] **Step 9: Run repository-level hygiene checks** + +Run from repository root: + +```bash +git diff --check +git status --short +git diff --stat +``` + +Inspect all unexpected files before staging. Ensure `.next/`, `out/`, `coverage/`, and `node_modules/` are absent from status. + +- [ ] **Step 10: Create the one authorized feature commit** + +Stage only the repository ignore update and Polaris project: + +```bash +git add .gitignore ts/web/nextjs/polaris +``` + +Review staged scope: + +```bash +git diff --cached --stat +git diff --cached --check +``` + +Commit without pushing: + +```bash +git commit -m "$(cat <<'EOF' +feat(ts): 实现 Polaris 可交互首页 + +Co-Authored-By: Claude Opus 4.8 (1M context) +EOF +)" +``` + +Expected: one new commit on `feat/polaris-homepage`; working tree clean. + +- [ ] **Step 11: Report evidence and stop at the homepage boundary** + +Final report must include: + +- commit hash and message; +- dependency/tool versions actually resolved in `package.json`; +- exact results for format, lint, typecheck, test and build; +- visual results at all three viewport sizes; +- interaction checks performed; +- screenshots or their saved locations when available; +- any remaining visual deviation or browser limitation; +- explicit statement that classification pages were not implemented and further first-stage work awaits user approval. diff --git a/ts/web/nextjs/polaris/eslint.config.mjs b/ts/web/nextjs/polaris/eslint.config.mjs new file mode 100644 index 0000000..cbaddc5 --- /dev/null +++ b/ts/web/nextjs/polaris/eslint.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig, globalIgnores } from "eslint/config" +import nextVitals from "eslint-config-next/core-web-vitals" +import nextTypeScript from "eslint-config-next/typescript" + +export default defineConfig([ + ...nextVitals, + ...nextTypeScript, + globalIgnores([".next/**", "out/**", "coverage/**", "next-env.d.ts"]), +]) diff --git a/ts/web/nextjs/polaris/next-env.d.ts b/ts/web/nextjs/polaris/next-env.d.ts new file mode 100644 index 0000000..ce4e94a --- /dev/null +++ b/ts/web/nextjs/polaris/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/ts/web/nextjs/polaris/next.config.ts b/ts/web/nextjs/polaris/next.config.ts new file mode 100644 index 0000000..9c8987d --- /dev/null +++ b/ts/web/nextjs/polaris/next.config.ts @@ -0,0 +1,11 @@ +import type { NextConfig } from "next" + +const nextConfig: NextConfig = { + output: "export", + trailingSlash: true, + images: { + unoptimized: true, + }, +} + +export default nextConfig diff --git a/ts/web/nextjs/polaris/package.json b/ts/web/nextjs/polaris/package.json new file mode 100644 index 0000000..7dd0c45 --- /dev/null +++ b/ts/web/nextjs/polaris/package.json @@ -0,0 +1,52 @@ +{ + "name": "polaris", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "pnpm@11.24.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "engines": { + "node": ">=22.22.2" + }, + "dependencies": { + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-tooltip": "^1.2.16", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.34.0", + "next": "^16.3.3", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "simple-icons": "^16.28.0", + "tailwind-merge": "^3.6.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.3.3", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.6", + "@types/node": "^26.3.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "@vitejs/plugin-react": "^6.1.0", + "eslint": "^9.39.5", + "eslint-config-next": "^16.3.3", + "jsdom": "^30.0.1", + "postcss": "^8.5.26", + "prettier": "^3.9.6", + "prettier-plugin-tailwindcss": "^0.8.1", + "tailwindcss": "^4.3.3", + "typescript": "^5.9.3", + "vitest": "^4.1.11" + } +} diff --git a/ts/web/nextjs/polaris/pnpm-lock.yaml b/ts/web/nextjs/polaris/pnpm-lock.yaml new file mode 100644 index 0000000..250a8e1 --- /dev/null +++ b/ts/web/nextjs/polaris/pnpm-lock.yaml @@ -0,0 +1,7945 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + dependencies: + "@radix-ui/react-dropdown-menu": + specifier: ^2.1.24 + version: 2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-slot": + specifier: ^1.3.3 + version: 1.3.3(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-tooltip": + specifier: ^1.2.16 + version: 1.2.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^1.34.0 + version: 1.34.0(react@19.2.8) + next: + specifier: ^16.3.3 + version: 16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.3.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: + specifier: ^19.2.8 + version: 19.2.8 + react-dom: + specifier: ^19.2.8 + version: 19.2.8(react@19.2.8) + simple-icons: + specifier: ^16.28.0 + version: 16.28.0 + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 + devDependencies: + "@tailwindcss/postcss": + specifier: ^4.3.3 + version: 4.3.3 + "@testing-library/jest-dom": + specifier: ^7.0.1 + version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11(@types/node@26.3.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0))) + "@testing-library/react": + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@testing-library/user-event": + specifier: ^14.6.6 + version: 14.6.6(@testing-library/dom@10.4.1) + "@types/node": + specifier: ^26.3.0 + version: 26.3.0 + "@types/react": + specifier: ^19.2.18 + version: 19.2.18 + "@types/react-dom": + specifier: ^19.2.5 + version: 19.2.5(@types/react@19.2.18) + "@vitejs/plugin-react": + specifier: ^6.1.0 + version: 6.1.0(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0)) + eslint: + specifier: ^9.39.5 + version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-config-next: + specifier: ^16.3.3 + version: 16.3.3(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + jsdom: + specifier: ^30.0.1 + version: 30.0.1 + postcss: + specifier: ^8.5.26 + version: 8.5.26 + prettier: + specifier: ^3.9.6 + version: 3.9.6 + prettier-plugin-tailwindcss: + specifier: ^0.8.1 + version: 0.8.1(prettier@3.9.6) + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.3.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0)) + +packages: + "@adobe/css-tools@4.5.0": + resolution: + { + integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==, + } + + "@alloc/quick-lru@5.2.0": + resolution: + { + integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==, + } + engines: { node: ">=10" } + + "@asamuzakjp/css-color@6.0.7": + resolution: + { + integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==, + } + engines: { node: ^22.13.0 || >=24.0.0 } + + "@asamuzakjp/dom-selector@8.3.2": + resolution: + { + integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==, + } + engines: { node: ^22.13.0 || >=24.0.0 } + + "@babel/code-frame@7.29.7": + resolution: + { + integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==, + } + engines: { node: ">=6.9.0" } + + "@babel/compat-data@7.29.7": + resolution: + { + integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==, + } + engines: { node: ">=6.9.0" } + + "@babel/core@7.29.7": + resolution: + { + integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==, + } + engines: { node: ">=6.9.0" } + + "@babel/generator@7.29.8": + resolution: + { + integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-compilation-targets@7.29.7": + resolution: + { + integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-globals@7.29.7": + resolution: + { + integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-module-imports@7.29.7": + resolution: + { + integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-module-transforms@7.29.7": + resolution: + { + integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0 + + "@babel/helper-string-parser@7.29.7": + resolution: + { + integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-validator-identifier@7.29.7": + resolution: + { + integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-validator-option@7.29.7": + resolution: + { + integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helpers@7.29.7": + resolution: + { + integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==, + } + engines: { node: ">=6.9.0" } + + "@babel/parser@7.29.8": + resolution: + { + integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==, + } + engines: { node: ">=6.0.0" } + hasBin: true + + "@babel/runtime@7.29.7": + resolution: + { + integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==, + } + engines: { node: ">=6.9.0" } + + "@babel/template@7.29.7": + resolution: + { + integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==, + } + engines: { node: ">=6.9.0" } + + "@babel/traverse@7.29.8": + resolution: + { + integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==, + } + engines: { node: ">=6.9.0" } + + "@babel/types@7.29.8": + resolution: + { + integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==, + } + engines: { node: ">=6.9.0" } + + "@bramus/specificity@2.4.2": + resolution: + { + integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==, + } + hasBin: true + + "@csstools/color-helpers@6.1.1": + resolution: + { + integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==, + } + engines: { node: ">=20.19.0" } + + "@csstools/css-calc@3.3.0": + resolution: + { + integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==, + } + engines: { node: ">=20.19.0" } + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + + "@csstools/css-color-parser@4.2.1": + resolution: + { + integrity: sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A==, + } + engines: { node: ">=20.19.0" } + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + + "@csstools/css-parser-algorithms@4.0.0": + resolution: + { + integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==, + } + engines: { node: ">=20.19.0" } + peerDependencies: + "@csstools/css-tokenizer": ^4.0.0 + + "@csstools/css-syntax-patches-for-csstree@1.1.9": + resolution: + { + integrity: sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A==, + } + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + "@csstools/css-tokenizer@4.0.0": + resolution: + { + integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==, + } + engines: { node: ">=20.19.0" } + + "@emnapi/core@1.10.0": + resolution: + { + integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==, + } + + "@emnapi/runtime@1.10.0": + resolution: + { + integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==, + } + + "@emnapi/runtime@1.11.3": + resolution: + { + integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==, + } + + "@emnapi/wasi-threads@1.2.1": + resolution: + { + integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==, + } + + "@eslint-community/eslint-utils@4.10.1": + resolution: + { + integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + "@eslint-community/eslint-utils@4.9.1": + resolution: + { + integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + "@eslint-community/regexpp@4.12.2": + resolution: + { + integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } + + "@eslint/config-array@0.21.2": + resolution: + { + integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/config-helpers@0.4.2": + resolution: + { + integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/core@0.17.0": + resolution: + { + integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/eslintrc@3.3.6": + resolution: + { + integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/js@9.39.5": + resolution: + { + integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/object-schema@2.1.7": + resolution: + { + integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/plugin-kit@0.4.1": + resolution: + { + integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@exodus/bytes@1.15.1": + resolution: + { + integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + + "@floating-ui/core@1.8.0": + resolution: + { + integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==, + } + + "@floating-ui/dom@1.8.0": + resolution: + { + integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==, + } + + "@floating-ui/react-dom@2.1.9": + resolution: + { + integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==, + } + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + + "@floating-ui/utils@0.2.12": + resolution: + { + integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==, + } + + "@humanfs/core@0.19.2": + resolution: + { + integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==, + } + engines: { node: ">=18.18.0" } + + "@humanfs/node@0.16.8": + resolution: + { + integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==, + } + engines: { node: ">=18.18.0" } + + "@humanfs/types@0.15.0": + resolution: + { + integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==, + } + engines: { node: ">=18.18.0" } + + "@humanwhocodes/module-importer@1.0.1": + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, + } + engines: { node: ">=12.22" } + + "@humanwhocodes/retry@0.4.3": + resolution: + { + integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, + } + engines: { node: ">=18.18" } + + "@img/colour@1.1.0": + resolution: + { + integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==, + } + engines: { node: ">=18" } + + "@img/sharp-darwin-arm64@0.35.4": + resolution: + { + integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==, + } + engines: { node: ">=20.9.0" } + cpu: [arm64] + os: [darwin] + + "@img/sharp-darwin-x64@0.35.4": + resolution: + { + integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==, + } + engines: { node: ">=20.9.0" } + cpu: [x64] + os: [darwin] + + "@img/sharp-freebsd-wasm32@0.35.4": + resolution: + { + integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==, + } + engines: { node: ">=20.9.0" } + os: [freebsd] + + "@img/sharp-libvips-darwin-arm64@1.3.3": + resolution: + { + integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==, + } + cpu: [arm64] + os: [darwin] + + "@img/sharp-libvips-darwin-x64@1.3.3": + resolution: + { + integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==, + } + cpu: [x64] + os: [darwin] + + "@img/sharp-libvips-linux-arm64@1.3.3": + resolution: + { + integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==, + } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@img/sharp-libvips-linux-arm@1.3.3": + resolution: + { + integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==, + } + cpu: [arm] + os: [linux] + libc: [glibc] + + "@img/sharp-libvips-linux-ppc64@1.3.3": + resolution: + { + integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==, + } + cpu: [ppc64] + os: [linux] + libc: [glibc] + + "@img/sharp-libvips-linux-riscv64@1.3.3": + resolution: + { + integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==, + } + cpu: [riscv64] + os: [linux] + libc: [glibc] + + "@img/sharp-libvips-linux-s390x@1.3.3": + resolution: + { + integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==, + } + cpu: [s390x] + os: [linux] + libc: [glibc] + + "@img/sharp-libvips-linux-x64@1.3.3": + resolution: + { + integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==, + } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@img/sharp-libvips-linuxmusl-arm64@1.3.3": + resolution: + { + integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==, + } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@img/sharp-libvips-linuxmusl-x64@1.3.3": + resolution: + { + integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==, + } + cpu: [x64] + os: [linux] + libc: [musl] + + "@img/sharp-linux-arm64@0.35.4": + resolution: + { + integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==, + } + engines: { node: ">=20.9.0" } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@img/sharp-linux-arm@0.35.4": + resolution: + { + integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==, + } + engines: { node: ">=20.9.0" } + cpu: [arm] + os: [linux] + libc: [glibc] + + "@img/sharp-linux-ppc64@0.35.4": + resolution: + { + integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==, + } + engines: { node: ">=20.9.0" } + cpu: [ppc64] + os: [linux] + libc: [glibc] + + "@img/sharp-linux-riscv64@0.35.4": + resolution: + { + integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==, + } + engines: { node: ">=20.9.0" } + cpu: [riscv64] + os: [linux] + libc: [glibc] + + "@img/sharp-linux-s390x@0.35.4": + resolution: + { + integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==, + } + engines: { node: ">=20.9.0" } + cpu: [s390x] + os: [linux] + libc: [glibc] + + "@img/sharp-linux-x64@0.35.4": + resolution: + { + integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==, + } + engines: { node: ">=20.9.0" } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@img/sharp-linuxmusl-arm64@0.35.4": + resolution: + { + integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==, + } + engines: { node: ">=20.9.0" } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@img/sharp-linuxmusl-x64@0.35.4": + resolution: + { + integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==, + } + engines: { node: ">=20.9.0" } + cpu: [x64] + os: [linux] + libc: [musl] + + "@img/sharp-wasm32@0.35.4": + resolution: + { + integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==, + } + engines: { node: ">=20.9.0" } + + "@img/sharp-webcontainers-wasm32@0.35.4": + resolution: + { + integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==, + } + engines: { node: ">=20.9.0" } + cpu: [wasm32] + + "@img/sharp-win32-arm64@0.35.4": + resolution: + { + integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==, + } + engines: { node: ">=20.9.0" } + cpu: [arm64] + os: [win32] + + "@img/sharp-win32-ia32@0.35.4": + resolution: + { + integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==, + } + engines: { node: ^20.9.0 } + cpu: [ia32] + os: [win32] + + "@img/sharp-win32-x64@0.35.4": + resolution: + { + integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==, + } + engines: { node: ">=20.9.0" } + cpu: [x64] + os: [win32] + + "@jridgewell/gen-mapping@0.3.13": + resolution: + { + integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, + } + + "@jridgewell/remapping@2.3.5": + resolution: + { + integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, + } + + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } + + "@jridgewell/sourcemap-codec@1.5.5": + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, + } + + "@jridgewell/trace-mapping@0.3.31": + resolution: + { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, + } + + "@napi-rs/wasm-runtime@1.2.3": + resolution: + { + integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=23.5.0 } + peerDependencies: + "@emnapi/core": ^1.7.1 || ^2.0.0-alpha.4 + "@emnapi/runtime": ^1.7.1 || ^2.0.0-alpha.4 + + "@next/env@16.3.3": + resolution: + { + integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==, + } + + "@next/eslint-plugin-next@16.3.3": + resolution: + { + integrity: sha512-pbEh30vvjKpDoTAmo1v3q2uM4JUi8QaEBpbmjWvGfoec2jLghy/WNtvzAT0bk+Ik9oz6etjt4YjXEk4BQnicCw==, + } + + "@next/swc-darwin-arm64@16.3.3": + resolution: + { + integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [darwin] + + "@next/swc-darwin-x64@16.3.3": + resolution: + { + integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [darwin] + + "@next/swc-linux-arm64-gnu@16.3.3": + resolution: + { + integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@next/swc-linux-arm64-musl@16.3.3": + resolution: + { + integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@next/swc-linux-x64-gnu@16.3.3": + resolution: + { + integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@next/swc-linux-x64-musl@16.3.3": + resolution: + { + integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [linux] + libc: [musl] + + "@next/swc-win32-arm64-msvc@16.3.3": + resolution: + { + integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [win32] + + "@next/swc-win32-x64-msvc@16.3.3": + resolution: + { + integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [win32] + + "@nodelib/fs.scandir@2.1.5": + resolution: + { + integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, + } + engines: { node: ">= 8" } + + "@nodelib/fs.stat@2.0.5": + resolution: + { + integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, + } + engines: { node: ">= 8" } + + "@nodelib/fs.walk@1.2.8": + resolution: + { + integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, + } + engines: { node: ">= 8" } + + "@nolyfill/is-core-module@1.0.39": + resolution: + { + integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==, + } + engines: { node: ">=12.4.0" } + + "@oxc-project/types@0.147.0": + resolution: + { + integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==, + } + + "@radix-ui/primitive@1.1.7": + resolution: + { + integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==, + } + + "@radix-ui/react-arrow@1.1.15": + resolution: + { + integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-collection@1.1.15": + resolution: + { + integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-compose-refs@1.1.5": + resolution: + { + integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-context@1.2.2": + resolution: + { + integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-direction@1.1.4": + resolution: + { + integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-dismissable-layer@1.1.19": + resolution: + { + integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-dropdown-menu@2.1.24": + resolution: + { + integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-focus-guards@1.1.6": + resolution: + { + integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-focus-scope@1.1.16": + resolution: + { + integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-id@1.1.4": + resolution: + { + integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-menu@2.1.24": + resolution: + { + integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-popper@1.3.7": + resolution: + { + integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-portal@1.1.17": + resolution: + { + integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-presence@1.1.10": + resolution: + { + integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-primitive@2.1.10": + resolution: + { + integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-roving-focus@1.1.19": + resolution: + { + integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-slot@1.3.3": + resolution: + { + integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-tooltip@1.2.16": + resolution: + { + integrity: sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-use-callback-ref@1.1.4": + resolution: + { + integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-controllable-state@1.2.6": + resolution: + { + integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-effect-event@0.0.5": + resolution: + { + integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-is-hydrated@0.1.3": + resolution: + { + integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-layout-effect@1.1.4": + resolution: + { + integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-rect@1.1.4": + resolution: + { + integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-size@1.1.4": + resolution: + { + integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-visually-hidden@1.2.11": + resolution: + { + integrity: sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/rect@1.1.3": + resolution: + { + integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==, + } + + "@rolldown/binding-android-arm-eabi@1.2.6": + resolution: + { + integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [android] + + "@rolldown/binding-android-arm64@1.2.6": + resolution: + { + integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [android] + + "@rolldown/binding-darwin-arm64@1.2.6": + resolution: + { + integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [darwin] + + "@rolldown/binding-darwin-x64@1.2.6": + resolution: + { + integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [darwin] + + "@rolldown/binding-freebsd-x64@1.2.6": + resolution: + { + integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [freebsd] + + "@rolldown/binding-linux-arm-gnueabihf@1.2.6": + resolution: + { + integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [linux] + + "@rolldown/binding-linux-arm64-gnu@1.2.6": + resolution: + { + integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@rolldown/binding-linux-arm64-musl@1.2.6": + resolution: + { + integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@rolldown/binding-linux-ppc64-gnu@1.2.6": + resolution: + { + integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [ppc64] + os: [linux] + libc: [glibc] + + "@rolldown/binding-linux-s390x-gnu@1.2.6": + resolution: + { + integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [s390x] + os: [linux] + libc: [glibc] + + "@rolldown/binding-linux-x64-gnu@1.2.6": + resolution: + { + integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@rolldown/binding-linux-x64-musl@1.2.6": + resolution: + { + integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [linux] + libc: [musl] + + "@rolldown/binding-openharmony-arm64@1.2.6": + resolution: + { + integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [openharmony] + + "@rolldown/binding-win32-arm64-msvc@1.2.6": + resolution: + { + integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [win32] + + "@rolldown/binding-win32-x64-msvc@1.2.6": + resolution: + { + integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [win32] + + "@rolldown/pluginutils@1.0.1": + resolution: + { + integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==, + } + + "@rtsao/scc@1.1.0": + resolution: + { + integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==, + } + + "@standard-schema/spec@1.1.0": + resolution: + { + integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, + } + + "@swc/helpers@0.5.23": + resolution: + { + integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==, + } + + "@tailwindcss/node@4.3.3": + resolution: + { + integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==, + } + + "@tailwindcss/oxide-android-arm64@4.3.3": + resolution: + { + integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==, + } + engines: { node: ">= 20" } + cpu: [arm64] + os: [android] + + "@tailwindcss/oxide-darwin-arm64@4.3.3": + resolution: + { + integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==, + } + engines: { node: ">= 20" } + cpu: [arm64] + os: [darwin] + + "@tailwindcss/oxide-darwin-x64@4.3.3": + resolution: + { + integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==, + } + engines: { node: ">= 20" } + cpu: [x64] + os: [darwin] + + "@tailwindcss/oxide-freebsd-x64@4.3.3": + resolution: + { + integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==, + } + engines: { node: ">= 20" } + cpu: [x64] + os: [freebsd] + + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": + resolution: + { + integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==, + } + engines: { node: ">= 20" } + cpu: [arm] + os: [linux] + + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": + resolution: + { + integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==, + } + engines: { node: ">= 20" } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": + resolution: + { + integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==, + } + engines: { node: ">= 20" } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": + resolution: + { + integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==, + } + engines: { node: ">= 20" } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@tailwindcss/oxide-linux-x64-musl@4.3.3": + resolution: + { + integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==, + } + engines: { node: ">= 20" } + cpu: [x64] + os: [linux] + libc: [musl] + + "@tailwindcss/oxide-wasm32-wasi@4.3.3": + resolution: + { + integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==, + } + engines: { node: ">=14.0.0" } + cpu: [wasm32] + bundledDependencies: + - "@napi-rs/wasm-runtime" + - "@emnapi/core" + - "@emnapi/runtime" + - "@tybys/wasm-util" + - "@emnapi/wasi-threads" + - tslib + + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": + resolution: + { + integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==, + } + engines: { node: ">= 20" } + cpu: [arm64] + os: [win32] + + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": + resolution: + { + integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==, + } + engines: { node: ">= 20" } + cpu: [x64] + os: [win32] + + "@tailwindcss/oxide@4.3.3": + resolution: + { + integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==, + } + engines: { node: ">= 20" } + + "@tailwindcss/postcss@4.3.3": + resolution: + { + integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==, + } + + "@testing-library/dom@10.4.1": + resolution: + { + integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==, + } + engines: { node: ">=18" } + + "@testing-library/jest-dom@7.0.1": + resolution: + { + integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==, + } + engines: { node: ">=22", npm: ">=6", yarn: ">=1" } + peerDependencies: + "@testing-library/dom": ">=10 <11" + vitest: ">= 0.32" + peerDependenciesMeta: + vitest: + optional: true + + "@testing-library/react@16.3.2": + resolution: + { + integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==, + } + engines: { node: ">=18" } + peerDependencies: + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@testing-library/user-event@14.6.6": + resolution: + { + integrity: sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==, + } + engines: { node: ">=12", npm: ">=6" } + peerDependencies: + "@testing-library/dom": ">=7.21.4" + + "@tybys/wasm-util@0.10.3": + resolution: + { + integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==, + } + + "@types/aria-query@5.0.4": + resolution: + { + integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, + } + + "@types/chai@5.2.3": + resolution: + { + integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==, + } + + "@types/deep-eql@4.0.2": + resolution: + { + integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==, + } + + "@types/estree@1.0.9": + resolution: + { + integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==, + } + + "@types/json-schema@7.0.15": + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } + + "@types/json5@0.0.29": + resolution: + { + integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==, + } + + "@types/node@26.3.0": + resolution: + { + integrity: sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==, + } + + "@types/react-dom@19.2.5": + resolution: + { + integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==, + } + peerDependencies: + "@types/react": ^19.2.0 + + "@types/react@19.2.18": + resolution: + { + integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==, + } + + "@typescript-eslint/eslint-plugin@8.68.0": + resolution: + { + integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + "@typescript-eslint/parser": ^8.68.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/parser@8.68.0": + resolution: + { + integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/project-service@8.68.0": + resolution: + { + integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/scope-manager@8.68.0": + resolution: + { + integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/tsconfig-utils@8.68.0": + resolution: + { + integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/type-utils@8.68.0": + resolution: + { + integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/types@8.68.0": + resolution: + { + integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/typescript-estree@8.68.0": + resolution: + { + integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/utils@8.68.0": + resolution: + { + integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/visitor-keys@8.68.0": + resolution: + { + integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@unrs/resolver-binding-android-arm-eabi@1.12.2": + resolution: + { + integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==, + } + cpu: [arm] + os: [android] + + "@unrs/resolver-binding-android-arm64@1.12.2": + resolution: + { + integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==, + } + cpu: [arm64] + os: [android] + + "@unrs/resolver-binding-darwin-arm64@1.12.2": + resolution: + { + integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==, + } + cpu: [arm64] + os: [darwin] + + "@unrs/resolver-binding-darwin-x64@1.12.2": + resolution: + { + integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==, + } + cpu: [x64] + os: [darwin] + + "@unrs/resolver-binding-freebsd-x64@1.12.2": + resolution: + { + integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==, + } + cpu: [x64] + os: [freebsd] + + "@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2": + resolution: + { + integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==, + } + cpu: [arm] + os: [linux] + + "@unrs/resolver-binding-linux-arm-musleabihf@1.12.2": + resolution: + { + integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==, + } + cpu: [arm] + os: [linux] + + "@unrs/resolver-binding-linux-arm64-gnu@1.12.2": + resolution: + { + integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==, + } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@unrs/resolver-binding-linux-arm64-musl@1.12.2": + resolution: + { + integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==, + } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@unrs/resolver-binding-linux-loong64-gnu@1.12.2": + resolution: + { + integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==, + } + cpu: [loong64] + os: [linux] + libc: [glibc] + + "@unrs/resolver-binding-linux-loong64-musl@1.12.2": + resolution: + { + integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==, + } + cpu: [loong64] + os: [linux] + libc: [musl] + + "@unrs/resolver-binding-linux-ppc64-gnu@1.12.2": + resolution: + { + integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==, + } + cpu: [ppc64] + os: [linux] + libc: [glibc] + + "@unrs/resolver-binding-linux-riscv64-gnu@1.12.2": + resolution: + { + integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==, + } + cpu: [riscv64] + os: [linux] + libc: [glibc] + + "@unrs/resolver-binding-linux-riscv64-musl@1.12.2": + resolution: + { + integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==, + } + cpu: [riscv64] + os: [linux] + libc: [musl] + + "@unrs/resolver-binding-linux-s390x-gnu@1.12.2": + resolution: + { + integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==, + } + cpu: [s390x] + os: [linux] + libc: [glibc] + + "@unrs/resolver-binding-linux-x64-gnu@1.12.2": + resolution: + { + integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==, + } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@unrs/resolver-binding-linux-x64-musl@1.12.2": + resolution: + { + integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==, + } + cpu: [x64] + os: [linux] + libc: [musl] + + "@unrs/resolver-binding-openharmony-arm64@1.12.2": + resolution: + { + integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==, + } + cpu: [arm64] + os: [openharmony] + + "@unrs/resolver-binding-wasm32-wasi@1.12.2": + resolution: + { + integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==, + } + engines: { node: ">=14.0.0" } + cpu: [wasm32] + + "@unrs/resolver-binding-win32-arm64-msvc@1.12.2": + resolution: + { + integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==, + } + cpu: [arm64] + os: [win32] + + "@unrs/resolver-binding-win32-ia32-msvc@1.12.2": + resolution: + { + integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==, + } + cpu: [ia32] + os: [win32] + + "@unrs/resolver-binding-win32-x64-msvc@1.12.2": + resolution: + { + integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==, + } + cpu: [x64] + os: [win32] + + "@vitejs/plugin-react@6.1.0": + resolution: + { + integrity: sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + peerDependencies: + "@rolldown/plugin-babel": ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.145.0 + vite: ^8.0.0 + peerDependenciesMeta: + "@rolldown/plugin-babel": + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + + "@vitest/expect@4.1.11": + resolution: + { + integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==, + } + + "@vitest/mocker@4.1.11": + resolution: + { + integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==, + } + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + "@vitest/pretty-format@4.1.11": + resolution: + { + integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==, + } + + "@vitest/runner@4.1.11": + resolution: + { + integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==, + } + + "@vitest/snapshot@4.1.11": + resolution: + { + integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==, + } + + "@vitest/spy@4.1.11": + resolution: + { + integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==, + } + + "@vitest/utils@4.1.11": + resolution: + { + integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==, + } + + acorn-jsx@5.3.2: + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: + { + integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==, + } + engines: { node: ">=0.4.0" } + hasBin: true + + ajv@6.15.0: + resolution: + { + integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==, + } + + ansi-regex@5.0.1: + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: ">=8" } + + ansi-styles@4.3.0: + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: ">=8" } + + ansi-styles@5.2.0: + resolution: + { + integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, + } + engines: { node: ">=10" } + + argparse@2.0.1: + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } + + aria-hidden@1.2.6: + resolution: + { + integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==, + } + engines: { node: ">=10" } + + aria-query@5.3.0: + resolution: + { + integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==, + } + + aria-query@5.3.2: + resolution: + { + integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==, + } + engines: { node: ">= 0.4" } + + array-buffer-byte-length@1.0.2: + resolution: + { + integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==, + } + engines: { node: ">= 0.4" } + + array-includes@3.1.9: + resolution: + { + integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==, + } + engines: { node: ">= 0.4" } + + array.prototype.findlast@1.2.5: + resolution: + { + integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==, + } + engines: { node: ">= 0.4" } + + array.prototype.findlastindex@1.2.6: + resolution: + { + integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==, + } + engines: { node: ">= 0.4" } + + array.prototype.flat@1.3.3: + resolution: + { + integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==, + } + engines: { node: ">= 0.4" } + + array.prototype.flatmap@1.3.3: + resolution: + { + integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==, + } + engines: { node: ">= 0.4" } + + array.prototype.tosorted@1.1.4: + resolution: + { + integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==, + } + engines: { node: ">= 0.4" } + + arraybuffer.prototype.slice@1.0.4: + resolution: + { + integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==, + } + engines: { node: ">= 0.4" } + + assertion-error@2.0.1: + resolution: + { + integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, + } + engines: { node: ">=12" } + + ast-types-flow@0.0.8: + resolution: + { + integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==, + } + + async-function@1.0.0: + resolution: + { + integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==, + } + engines: { node: ">= 0.4" } + + available-typed-arrays@1.0.7: + resolution: + { + integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==, + } + engines: { node: ">= 0.4" } + + axe-core@4.13.0: + resolution: + { + integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==, + } + engines: { node: ">=4" } + + axobject-query@4.1.0: + resolution: + { + integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==, + } + engines: { node: ">= 0.4" } + + balanced-match@1.0.2: + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } + + balanced-match@4.0.4: + resolution: + { + integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, + } + engines: { node: 18 || 20 || >=22 } + + baseline-browser-mapping@2.11.19: + resolution: + { + integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==, + } + engines: { node: ">=6.0.0" } + hasBin: true + + bidi-js@1.0.3: + resolution: + { + integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==, + } + + brace-expansion@1.1.18: + resolution: + { + integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==, + } + + brace-expansion@5.0.9: + resolution: + { + integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==, + } + engines: { node: 20 || >=22 } + + braces@3.0.3: + resolution: + { + integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, + } + engines: { node: ">=8" } + + browserslist@4.28.8: + resolution: + { + integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: + { + integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, + } + engines: { node: ">= 0.4" } + + call-bind@1.0.9: + resolution: + { + integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==, + } + engines: { node: ">= 0.4" } + + call-bound@1.0.4: + resolution: + { + integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, + } + engines: { node: ">= 0.4" } + + callsites@3.1.0: + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: ">=6" } + + caniuse-lite@1.0.30001810: + resolution: + { + integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==, + } + + chai@6.2.2: + resolution: + { + integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==, + } + engines: { node: ">=18" } + + chalk@4.1.2: + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: ">=10" } + + class-variance-authority@0.7.1: + resolution: + { + integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==, + } + + client-only@0.0.1: + resolution: + { + integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, + } + + clsx@2.1.1: + resolution: + { + integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==, + } + engines: { node: ">=6" } + + color-convert@2.0.1: + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: ">=7.0.0" } + + color-name@1.1.4: + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } + + concat-map@0.0.1: + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } + + convert-source-map@2.0.0: + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } + + cross-spawn@7.0.6: + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } + + css-tree@3.2.1: + resolution: + { + integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==, + } + engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0 } + + css.escape@1.5.1: + resolution: + { + integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==, + } + + csstype@3.2.3: + resolution: + { + integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, + } + + damerau-levenshtein@1.0.8: + resolution: + { + integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==, + } + + data-urls@7.0.0: + resolution: + { + integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + + data-view-buffer@1.0.2: + resolution: + { + integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==, + } + engines: { node: ">= 0.4" } + + data-view-byte-length@1.0.2: + resolution: + { + integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==, + } + engines: { node: ">= 0.4" } + + data-view-byte-offset@1.0.1: + resolution: + { + integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==, + } + engines: { node: ">= 0.4" } + + debug@3.2.7: + resolution: + { + integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==, + } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: + { + integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==, + } + + deep-is@0.1.4: + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, + } + + define-data-property@1.1.4: + resolution: + { + integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==, + } + engines: { node: ">= 0.4" } + + define-properties@1.2.1: + resolution: + { + integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==, + } + engines: { node: ">= 0.4" } + + dequal@2.0.3: + resolution: + { + integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, + } + engines: { node: ">=6" } + + detect-libc@2.1.2: + resolution: + { + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, + } + engines: { node: ">=8" } + + detect-node-es@1.1.0: + resolution: + { + integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==, + } + + doctrine@2.1.0: + resolution: + { + integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==, + } + engines: { node: ">=0.10.0" } + + dom-accessibility-api@0.5.16: + resolution: + { + integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==, + } + + dom-accessibility-api@0.6.3: + resolution: + { + integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==, + } + + dunder-proto@1.0.1: + resolution: + { + integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, + } + engines: { node: ">= 0.4" } + + electron-to-chromium@1.5.415: + resolution: + { + integrity: sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==, + } + + emoji-regex@9.2.2: + resolution: + { + integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, + } + + enhanced-resolve@5.24.5: + resolution: + { + integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==, + } + engines: { node: ">=10.13.0" } + + entities@8.0.0: + resolution: + { + integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==, + } + engines: { node: ">=20.19.0" } + + es-abstract-get@1.0.0: + resolution: + { + integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==, + } + engines: { node: ">= 0.4" } + + es-abstract@1.24.2: + resolution: + { + integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==, + } + engines: { node: ">= 0.4" } + + es-define-property@1.0.1: + resolution: + { + integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, + } + engines: { node: ">= 0.4" } + + es-errors@1.3.0: + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, + } + engines: { node: ">= 0.4" } + + es-iterator-helpers@1.4.0: + resolution: + { + integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==, + } + engines: { node: ">= 0.4" } + + es-module-lexer@2.3.2: + resolution: + { + integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==, + } + + es-object-atoms@1.1.2: + resolution: + { + integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==, + } + engines: { node: ">= 0.4" } + + es-set-tostringtag@2.1.0: + resolution: + { + integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==, + } + engines: { node: ">= 0.4" } + + es-shim-unscopables@1.1.0: + resolution: + { + integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==, + } + engines: { node: ">= 0.4" } + + es-to-primitive@1.3.4: + resolution: + { + integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==, + } + engines: { node: ">= 0.4" } + + escalade@3.2.0: + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } + + escape-string-regexp@4.0.0: + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: ">=10" } + + eslint-config-next@16.3.3: + resolution: + { + integrity: sha512-teqtsR26tnlfXFHfVLTM/4tzEzU8DMu6GS1sddZzhfGzgd2f2ofbgDUcsk6cssSCzX6Tk6fmWifJcdANSdPJrw==, + } + peerDependencies: + eslint: ">=9.0.0" + typescript: ">=3.3.1" + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.10: + resolution: + { + integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==, + } + + eslint-import-resolver-typescript@3.10.1: + resolution: + { + integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==, + } + engines: { node: ^14.18.0 || >=16.0.0 } + peerDependencies: + eslint: "*" + eslint-plugin-import: "*" + eslint-plugin-import-x: "*" + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.14.0: + resolution: + { + integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==, + } + engines: { node: ">=4" } + peerDependencies: + "@typescript-eslint/parser": "*" + eslint: "*" + eslint-import-resolver-node: "*" + eslint-import-resolver-typescript: "*" + eslint-import-resolver-webpack: "*" + peerDependenciesMeta: + "@typescript-eslint/parser": + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: + { + integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==, + } + engines: { node: ">=4" } + peerDependencies: + "@typescript-eslint/parser": "*" + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + "@typescript-eslint/parser": + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: + { + integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==, + } + engines: { node: ">=4.0" } + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.1.1: + resolution: + { + integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==, + } + engines: { node: ">=18" } + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react@7.37.5: + resolution: + { + integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==, + } + engines: { node: ">=4" } + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: + { + integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@3.4.3: + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + + eslint-visitor-keys@4.2.1: + resolution: + { + integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@5.0.1: + resolution: + { + integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + eslint@9.39.5: + resolution: + { + integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: + { + integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + esquery@1.7.0: + resolution: + { + integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, + } + engines: { node: ">=0.10" } + + esrecurse@4.3.0: + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, + } + engines: { node: ">=4.0" } + + estraverse@5.3.0: + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: ">=4.0" } + + estree-walker@3.0.3: + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, + } + + esutils@2.0.3: + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: ">=0.10.0" } + + expect-type@1.4.0: + resolution: + { + integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==, + } + engines: { node: ">=12.0.0" } + + fast-deep-equal@3.1.3: + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } + + fast-glob@3.3.1: + resolution: + { + integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==, + } + engines: { node: ">=8.6.0" } + + fast-json-stable-stringify@2.1.0: + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, + } + + fast-levenshtein@2.0.6: + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, + } + + fastq@1.20.1: + resolution: + { + integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==, + } + + fdir@6.5.0: + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, + } + engines: { node: ">=12.0.0" } + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: + { + integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, + } + engines: { node: ">=16.0.0" } + + fill-range@7.1.1: + resolution: + { + integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, + } + engines: { node: ">=8" } + + find-up@5.0.0: + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: ">=10" } + + flat-cache@4.0.1: + resolution: + { + integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, + } + engines: { node: ">=16" } + + flatted@3.4.4: + resolution: + { + integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==, + } + + for-each@0.3.5: + resolution: + { + integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==, + } + engines: { node: ">= 0.4" } + + fsevents@2.3.3: + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + function-bind@1.1.2: + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } + + function.prototype.name@1.2.0: + resolution: + { + integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==, + } + engines: { node: ">= 0.4" } + + functions-have-names@1.2.3: + resolution: + { + integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==, + } + + generator-function@2.0.1: + resolution: + { + integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==, + } + engines: { node: ">= 0.4" } + + gensync@1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: ">=6.9.0" } + + get-intrinsic@1.3.0: + resolution: + { + integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, + } + engines: { node: ">= 0.4" } + + get-nonce@1.0.1: + resolution: + { + integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==, + } + engines: { node: ">=6" } + + get-proto@1.0.1: + resolution: + { + integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, + } + engines: { node: ">= 0.4" } + + get-symbol-description@1.1.0: + resolution: + { + integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==, + } + engines: { node: ">= 0.4" } + + get-tsconfig@4.14.3: + resolution: + { + integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==, + } + + glob-parent@5.1.2: + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, + } + engines: { node: ">= 6" } + + glob-parent@6.0.2: + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: ">=10.13.0" } + + globals@14.0.0: + resolution: + { + integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, + } + engines: { node: ">=18" } + + globals@16.4.0: + resolution: + { + integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==, + } + engines: { node: ">=18" } + + globalthis@1.0.4: + resolution: + { + integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==, + } + engines: { node: ">= 0.4" } + + gopd@1.2.0: + resolution: + { + integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, + } + engines: { node: ">= 0.4" } + + graceful-fs@4.2.11: + resolution: + { + integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, + } + + has-bigints@1.1.0: + resolution: + { + integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==, + } + engines: { node: ">= 0.4" } + + has-flag@4.0.0: + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: ">=8" } + + has-property-descriptors@1.0.2: + resolution: + { + integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==, + } + + has-proto@1.2.0: + resolution: + { + integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==, + } + engines: { node: ">= 0.4" } + + has-symbols@1.1.0: + resolution: + { + integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, + } + engines: { node: ">= 0.4" } + + has-tostringtag@1.0.2: + resolution: + { + integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, + } + engines: { node: ">= 0.4" } + + hasown@2.0.4: + resolution: + { + integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==, + } + engines: { node: ">= 0.4" } + + hermes-estree@0.25.1: + resolution: + { + integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==, + } + + hermes-parser@0.25.1: + resolution: + { + integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==, + } + + html-encoding-sniffer@6.0.0: + resolution: + { + integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + + ignore@5.3.2: + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, + } + engines: { node: ">= 4" } + + ignore@7.0.6: + resolution: + { + integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==, + } + engines: { node: ">= 4" } + + import-fresh@3.3.1: + resolution: + { + integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, + } + engines: { node: ">=6" } + + imurmurhash@0.1.4: + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, + } + engines: { node: ">=0.8.19" } + + indent-string@4.0.0: + resolution: + { + integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==, + } + engines: { node: ">=8" } + + internal-slot@1.1.0: + resolution: + { + integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==, + } + engines: { node: ">= 0.4" } + + is-array-buffer@3.0.5: + resolution: + { + integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==, + } + engines: { node: ">= 0.4" } + + is-async-function@2.1.1: + resolution: + { + integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==, + } + engines: { node: ">= 0.4" } + + is-bigint@1.1.0: + resolution: + { + integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==, + } + engines: { node: ">= 0.4" } + + is-boolean-object@1.2.2: + resolution: + { + integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==, + } + engines: { node: ">= 0.4" } + + is-bun-module@2.0.0: + resolution: + { + integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==, + } + + is-callable@1.2.7: + resolution: + { + integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==, + } + engines: { node: ">= 0.4" } + + is-core-module@2.16.2: + resolution: + { + integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==, + } + engines: { node: ">= 0.4" } + + is-data-view@1.0.2: + resolution: + { + integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==, + } + engines: { node: ">= 0.4" } + + is-date-object@1.1.0: + resolution: + { + integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==, + } + engines: { node: ">= 0.4" } + + is-document.all@1.0.0: + resolution: + { + integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==, + } + engines: { node: ">= 0.4" } + + is-extglob@2.1.1: + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: ">=0.10.0" } + + is-finalizationregistry@1.1.1: + resolution: + { + integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==, + } + engines: { node: ">= 0.4" } + + is-generator-function@1.1.2: + resolution: + { + integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==, + } + engines: { node: ">= 0.4" } + + is-glob@4.0.3: + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: ">=0.10.0" } + + is-map@2.0.3: + resolution: + { + integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==, + } + engines: { node: ">= 0.4" } + + is-negative-zero@2.0.3: + resolution: + { + integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==, + } + engines: { node: ">= 0.4" } + + is-number-object@1.1.1: + resolution: + { + integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==, + } + engines: { node: ">= 0.4" } + + is-number@7.0.0: + resolution: + { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, + } + engines: { node: ">=0.12.0" } + + is-potential-custom-element-name@1.0.1: + resolution: + { + integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==, + } + + is-regex@1.2.1: + resolution: + { + integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==, + } + engines: { node: ">= 0.4" } + + is-set@2.0.3: + resolution: + { + integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==, + } + engines: { node: ">= 0.4" } + + is-shared-array-buffer@1.0.4: + resolution: + { + integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==, + } + engines: { node: ">= 0.4" } + + is-string@1.1.1: + resolution: + { + integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==, + } + engines: { node: ">= 0.4" } + + is-symbol@1.1.1: + resolution: + { + integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==, + } + engines: { node: ">= 0.4" } + + is-typed-array@1.1.15: + resolution: + { + integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==, + } + engines: { node: ">= 0.4" } + + is-weakmap@2.0.2: + resolution: + { + integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==, + } + engines: { node: ">= 0.4" } + + is-weakref@1.1.1: + resolution: + { + integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==, + } + engines: { node: ">= 0.4" } + + is-weakset@2.0.4: + resolution: + { + integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==, + } + engines: { node: ">= 0.4" } + + isarray@2.0.5: + resolution: + { + integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==, + } + + isexe@2.0.0: + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } + + iterator.prototype@1.1.5: + resolution: + { + integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==, + } + engines: { node: ">= 0.4" } + + jiti@2.7.0: + resolution: + { + integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==, + } + hasBin: true + + js-tokens@4.0.0: + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } + + js-yaml@4.3.1: + resolution: + { + integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==, + } + hasBin: true + + jsdom@30.0.1: + resolution: + { + integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==, + } + engines: { node: ^22.22.2 || ^24.15.0 || >=26.0.0 } + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: + { + integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, + } + engines: { node: ">=6" } + hasBin: true + + json-buffer@3.0.1: + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } + + json-schema-traverse@0.4.1: + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, + } + + json-stable-stringify-without-jsonify@1.0.1: + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, + } + + json5@1.0.2: + resolution: + { + integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==, + } + hasBin: true + + json5@2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: + { + integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==, + } + engines: { node: ">=4.0" } + + keyv@4.5.4: + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, + } + + language-subtag-registry@0.3.23: + resolution: + { + integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==, + } + + language-tags@1.0.9: + resolution: + { + integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==, + } + engines: { node: ">=0.10" } + + levn@0.4.1: + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, + } + engines: { node: ">= 0.8.0" } + + lightningcss-android-arm64@1.32.0: + resolution: + { + integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: + { + integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: + { + integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: + { + integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: + { + integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: + { + integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: + { + integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: + { + integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: + { + integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: + { + integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: + { + integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: + { + integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: + { + integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: + { + integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: + { + integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: + { + integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: + { + integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: + { + integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: + { + integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: + { + integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==, + } + engines: { node: ">= 12.0.0" } + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: + { + integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: + { + integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==, + } + engines: { node: ">= 12.0.0" } + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: + { + integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, + } + engines: { node: ">= 12.0.0" } + + lightningcss@1.33.0: + resolution: + { + integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==, + } + engines: { node: ">= 12.0.0" } + + locate-path@6.0.0: + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: ">=10" } + + lodash.merge@4.6.2: + resolution: + { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, + } + + loose-envify@1.4.0: + resolution: + { + integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, + } + hasBin: true + + lru-cache@11.5.2: + resolution: + { + integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==, + } + engines: { node: 20 || >=22 } + + lru-cache@5.1.1: + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } + + lucide-react@1.34.0: + resolution: + { + integrity: sha512-vnjGJNI7Htk5+oWW8gXGuaLgwgAb0T6/iZbBrp9JCfRFwdNWZ0YTm3eyxjOLgwN6r8iyAf3UA70zNmBRBNv7yg==, + } + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lz-string@1.5.0: + resolution: + { + integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, + } + hasBin: true + + magic-string@0.30.21: + resolution: + { + integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, + } + + math-intrinsics@1.1.0: + resolution: + { + integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, + } + engines: { node: ">= 0.4" } + + mdn-data@2.27.1: + resolution: + { + integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==, + } + + merge2@1.4.1: + resolution: + { + integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, + } + engines: { node: ">= 8" } + + micromatch@4.0.8: + resolution: + { + integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, + } + engines: { node: ">=8.6" } + + min-indent@1.0.1: + resolution: + { + integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==, + } + engines: { node: ">=4" } + + minimatch@10.2.6: + resolution: + { + integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==, + } + engines: { node: 18 || 20 || >=22 } + + minimatch@3.1.5: + resolution: + { + integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, + } + + minimist@1.2.8: + resolution: + { + integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, + } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } + + nanoid@3.3.18: + resolution: + { + integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + + napi-postinstall@0.3.4: + resolution: + { + integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==, + } + engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } + hasBin: true + + natural-compare@1.4.0: + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } + + next@16.3.3: + resolution: + { + integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==, + } + engines: { node: ">=20.9.0" } + hasBin: true + peerDependencies: + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.51.1 + babel-plugin-react-compiler: "*" + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@playwright/test": + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-exports-info@1.6.2: + resolution: + { + integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==, + } + engines: { node: ">= 0.4" } + + node-releases@2.0.53: + resolution: + { + integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==, + } + engines: { node: ">=18" } + + object-assign@4.1.1: + resolution: + { + integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, + } + engines: { node: ">=0.10.0" } + + object-inspect@1.13.4: + resolution: + { + integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, + } + engines: { node: ">= 0.4" } + + object-keys@1.1.1: + resolution: + { + integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==, + } + engines: { node: ">= 0.4" } + + object.assign@4.1.7: + resolution: + { + integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==, + } + engines: { node: ">= 0.4" } + + object.entries@1.1.9: + resolution: + { + integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==, + } + engines: { node: ">= 0.4" } + + object.fromentries@2.0.8: + resolution: + { + integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==, + } + engines: { node: ">= 0.4" } + + object.groupby@1.0.3: + resolution: + { + integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==, + } + engines: { node: ">= 0.4" } + + object.values@1.2.1: + resolution: + { + integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==, + } + engines: { node: ">= 0.4" } + + obug@2.1.4: + resolution: + { + integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==, + } + engines: { node: ">=12.20.0" } + + optionator@0.9.4: + resolution: + { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + } + engines: { node: ">= 0.8.0" } + + own-keys@1.0.2: + resolution: + { + integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==, + } + engines: { node: ">= 0.4" } + + p-limit@3.1.0: + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: ">=10" } + + p-locate@5.0.0: + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: ">=10" } + + parent-module@1.0.1: + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: ">=6" } + + parse5@8.0.1: + resolution: + { + integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==, + } + + path-exists@4.0.0: + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: ">=8" } + + path-key@3.1.1: + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } + + path-parse@1.0.7: + resolution: + { + integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, + } + + pathe@2.0.3: + resolution: + { + integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, + } + + picocolors@1.1.1: + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } + + picomatch@2.3.2: + resolution: + { + integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==, + } + engines: { node: ">=8.6" } + + picomatch@4.0.7: + resolution: + { + integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==, + } + engines: { node: ">=12" } + + possible-typed-array-names@1.1.0: + resolution: + { + integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==, + } + engines: { node: ">= 0.4" } + + postcss@8.5.23: + resolution: + { + integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==, + } + engines: { node: ^10 || ^12 || >=14 } + + postcss@8.5.26: + resolution: + { + integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==, + } + engines: { node: ^10 || ^12 || >=14 } + + prelude-ls@1.2.1: + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, + } + engines: { node: ">= 0.8.0" } + + prettier-plugin-tailwindcss@0.8.1: + resolution: + { + integrity: sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==, + } + engines: { node: ">=20.19" } + peerDependencies: + "@ianvs/prettier-plugin-sort-imports": "*" + "@prettier/plugin-hermes": "*" + "@prettier/plugin-oxc": "*" + "@prettier/plugin-pug": "*" + "@shopify/prettier-plugin-liquid": "*" + "@trivago/prettier-plugin-sort-imports": "*" + "@zackad/prettier-plugin-twig": "*" + prettier: ^3.0 + prettier-plugin-astro: "*" + prettier-plugin-css-order: "*" + prettier-plugin-jsdoc: "*" + prettier-plugin-marko: "*" + prettier-plugin-multiline-arrays: "*" + prettier-plugin-organize-attributes: "*" + prettier-plugin-organize-imports: "*" + prettier-plugin-sort-imports: "*" + prettier-plugin-svelte: "*" + peerDependenciesMeta: + "@ianvs/prettier-plugin-sort-imports": + optional: true + "@prettier/plugin-hermes": + optional: true + "@prettier/plugin-oxc": + optional: true + "@prettier/plugin-pug": + optional: true + "@shopify/prettier-plugin-liquid": + optional: true + "@trivago/prettier-plugin-sort-imports": + optional: true + "@zackad/prettier-plugin-twig": + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.9.6: + resolution: + { + integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==, + } + engines: { node: ">=14" } + hasBin: true + + pretty-format@27.5.1: + resolution: + { + integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==, + } + engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 } + + prop-types@15.8.1: + resolution: + { + integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, + } + + punycode@2.3.1: + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: ">=6" } + + queue-microtask@1.2.3: + resolution: + { + integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, + } + + react-dom@19.2.8: + resolution: + { + integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==, + } + peerDependencies: + react: ^19.2.8 + + react-is@16.13.1: + resolution: + { + integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, + } + + react-is@17.0.2: + resolution: + { + integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==, + } + + react-remove-scroll-bar@2.3.8: + resolution: + { + integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==, + } + engines: { node: ">=10" } + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + + react-remove-scroll@2.7.2: + resolution: + { + integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==, + } + engines: { node: ">=10" } + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + react-style-singleton@2.2.3: + resolution: + { + integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==, + } + engines: { node: ">=10" } + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + react@19.2.8: + resolution: + { + integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==, + } + engines: { node: ">=0.10.0" } + + redent@3.0.0: + resolution: + { + integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==, + } + engines: { node: ">=8" } + + reflect.getprototypeof@1.0.10: + resolution: + { + integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==, + } + engines: { node: ">= 0.4" } + + regexp.prototype.flags@1.5.4: + resolution: + { + integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==, + } + engines: { node: ">= 0.4" } + + require-from-string@2.0.2: + resolution: + { + integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==, + } + engines: { node: ">=0.10.0" } + + resolve-from@4.0.0: + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: ">=4" } + + resolve-pkg-maps@1.0.0: + resolution: + { + integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, + } + + resolve@2.0.0-next.7: + resolution: + { + integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==, + } + engines: { node: ">= 0.4" } + hasBin: true + + reusify@1.1.0: + resolution: + { + integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, + } + engines: { iojs: ">=1.0.0", node: ">=0.10.0" } + + rolldown@1.2.6: + resolution: + { + integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + + run-parallel@1.2.0: + resolution: + { + integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, + } + + safe-array-concat@1.1.4: + resolution: + { + integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==, + } + engines: { node: ">=0.4" } + + safe-push-apply@1.0.0: + resolution: + { + integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==, + } + engines: { node: ">= 0.4" } + + safe-regex-test@1.1.0: + resolution: + { + integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==, + } + engines: { node: ">= 0.4" } + + saxes@6.0.0: + resolution: + { + integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==, + } + engines: { node: ">=v12.22.7" } + + scheduler@0.27.0: + resolution: + { + integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==, + } + + semver@6.3.1: + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } + hasBin: true + + semver@7.8.5: + resolution: + { + integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==, + } + engines: { node: ">=10" } + hasBin: true + + set-function-length@1.2.2: + resolution: + { + integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==, + } + engines: { node: ">= 0.4" } + + set-function-name@2.0.2: + resolution: + { + integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==, + } + engines: { node: ">= 0.4" } + + set-proto@1.0.0: + resolution: + { + integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==, + } + engines: { node: ">= 0.4" } + + sharp@0.35.4: + resolution: + { + integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==, + } + engines: { node: ">=20.9.0" } + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + + shebang-command@2.0.0: + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } + + shebang-regex@3.0.0: + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } + + side-channel-list@1.0.1: + resolution: + { + integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==, + } + engines: { node: ">= 0.4" } + + side-channel-map@1.0.1: + resolution: + { + integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, + } + engines: { node: ">= 0.4" } + + side-channel-weakmap@1.0.2: + resolution: + { + integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, + } + engines: { node: ">= 0.4" } + + side-channel@1.1.1: + resolution: + { + integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==, + } + engines: { node: ">= 0.4" } + + siginfo@2.0.0: + resolution: + { + integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, + } + + simple-icons@16.28.0: + resolution: + { + integrity: sha512-sQPR5AtK/ijRjou7zw7mlLp08oB6FH7i0lOy5XJ2zp9mJs/yejgiOn7KvQoe2q4YJIx6VmgUSW5AOefebPt5kg==, + } + engines: { node: ">=0.12.18" } + + source-map-js@1.2.1: + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: ">=0.10.0" } + + stable-hash@0.0.5: + resolution: + { + integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==, + } + + stackback@0.0.2: + resolution: + { + integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, + } + + std-env@4.2.0: + resolution: + { + integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==, + } + + stop-iteration-iterator@1.1.0: + resolution: + { + integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==, + } + engines: { node: ">= 0.4" } + + string.prototype.includes@2.0.1: + resolution: + { + integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==, + } + engines: { node: ">= 0.4" } + + string.prototype.matchall@4.0.12: + resolution: + { + integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==, + } + engines: { node: ">= 0.4" } + + string.prototype.repeat@1.0.0: + resolution: + { + integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==, + } + + string.prototype.trim@1.2.11: + resolution: + { + integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==, + } + engines: { node: ">= 0.4" } + + string.prototype.trimend@1.0.10: + resolution: + { + integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==, + } + engines: { node: ">= 0.4" } + + string.prototype.trimstart@1.0.8: + resolution: + { + integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==, + } + engines: { node: ">= 0.4" } + + strip-bom@3.0.0: + resolution: + { + integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, + } + engines: { node: ">=4" } + + strip-indent@3.0.0: + resolution: + { + integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==, + } + engines: { node: ">=8" } + + strip-json-comments@3.1.1: + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, + } + engines: { node: ">=8" } + + styled-jsx@5.1.6: + resolution: + { + integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==, + } + engines: { node: ">= 12.0.0" } + peerDependencies: + "@babel/core": "*" + babel-plugin-macros: "*" + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: ">=8" } + + supports-preserve-symlinks-flag@1.0.0: + resolution: + { + integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, + } + engines: { node: ">= 0.4" } + + symbol-tree@3.2.4: + resolution: + { + integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, + } + + tailwind-merge@3.6.0: + resolution: + { + integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==, + } + + tailwindcss@4.3.3: + resolution: + { + integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==, + } + + tapable@2.3.3: + resolution: + { + integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==, + } + engines: { node: ">=6" } + + tinybench@2.9.0: + resolution: + { + integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==, + } + + tinyexec@1.3.0: + resolution: + { + integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==, + } + engines: { node: ">=18" } + + tinyglobby@0.2.17: + resolution: + { + integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==, + } + engines: { node: ">=12.0.0" } + + tinyrainbow@3.1.1: + resolution: + { + integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==, + } + engines: { node: ">=14.0.0" } + + tldts-core@7.4.11: + resolution: + { + integrity: sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==, + } + + tldts@7.4.11: + resolution: + { + integrity: sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==, + } + hasBin: true + + to-regex-range@5.0.1: + resolution: + { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, + } + engines: { node: ">=8.0" } + + tough-cookie@6.0.2: + resolution: + { + integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==, + } + engines: { node: ">=16" } + + tr46@6.0.0: + resolution: + { + integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==, + } + engines: { node: ">=20" } + + ts-api-utils@2.5.0: + resolution: + { + integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==, + } + engines: { node: ">=18.12" } + peerDependencies: + typescript: ">=4.8.4" + + tsconfig-paths@3.15.0: + resolution: + { + integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==, + } + + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + + type-check@0.4.0: + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, + } + engines: { node: ">= 0.8.0" } + + typed-array-buffer@1.0.3: + resolution: + { + integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==, + } + engines: { node: ">= 0.4" } + + typed-array-byte-length@1.0.3: + resolution: + { + integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==, + } + engines: { node: ">= 0.4" } + + typed-array-byte-offset@1.0.4: + resolution: + { + integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==, + } + engines: { node: ">= 0.4" } + + typed-array-length@1.0.8: + resolution: + { + integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==, + } + engines: { node: ">= 0.4" } + + typescript-eslint@8.68.0: + resolution: + { + integrity: sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + typescript@5.9.3: + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } + hasBin: true + + unbox-primitive@1.1.0: + resolution: + { + integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==, + } + engines: { node: ">= 0.4" } + + undici-types@8.3.0: + resolution: + { + integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==, + } + + undici@8.10.0: + resolution: + { + integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==, + } + engines: { node: ">=22.19.0" } + + unrs-resolver@1.12.2: + resolution: + { + integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==, + } + + update-browserslist-db@1.3.1: + resolution: + { + integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==, + } + hasBin: true + peerDependencies: + browserslist: ">= 4.21.0" + + uri-js@4.4.1: + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, + } + + use-callback-ref@1.3.3: + resolution: + { + integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==, + } + engines: { node: ">=10" } + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + use-sidecar@1.1.3: + resolution: + { + integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==, + } + engines: { node: ">=10" } + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + vite@8.2.2: + resolution: + { + integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: ">=1.21.0" + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + "@types/node": + optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.11: + resolution: + { + integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==, + } + engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } + hasBin: true + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.11 + "@vitest/browser-preview": 4.1.11 + "@vitest/browser-webdriverio": 4.1.11 + "@vitest/coverage-istanbul": 4.1.11 + "@vitest/coverage-v8": 4.1.11 + "@vitest/ui": 4.1.11 + happy-dom: "*" + jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: + { + integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==, + } + engines: { node: ">=18" } + + webidl-conversions@8.0.1: + resolution: + { + integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==, + } + engines: { node: ">=20" } + + whatwg-mimetype@5.0.0: + resolution: + { + integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==, + } + engines: { node: ">=20" } + + whatwg-url@16.0.1: + resolution: + { + integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==, + } + engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + + whatwg-url@17.1.0: + resolution: + { + integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==, + } + engines: { node: ^22.14.0 || >=24.0.0 } + + which-boxed-primitive@1.1.1: + resolution: + { + integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==, + } + engines: { node: ">= 0.4" } + + which-builtin-type@1.2.1: + resolution: + { + integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==, + } + engines: { node: ">= 0.4" } + + which-collection@1.0.2: + resolution: + { + integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==, + } + engines: { node: ">= 0.4" } + + which-typed-array@1.1.22: + resolution: + { + integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==, + } + engines: { node: ">= 0.4" } + + which@2.0.2: + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } + hasBin: true + + why-is-node-running@2.3.0: + resolution: + { + integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==, + } + engines: { node: ">=8" } + hasBin: true + + word-wrap@1.2.5: + resolution: + { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, + } + engines: { node: ">=0.10.0" } + + xml-name-validator@5.0.0: + resolution: + { + integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==, + } + engines: { node: ">=18" } + + xmlchars@2.2.0: + resolution: + { + integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==, + } + + yallist@3.1.1: + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } + + yocto-queue@0.1.0: + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: ">=10" } + + zod-validation-error@4.0.2: + resolution: + { + integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==, + } + engines: { node: ">=18.0.0" } + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: + { + integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==, + } + +snapshots: + "@adobe/css-tools@4.5.0": {} + + "@alloc/quick-lru@5.2.0": {} + + "@asamuzakjp/css-color@6.0.7": + dependencies: + "@csstools/css-calc": 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + "@csstools/css-color-parser": 4.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) + "@csstools/css-tokenizer": 4.0.0 + lru-cache: 11.5.2 + + "@asamuzakjp/dom-selector@8.3.2": + dependencies: + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + + "@babel/code-frame@7.29.7": + dependencies: + "@babel/helper-validator-identifier": 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + "@babel/compat-data@7.29.7": {} + + "@babel/core@7.29.7(supports-color@7.2.0)": + dependencies: + "@babel/code-frame": 7.29.7 + "@babel/generator": 7.29.8 + "@babel/helper-compilation-targets": 7.29.7 + "@babel/helper-module-transforms": 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + "@babel/helpers": 7.29.7 + "@babel/parser": 7.29.8 + "@babel/template": 7.29.7 + "@babel/traverse": 7.29.8(supports-color@7.2.0) + "@babel/types": 7.29.8 + "@jridgewell/remapping": 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@7.2.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + "@babel/generator@7.29.8": + dependencies: + "@babel/parser": 7.29.8 + "@babel/types": 7.29.8 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 + jsesc: 3.1.0 + + "@babel/helper-compilation-targets@7.29.7": + dependencies: + "@babel/compat-data": 7.29.7 + "@babel/helper-validator-option": 7.29.7 + browserslist: 4.28.8 + lru-cache: 5.1.1 + semver: 6.3.1 + + "@babel/helper-globals@7.29.7": {} + + "@babel/helper-module-imports@7.29.7(supports-color@7.2.0)": + dependencies: + "@babel/traverse": 7.29.8(supports-color@7.2.0) + "@babel/types": 7.29.8 + transitivePeerDependencies: + - supports-color + + "@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)": + dependencies: + "@babel/core": 7.29.7(supports-color@7.2.0) + "@babel/helper-module-imports": 7.29.7(supports-color@7.2.0) + "@babel/helper-validator-identifier": 7.29.7 + "@babel/traverse": 7.29.8(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + "@babel/helper-string-parser@7.29.7": {} + + "@babel/helper-validator-identifier@7.29.7": {} + + "@babel/helper-validator-option@7.29.7": {} + + "@babel/helpers@7.29.7": + dependencies: + "@babel/template": 7.29.7 + "@babel/types": 7.29.8 + + "@babel/parser@7.29.8": + dependencies: + "@babel/types": 7.29.8 + + "@babel/runtime@7.29.7": {} + + "@babel/template@7.29.7": + dependencies: + "@babel/code-frame": 7.29.7 + "@babel/parser": 7.29.8 + "@babel/types": 7.29.8 + + "@babel/traverse@7.29.8(supports-color@7.2.0)": + dependencies: + "@babel/code-frame": 7.29.7 + "@babel/generator": 7.29.8 + "@babel/helper-globals": 7.29.7 + "@babel/parser": 7.29.8 + "@babel/template": 7.29.7 + "@babel/types": 7.29.8 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + "@babel/types@7.29.8": + dependencies: + "@babel/helper-string-parser": 7.29.7 + "@babel/helper-validator-identifier": 7.29.7 + + "@bramus/specificity@2.4.2": + dependencies: + css-tree: 3.2.1 + + "@csstools/color-helpers@6.1.1": {} + + "@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": + dependencies: + "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) + "@csstools/css-tokenizer": 4.0.0 + + "@csstools/css-color-parser@4.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": + dependencies: + "@csstools/color-helpers": 6.1.1 + "@csstools/css-calc": 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) + "@csstools/css-tokenizer": 4.0.0 + + "@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)": + dependencies: + "@csstools/css-tokenizer": 4.0.0 + + "@csstools/css-syntax-patches-for-csstree@1.1.9(css-tree@3.2.1)": + optionalDependencies: + css-tree: 3.2.1 + + "@csstools/css-tokenizer@4.0.0": {} + + "@emnapi/core@1.10.0": + dependencies: + "@emnapi/wasi-threads": 1.2.1 + tslib: 2.8.1 + optional: true + + "@emnapi/runtime@1.10.0": + dependencies: + tslib: 2.8.1 + optional: true + + "@emnapi/runtime@1.11.3": + dependencies: + tslib: 2.8.1 + optional: true + + "@emnapi/wasi-threads@1.2.1": + dependencies: + tslib: 2.8.1 + optional: true + + "@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))": + dependencies: + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + "@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))": + dependencies: + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + "@eslint-community/regexpp@4.12.2": {} + + "@eslint/config-array@0.21.2(supports-color@7.2.0)": + dependencies: + "@eslint/object-schema": 2.1.7 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + "@eslint/config-helpers@0.4.2": + dependencies: + "@eslint/core": 0.17.0 + + "@eslint/core@0.17.0": + dependencies: + "@types/json-schema": 7.0.15 + + "@eslint/eslintrc@3.3.6(supports-color@7.2.0)": + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@7.2.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + "@eslint/js@9.39.5": {} + + "@eslint/object-schema@2.1.7": {} + + "@eslint/plugin-kit@0.4.1": + dependencies: + "@eslint/core": 0.17.0 + levn: 0.4.1 + + "@exodus/bytes@1.15.1": {} + + "@floating-ui/core@1.8.0": + dependencies: + "@floating-ui/utils": 0.2.12 + + "@floating-ui/dom@1.8.0": + dependencies: + "@floating-ui/core": 1.8.0 + "@floating-ui/utils": 0.2.12 + + "@floating-ui/react-dom@2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@floating-ui/dom": 1.8.0 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + "@floating-ui/utils@0.2.12": {} + + "@humanfs/core@0.19.2": + dependencies: + "@humanfs/types": 0.15.0 + + "@humanfs/node@0.16.8": + dependencies: + "@humanfs/core": 0.19.2 + "@humanfs/types": 0.15.0 + "@humanwhocodes/retry": 0.4.3 + + "@humanfs/types@0.15.0": {} + + "@humanwhocodes/module-importer@1.0.1": {} + + "@humanwhocodes/retry@0.4.3": {} + + "@img/colour@1.1.0": + optional: true + + "@img/sharp-darwin-arm64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-darwin-arm64": 1.3.3 + optional: true + + "@img/sharp-darwin-x64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-darwin-x64": 1.3.3 + optional: true + + "@img/sharp-freebsd-wasm32@0.35.4": + dependencies: + "@img/sharp-wasm32": 0.35.4 + optional: true + + "@img/sharp-libvips-darwin-arm64@1.3.3": + optional: true + + "@img/sharp-libvips-darwin-x64@1.3.3": + optional: true + + "@img/sharp-libvips-linux-arm64@1.3.3": + optional: true + + "@img/sharp-libvips-linux-arm@1.3.3": + optional: true + + "@img/sharp-libvips-linux-ppc64@1.3.3": + optional: true + + "@img/sharp-libvips-linux-riscv64@1.3.3": + optional: true + + "@img/sharp-libvips-linux-s390x@1.3.3": + optional: true + + "@img/sharp-libvips-linux-x64@1.3.3": + optional: true + + "@img/sharp-libvips-linuxmusl-arm64@1.3.3": + optional: true + + "@img/sharp-libvips-linuxmusl-x64@1.3.3": + optional: true + + "@img/sharp-linux-arm64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-arm64": 1.3.3 + optional: true + + "@img/sharp-linux-arm@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-arm": 1.3.3 + optional: true + + "@img/sharp-linux-ppc64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-ppc64": 1.3.3 + optional: true + + "@img/sharp-linux-riscv64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-riscv64": 1.3.3 + optional: true + + "@img/sharp-linux-s390x@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-s390x": 1.3.3 + optional: true + + "@img/sharp-linux-x64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linux-x64": 1.3.3 + optional: true + + "@img/sharp-linuxmusl-arm64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64": 1.3.3 + optional: true + + "@img/sharp-linuxmusl-x64@0.35.4": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64": 1.3.3 + optional: true + + "@img/sharp-wasm32@0.35.4": + dependencies: + "@emnapi/runtime": 1.11.3 + optional: true + + "@img/sharp-webcontainers-wasm32@0.35.4": + dependencies: + "@img/sharp-wasm32": 0.35.4 + optional: true + + "@img/sharp-win32-arm64@0.35.4": + optional: true + + "@img/sharp-win32-ia32@0.35.4": + optional: true + + "@img/sharp-win32-x64@0.35.4": + optional: true + + "@jridgewell/gen-mapping@0.3.13": + dependencies: + "@jridgewell/sourcemap-codec": 1.5.5 + "@jridgewell/trace-mapping": 0.3.31 + + "@jridgewell/remapping@2.3.5": + dependencies: + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 + + "@jridgewell/resolve-uri@3.1.2": {} + + "@jridgewell/sourcemap-codec@1.5.5": {} + + "@jridgewell/trace-mapping@0.3.31": + dependencies: + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.5.5 + + "@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)": + dependencies: + "@emnapi/core": 1.10.0 + "@emnapi/runtime": 1.10.0 + "@tybys/wasm-util": 0.10.3 + optional: true + + "@next/env@16.3.3": {} + + "@next/eslint-plugin-next@16.3.3(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))": + dependencies: + "@eslint-community/eslint-utils": 4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + fast-glob: 3.3.1 + transitivePeerDependencies: + - eslint + + "@next/swc-darwin-arm64@16.3.3": + optional: true + + "@next/swc-darwin-x64@16.3.3": + optional: true + + "@next/swc-linux-arm64-gnu@16.3.3": + optional: true + + "@next/swc-linux-arm64-musl@16.3.3": + optional: true + + "@next/swc-linux-x64-gnu@16.3.3": + optional: true + + "@next/swc-linux-x64-musl@16.3.3": + optional: true + + "@next/swc-win32-arm64-msvc@16.3.3": + optional: true + + "@next/swc-win32-x64-msvc@16.3.3": + optional: true + + "@nodelib/fs.scandir@2.1.5": + dependencies: + "@nodelib/fs.stat": 2.0.5 + run-parallel: 1.2.0 + + "@nodelib/fs.stat@2.0.5": {} + + "@nodelib/fs.walk@1.2.8": + dependencies: + "@nodelib/fs.scandir": 2.1.5 + fastq: 1.20.1 + + "@nolyfill/is-core-module@1.0.39": {} + + "@oxc-project/types@0.147.0": {} + + "@radix-ui/primitive@1.1.7": {} + + "@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-context": 1.2.2(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-slot": 1.3.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.18)(react@19.2.8)": + dependencies: + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-context@1.2.2(@types/react@19.2.18)(react@19.2.8)": + dependencies: + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-direction@1.1.4(@types/react@19.2.18)(react@19.2.8)": + dependencies: + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/primitive": 1.1.7 + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-use-callback-ref": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-effect-event": 0.0.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/primitive": 1.1.7 + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-context": 1.2.2(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-id": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-menu": 2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-use-controllable-state": 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.18)(react@19.2.8)": + dependencies: + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-use-callback-ref": 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-id@1.1.4(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/primitive": 1.1.7 + "@radix-ui/react-collection": 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-context": 1.2.2(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-direction": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-dismissable-layer": 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-focus-guards": 1.1.6(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-focus-scope": 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-id": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-popper": 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-portal": 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-presence": 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-roving-focus": 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-slot": 1.3.3(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-callback-ref": 1.1.4(@types/react@19.2.18)(react@19.2.8) + aria-hidden: 1.2.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@floating-ui/react-dom": 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-arrow": 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-context": 1.2.2(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-use-callback-ref": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-rect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-size": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/rect": 1.1.3 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/react-slot": 1.3.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/primitive": 1.1.7 + "@radix-ui/react-collection": 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-context": 1.2.2(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-direction": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-id": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-use-callback-ref": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-controllable-state": 1.2.6(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-is-hydrated": 0.1.3(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-slot@1.3.3(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-tooltip@1.2.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/primitive": 1.1.7 + "@radix-ui/react-compose-refs": 1.1.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-context": 1.2.2(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-dismissable-layer": 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-id": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-popper": 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-portal": 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-presence": 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + "@radix-ui/react-slot": 1.3.3(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-controllable-state": 1.2.6(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-visually-hidden": 1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.18)(react@19.2.8)": + dependencies: + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@radix-ui/primitive": 1.1.7 + "@radix-ui/react-use-effect-event": 0.0.5(@types/react@19.2.18)(react@19.2.8) + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.18)(react@19.2.8)": + dependencies: + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.18)(react@19.2.8)": + dependencies: + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-use-rect@1.1.4(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@radix-ui/rect": 1.1.3 + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-use-size@1.1.4(@types/react@19.2.18)(react@19.2.8)": + dependencies: + "@radix-ui/react-use-layout-effect": 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + "@types/react": 19.2.18 + + "@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@radix-ui/react-primitive": 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@radix-ui/rect@1.1.3": {} + + "@rolldown/binding-android-arm-eabi@1.2.6": + optional: true + + "@rolldown/binding-android-arm64@1.2.6": + optional: true + + "@rolldown/binding-darwin-arm64@1.2.6": + optional: true + + "@rolldown/binding-darwin-x64@1.2.6": + optional: true + + "@rolldown/binding-freebsd-x64@1.2.6": + optional: true + + "@rolldown/binding-linux-arm-gnueabihf@1.2.6": + optional: true + + "@rolldown/binding-linux-arm64-gnu@1.2.6": + optional: true + + "@rolldown/binding-linux-arm64-musl@1.2.6": + optional: true + + "@rolldown/binding-linux-ppc64-gnu@1.2.6": + optional: true + + "@rolldown/binding-linux-s390x-gnu@1.2.6": + optional: true + + "@rolldown/binding-linux-x64-gnu@1.2.6": + optional: true + + "@rolldown/binding-linux-x64-musl@1.2.6": + optional: true + + "@rolldown/binding-openharmony-arm64@1.2.6": + optional: true + + "@rolldown/binding-win32-arm64-msvc@1.2.6": + optional: true + + "@rolldown/binding-win32-x64-msvc@1.2.6": + optional: true + + "@rolldown/pluginutils@1.0.1": {} + + "@rtsao/scc@1.1.0": {} + + "@standard-schema/spec@1.1.0": {} + + "@swc/helpers@0.5.23": + dependencies: + tslib: 2.8.1 + + "@tailwindcss/node@4.3.3": + dependencies: + "@jridgewell/remapping": 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + "@tailwindcss/oxide-android-arm64@4.3.3": + optional: true + + "@tailwindcss/oxide-darwin-arm64@4.3.3": + optional: true + + "@tailwindcss/oxide-darwin-x64@4.3.3": + optional: true + + "@tailwindcss/oxide-freebsd-x64@4.3.3": + optional: true + + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": + optional: true + + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": + optional: true + + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": + optional: true + + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": + optional: true + + "@tailwindcss/oxide-linux-x64-musl@4.3.3": + optional: true + + "@tailwindcss/oxide-wasm32-wasi@4.3.3": + optional: true + + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": + optional: true + + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": + optional: true + + "@tailwindcss/oxide@4.3.3": + optionalDependencies: + "@tailwindcss/oxide-android-arm64": 4.3.3 + "@tailwindcss/oxide-darwin-arm64": 4.3.3 + "@tailwindcss/oxide-darwin-x64": 4.3.3 + "@tailwindcss/oxide-freebsd-x64": 4.3.3 + "@tailwindcss/oxide-linux-arm-gnueabihf": 4.3.3 + "@tailwindcss/oxide-linux-arm64-gnu": 4.3.3 + "@tailwindcss/oxide-linux-arm64-musl": 4.3.3 + "@tailwindcss/oxide-linux-x64-gnu": 4.3.3 + "@tailwindcss/oxide-linux-x64-musl": 4.3.3 + "@tailwindcss/oxide-wasm32-wasi": 4.3.3 + "@tailwindcss/oxide-win32-arm64-msvc": 4.3.3 + "@tailwindcss/oxide-win32-x64-msvc": 4.3.3 + + "@tailwindcss/postcss@4.3.3": + dependencies: + "@alloc/quick-lru": 5.2.0 + "@tailwindcss/node": 4.3.3 + "@tailwindcss/oxide": 4.3.3 + postcss: 8.5.23 + tailwindcss: 4.3.3 + + "@testing-library/dom@10.4.1": + dependencies: + "@babel/code-frame": 7.29.7 + "@babel/runtime": 7.29.7 + "@types/aria-query": 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + "@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11(@types/node@26.3.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0)))": + dependencies: + "@adobe/css-tools": 4.5.0 + "@testing-library/dom": 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + optionalDependencies: + vitest: 4.1.11(@types/node@26.3.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0)) + + "@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)": + dependencies: + "@babel/runtime": 7.29.7 + "@testing-library/dom": 10.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + "@types/react-dom": 19.2.5(@types/react@19.2.18) + + "@testing-library/user-event@14.6.6(@testing-library/dom@10.4.1)": + dependencies: + "@testing-library/dom": 10.4.1 + + "@tybys/wasm-util@0.10.3": + dependencies: + tslib: 2.8.1 + optional: true + + "@types/aria-query@5.0.4": {} + + "@types/chai@5.2.3": + dependencies: + "@types/deep-eql": 4.0.2 + assertion-error: 2.0.1 + + "@types/deep-eql@4.0.2": {} + + "@types/estree@1.0.9": {} + + "@types/json-schema@7.0.15": {} + + "@types/json5@0.0.29": {} + + "@types/node@26.3.0": + dependencies: + undici-types: 8.3.0 + + "@types/react-dom@19.2.5(@types/react@19.2.18)": + dependencies: + "@types/react": 19.2.18 + + "@types/react@19.2.18": + dependencies: + csstype: 3.2.3 + + "@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@eslint-community/regexpp": 4.12.2 + "@typescript-eslint/parser": 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/scope-manager": 8.68.0 + "@typescript-eslint/type-utils": 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/utils": 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.68.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/scope-manager": 8.68.0 + "@typescript-eslint/types": 8.68.0 + "@typescript-eslint/typescript-estree": 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.68.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/project-service@8.68.0(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/tsconfig-utils": 8.68.0(typescript@5.9.3) + "@typescript-eslint/types": 8.68.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/scope-manager@8.68.0": + dependencies: + "@typescript-eslint/types": 8.68.0 + "@typescript-eslint/visitor-keys": 8.68.0 + + "@typescript-eslint/tsconfig-utils@8.68.0(typescript@5.9.3)": + dependencies: + typescript: 5.9.3 + + "@typescript-eslint/type-utils@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/types": 8.68.0 + "@typescript-eslint/typescript-estree": 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/utils": 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/types@8.68.0": {} + + "@typescript-eslint/typescript-estree@8.68.0(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/project-service": 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/tsconfig-utils": 8.68.0(typescript@5.9.3) + "@typescript-eslint/types": 8.68.0 + "@typescript-eslint/visitor-keys": 8.68.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/utils@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@eslint-community/eslint-utils": 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + "@typescript-eslint/scope-manager": 8.68.0 + "@typescript-eslint/types": 8.68.0 + "@typescript-eslint/typescript-estree": 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/visitor-keys@8.68.0": + dependencies: + "@typescript-eslint/types": 8.68.0 + eslint-visitor-keys: 5.0.1 + + "@unrs/resolver-binding-android-arm-eabi@1.12.2": + optional: true + + "@unrs/resolver-binding-android-arm64@1.12.2": + optional: true + + "@unrs/resolver-binding-darwin-arm64@1.12.2": + optional: true + + "@unrs/resolver-binding-darwin-x64@1.12.2": + optional: true + + "@unrs/resolver-binding-freebsd-x64@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-arm-musleabihf@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-arm64-gnu@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-arm64-musl@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-loong64-gnu@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-loong64-musl@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-ppc64-gnu@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-riscv64-gnu@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-riscv64-musl@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-s390x-gnu@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-x64-gnu@1.12.2": + optional: true + + "@unrs/resolver-binding-linux-x64-musl@1.12.2": + optional: true + + "@unrs/resolver-binding-openharmony-arm64@1.12.2": + optional: true + + "@unrs/resolver-binding-wasm32-wasi@1.12.2": + dependencies: + "@emnapi/core": 1.10.0 + "@emnapi/runtime": 1.10.0 + "@napi-rs/wasm-runtime": 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + "@unrs/resolver-binding-win32-arm64-msvc@1.12.2": + optional: true + + "@unrs/resolver-binding-win32-ia32-msvc@1.12.2": + optional: true + + "@unrs/resolver-binding-win32-x64-msvc@1.12.2": + optional: true + + "@vitejs/plugin-react@6.1.0(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0))": + dependencies: + "@rolldown/pluginutils": 1.0.1 + vite: 8.2.2(@types/node@26.3.0)(jiti@2.7.0) + + "@vitest/expect@4.1.11": + dependencies: + "@standard-schema/spec": 1.1.0 + "@types/chai": 5.2.3 + "@vitest/spy": 4.1.11 + "@vitest/utils": 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + "@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0))": + dependencies: + "@vitest/spy": 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2(@types/node@26.3.0)(jiti@2.7.0) + + "@vitest/pretty-format@4.1.11": + dependencies: + tinyrainbow: 3.1.1 + + "@vitest/runner@4.1.11": + dependencies: + "@vitest/utils": 4.1.11 + pathe: 2.0.3 + + "@vitest/snapshot@4.1.11": + dependencies: + "@vitest/pretty-format": 4.1.11 + "@vitest/utils": 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + "@vitest/spy@4.1.11": {} + + "@vitest/utils@4.1.11": + dependencies: + "@vitest/pretty-format": 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.13.0: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.19: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.19 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.415 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001810: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - "@noble/hashes" + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.415: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@8.0.0: {} + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.2 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.4.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@2.3.2: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@16.3.3(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + "@next/eslint-plugin-next": 16.3.3(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + globals: 16.4.0 + typescript-eslint: 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - "@typescript-eslint/parser" + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.10(supports-color@7.2.0): + dependencies: + debug: 3.2.7(supports-color@7.2.0) + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + "@nolyfill/is-core-module": 1.0.39 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + get-tsconfig: 4.14.3 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + debug: 3.2.7(supports-color@7.2.0) + optionalDependencies: + "@typescript-eslint/parser": 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + "@rtsao/scc": 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7(supports-color@7.2.0) + doctrine: 2.1.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + hasown: 2.0.4 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + "@typescript-eslint/parser": 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.13.0 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + "@babel/core": 7.29.7(supports-color@7.2.0) + "@babel/parser": 7.29.8 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.4.0 + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0): + dependencies: + "@eslint-community/eslint-utils": 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + "@eslint-community/regexpp": 4.12.2 + "@eslint/config-array": 0.21.2(supports-color@7.2.0) + "@eslint/config-helpers": 0.4.2 + "@eslint/core": 0.17.0 + "@eslint/eslintrc": 3.3.6(supports-color@7.2.0) + "@eslint/js": 9.39.5 + "@eslint/plugin-kit": 0.4.1 + "@humanfs/node": 0.16.8 + "@humanwhocodes/module-importer": 1.0.1 + "@humanwhocodes/retry": 0.4.3 + "@types/estree": 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + "@types/estree": 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + "@nodelib/fs.stat": 2.0.5 + "@nodelib/fs.walk": 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + html-encoding-sniffer@6.0.0: + dependencies: + "@exodus/bytes": 1.15.1 + transitivePeerDependencies: + - "@noble/hashes" + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + jsdom@30.0.1: + dependencies: + "@asamuzakjp/css-color": 6.0.7 + "@asamuzakjp/dom-selector": 8.3.2 + "@bramus/specificity": 2.4.2 + "@csstools/css-syntax-patches-for-csstree": 1.1.9(css-tree@3.2.1) + "@exodus/bytes": 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 8.10.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - "@noble/hashes" + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.34.0(react@19.2.8): + dependencies: + react: 19.2.8 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + "@jridgewell/sourcemap-codec": 1.5.5 + + math-intrinsics@1.1.0: {} + + mdn-data@2.27.1: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + min-indent@1.0.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next@16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.3.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + "@next/env": 16.3.3 + "@swc/helpers": 0.5.23 + baseline-browser-mapping: 2.11.19 + caniuse-lite: 1.0.30001810 + postcss: 8.5.23 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.8) + optionalDependencies: + "@next/swc-darwin-arm64": 16.3.3 + "@next/swc-darwin-x64": 16.3.3 + "@next/swc-linux-arm64-gnu": 16.3.3 + "@next/swc-linux-arm64-musl": 16.3.3 + "@next/swc-linux-x64-gnu": 16.3.3 + "@next/swc-linux-x64-musl": 16.3.3 + "@next/swc-win32-arm64-msvc": 16.3.3 + "@next/swc-win32-x64-msvc": 16.3.3 + sharp: 0.35.4(@types/node@26.3.0) + transitivePeerDependencies: + - "@babel/core" + - "@types/node" + - babel-plugin-macros + + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.53: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + obug@2.1.4: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-tailwindcss@0.8.1(prettier@3.9.6): + dependencies: + prettier: 3.9.6 + + prettier@3.9.6: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) + tslib: 2.8.1 + optionalDependencies: + "@types/react": 19.2.18 + + react-remove-scroll@2.7.2(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.18)(react@19.2.8) + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.18)(react@19.2.8) + use-sidecar: 1.1.3(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + "@types/react": 19.2.18 + + react-style-singleton@2.2.3(@types/react@19.2.18)(react@19.2.8): + dependencies: + get-nonce: 1.0.1 + react: 19.2.8 + tslib: 2.8.1 + optionalDependencies: + "@types/react": 19.2.18 + + react@19.2.8: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rolldown@1.2.6: + dependencies: + "@oxc-project/types": 0.147.0 + "@rolldown/pluginutils": 1.0.1 + optionalDependencies: + "@rolldown/binding-android-arm-eabi": 1.2.6 + "@rolldown/binding-android-arm64": 1.2.6 + "@rolldown/binding-darwin-arm64": 1.2.6 + "@rolldown/binding-darwin-x64": 1.2.6 + "@rolldown/binding-freebsd-x64": 1.2.6 + "@rolldown/binding-linux-arm-gnueabihf": 1.2.6 + "@rolldown/binding-linux-arm64-gnu": 1.2.6 + "@rolldown/binding-linux-arm64-musl": 1.2.6 + "@rolldown/binding-linux-ppc64-gnu": 1.2.6 + "@rolldown/binding-linux-s390x-gnu": 1.2.6 + "@rolldown/binding-linux-x64-gnu": 1.2.6 + "@rolldown/binding-linux-x64-musl": 1.2.6 + "@rolldown/binding-openharmony-arm64": 1.2.6 + "@rolldown/binding-win32-arm64-msvc": 1.2.6 + "@rolldown/binding-win32-x64-msvc": 1.2.6 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + sharp@0.35.4(@types/node@26.3.0): + dependencies: + "@img/colour": 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + "@img/sharp-darwin-arm64": 0.35.4 + "@img/sharp-darwin-x64": 0.35.4 + "@img/sharp-freebsd-wasm32": 0.35.4 + "@img/sharp-libvips-darwin-arm64": 1.3.3 + "@img/sharp-libvips-darwin-x64": 1.3.3 + "@img/sharp-libvips-linux-arm": 1.3.3 + "@img/sharp-libvips-linux-arm64": 1.3.3 + "@img/sharp-libvips-linux-ppc64": 1.3.3 + "@img/sharp-libvips-linux-riscv64": 1.3.3 + "@img/sharp-libvips-linux-s390x": 1.3.3 + "@img/sharp-libvips-linux-x64": 1.3.3 + "@img/sharp-libvips-linuxmusl-arm64": 1.3.3 + "@img/sharp-libvips-linuxmusl-x64": 1.3.3 + "@img/sharp-linux-arm": 0.35.4 + "@img/sharp-linux-arm64": 0.35.4 + "@img/sharp-linux-ppc64": 0.35.4 + "@img/sharp-linux-riscv64": 0.35.4 + "@img/sharp-linux-s390x": 0.35.4 + "@img/sharp-linux-x64": 0.35.4 + "@img/sharp-linuxmusl-arm64": 0.35.4 + "@img/sharp-linuxmusl-x64": 0.35.4 + "@img/sharp-webcontainers-wasm32": 0.35.4 + "@img/sharp-win32-arm64": 0.35.4 + "@img/sharp-win32-ia32": 0.35.4 + "@img/sharp-win32-x64": 0.35.4 + "@types/node": 26.3.0 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + simple-icons@16.28.0: {} + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-bom@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.8): + dependencies: + client-only: 0.0.1 + react: 19.2.8 + optionalDependencies: + "@babel/core": 7.29.7(supports-color@7.2.0) + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + tldts-core@7.4.11: {} + + tldts@7.4.11: + dependencies: + tldts-core: 7.4.11 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.11 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + "@types/json5": 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + "@typescript-eslint/eslint-plugin": 8.68.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/parser": 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/typescript-estree": 8.68.0(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/utils": 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@8.3.0: {} + + undici@8.10.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + "@unrs/resolver-binding-android-arm-eabi": 1.12.2 + "@unrs/resolver-binding-android-arm64": 1.12.2 + "@unrs/resolver-binding-darwin-arm64": 1.12.2 + "@unrs/resolver-binding-darwin-x64": 1.12.2 + "@unrs/resolver-binding-freebsd-x64": 1.12.2 + "@unrs/resolver-binding-linux-arm-gnueabihf": 1.12.2 + "@unrs/resolver-binding-linux-arm-musleabihf": 1.12.2 + "@unrs/resolver-binding-linux-arm64-gnu": 1.12.2 + "@unrs/resolver-binding-linux-arm64-musl": 1.12.2 + "@unrs/resolver-binding-linux-loong64-gnu": 1.12.2 + "@unrs/resolver-binding-linux-loong64-musl": 1.12.2 + "@unrs/resolver-binding-linux-ppc64-gnu": 1.12.2 + "@unrs/resolver-binding-linux-riscv64-gnu": 1.12.2 + "@unrs/resolver-binding-linux-riscv64-musl": 1.12.2 + "@unrs/resolver-binding-linux-s390x-gnu": 1.12.2 + "@unrs/resolver-binding-linux-x64-gnu": 1.12.2 + "@unrs/resolver-binding-linux-x64-musl": 1.12.2 + "@unrs/resolver-binding-openharmony-arm64": 1.12.2 + "@unrs/resolver-binding-wasm32-wasi": 1.12.2 + "@unrs/resolver-binding-win32-arm64-msvc": 1.12.2 + "@unrs/resolver-binding-win32-ia32-msvc": 1.12.2 + "@unrs/resolver-binding-win32-x64-msvc": 1.12.2 + + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + tslib: 2.8.1 + optionalDependencies: + "@types/react": 19.2.18 + + use-sidecar@1.1.3(@types/react@19.2.18)(react@19.2.8): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.8 + tslib: 2.8.1 + optionalDependencies: + "@types/react": 19.2.18 + + vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.26 + rolldown: 1.2.6 + tinyglobby: 0.2.17 + optionalDependencies: + "@types/node": 26.3.0 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@4.1.11(@types/node@26.3.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0)): + dependencies: + "@vitest/expect": 4.1.11 + "@vitest/mocker": 4.1.11(vite@8.2.2(@types/node@26.3.0)(jiti@2.7.0)) + "@vitest/pretty-format": 4.1.11 + "@vitest/runner": 4.1.11 + "@vitest/snapshot": 4.1.11 + "@vitest/spy": 4.1.11 + "@vitest/utils": 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2(@types/node@26.3.0)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + "@types/node": 26.3.0 + jsdom: 30.0.1 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + "@exodus/bytes": 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - "@noble/hashes" + + whatwg-url@17.1.0: + dependencies: + "@exodus/bytes": 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - "@noble/hashes" + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/ts/web/nextjs/polaris/pnpm-workspace.yaml b/ts/web/nextjs/polaris/pnpm-workspace.yaml new file mode 100644 index 0000000..351dd78 --- /dev/null +++ b/ts/web/nextjs/polaris/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - "." + +allowBuilds: + unrs-resolver: true diff --git a/ts/web/nextjs/polaris/postcss.config.mjs b/ts/web/nextjs/polaris/postcss.config.mjs new file mode 100644 index 0000000..2f8795a --- /dev/null +++ b/ts/web/nextjs/polaris/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +} + +export default config diff --git a/ts/web/nextjs/polaris/prettier.config.mjs b/ts/web/nextjs/polaris/prettier.config.mjs new file mode 100644 index 0000000..28f15dd --- /dev/null +++ b/ts/web/nextjs/polaris/prettier.config.mjs @@ -0,0 +1,9 @@ +/** @type {import("prettier").Config} */ +const config = { + plugins: ["prettier-plugin-tailwindcss"], + semi: false, + singleQuote: false, + trailingComma: "all", +} + +export default config diff --git a/ts/web/nextjs/polaris/logo.svg b/ts/web/nextjs/polaris/public/brand/logo.svg similarity index 100% rename from ts/web/nextjs/polaris/logo.svg rename to ts/web/nextjs/polaris/public/brand/logo.svg diff --git a/ts/web/nextjs/polaris/src/app/globals.css b/ts/web/nextjs/polaris/src/app/globals.css new file mode 100644 index 0000000..495ccef --- /dev/null +++ b/ts/web/nextjs/polaris/src/app/globals.css @@ -0,0 +1,635 @@ +@import "tailwindcss"; + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--text-primary); + --color-primary: var(--primary); + --color-primary-foreground: #ffffff; + --color-muted-foreground: var(--text-secondary); + --color-border: var(--border-subtle); + --radius-lg: var(--radius-card); + --radius-xl: var(--radius-panel); +} + +:root { + color-scheme: light; + --background: #dcebf5; + --text-primary: #0f172a; + --text-secondary: #64748b; + --text-muted: #94a3b8; + --primary: #3b82f6; + --primary-hover: #2563eb; + --primary-soft: rgba(59, 130, 246, 0.1); + --surface-sidebar: rgba(255, 255, 255, 0.68); + --surface-search: rgba(255, 255, 255, 0.72); + --surface-card: rgba(255, 255, 255, 0.62); + --surface-card-hover: rgba(255, 255, 255, 0.78); + --border-glass: rgba(255, 255, 255, 0.58); + --border-subtle: rgba(15, 23, 42, 0.08); + --shadow-soft: 0 14px 40px rgba(15, 23, 42, 0.1); + --shadow-medium: 0 20px 60px rgba(15, 23, 42, 0.12); + --shadow-panel: 0 24px 80px rgba(15, 23, 42, 0.1); + --radius-card: 20px; + --radius-panel: 24px; + --sidebar-expanded: 240px; + --sidebar-collapsed: 72px; + --layout-gap: 20px; +} + +* { + box-sizing: border-box; +} + +html, +body { + min-height: 100%; +} + +body { + isolation: isolate; + margin: 0; + overflow-x: hidden; + color: var(--text-primary); + background: + radial-gradient( + circle at 12% 15%, + rgba(255, 245, 239, 0.92) 0, + transparent 34% + ), + radial-gradient( + circle at 82% 14%, + rgba(194, 222, 246, 0.92) 0, + transparent 38% + ), + radial-gradient( + circle at 48% 82%, + rgba(226, 239, 247, 0.72) 0, + transparent 44% + ), + linear-gradient(135deg, #eef5fa 0%, #dceaf4 48%, #cbdff0 100%); + font-family: + Inter, + "SF Pro Display", + "SF Pro Text", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + "PingFang SC", + "Hiragino Sans GB", + "Microsoft YaHei", + sans-serif; +} + +body::before { + position: fixed; + inset: 0; + z-index: -1; + content: ""; + pointer-events: none; + background: + linear-gradient( + 180deg, + rgba(255, 255, 255, 0.2), + rgba(255, 255, 255, 0.08) + ), + repeating-linear-gradient( + 165deg, + transparent 0 84px, + rgba(255, 255, 255, 0.08) 85px 86px + ); + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); +} + +button, +input, +a { + font: inherit; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +:focus-visible { + outline: 3px solid rgba(59, 130, 246, 0.34); + outline-offset: 3px; +} + +.glass-panel { + border: 1px solid var(--border-glass); + border-radius: var(--radius-panel); + background: var(--surface-card); + box-shadow: var(--shadow-soft); + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); +} + +.app-shell { + --sidebar-width: var(--sidebar-expanded); + min-height: 100svh; +} + +.app-shell[data-sidebar-collapsed="true"] { + --sidebar-width: var(--sidebar-collapsed); +} + +.app-shell:not([data-hydrated="true"]) *, +.app-shell:not([data-hydrated="true"]) *::before, +.app-shell:not([data-hydrated="true"]) *::after { + transition: none !important; +} + +.sidebar { + position: fixed; + inset: 20px auto 20px 20px; + z-index: 20; + display: flex; + width: var(--sidebar-width); + overflow: visible; + flex-direction: column; + border: 1px solid var(--border-glass); + border-radius: var(--radius-panel); + background: var(--surface-sidebar); + box-shadow: var(--shadow-panel); + -webkit-backdrop-filter: blur(24px); + backdrop-filter: blur(24px); + transition: width 180ms ease; +} + +.sidebar__header { + position: relative; + display: flex; + min-height: 72px; + align-items: center; + gap: 10px; + padding: 0 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.46); +} + +.sidebar__brand { + display: flex; + min-width: 0; + flex: 1; + align-items: center; + gap: 12px; + color: var(--text-primary); + text-decoration: none; +} + +.sidebar__brand img { + display: block; + flex: none; +} + +.sidebar__site-name { + overflow: hidden; + font-size: 18px; + font-weight: 700; + letter-spacing: -0.025em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar__header > button { + flex: none; + color: var(--text-secondary); + border-radius: 12px; + transition: + color 180ms ease, + background 180ms ease; +} + +.sidebar__header > button:hover { + color: var(--primary-hover); + background: var(--primary-soft); +} + +.sidebar__nav { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; + gap: 8px; + overflow-y: auto; + padding: 12px 12px 16px; +} + +.sidebar__categories { + display: flex; + flex-direction: column; + gap: 6px; +} + +.sidebar__nav-item { + display: flex; + width: 100%; + height: 48px; + min-height: 48px; + align-items: center; + justify-content: flex-start; + gap: 12px; + padding: 0 14px; + color: #334155; + text-decoration: none; + border: 0; + border-radius: 16px; + background: transparent; + transition: + color 180ms ease, + background 180ms ease; +} + +.sidebar__nav-item svg { + width: 20px; + height: 20px; + flex: none; +} + +.sidebar__nav-item--active { + color: var(--primary-hover); + background: var(--primary-soft); + font-weight: 650; +} + +.sidebar__nav-item[aria-disabled="true"] { + cursor: not-allowed; + opacity: 0.82; +} + +.sidebar__nav-item[aria-disabled="true"]:hover { + color: #334155; + background: rgba(255, 255, 255, 0.28); +} + +.sidebar__label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app-shell[data-sidebar-collapsed="true"] .sidebar__header { + justify-content: center; + padding-inline: 10px; +} + +.app-shell[data-sidebar-collapsed="true"] .sidebar__brand { + justify-content: center; +} + +.app-shell[data-sidebar-collapsed="true"] .sidebar__header > button { + position: absolute; + top: 22px; + right: -14px; + width: 28px; + height: 28px; + border: 1px solid var(--border-glass); + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12); +} + +.app-shell[data-sidebar-collapsed="true"] .sidebar__nav { + padding-inline: 10px; +} + +.app-shell[data-sidebar-collapsed="true"] .sidebar__nav-item { + justify-content: center; + padding-inline: 0; +} + +.app-shell__content { + min-height: 100svh; + margin-left: calc(var(--sidebar-width) + 40px); + transition: margin-left 180ms ease; +} + +.home-content { + display: grid; + min-height: 100svh; + place-items: start center; + padding: clamp(108px, 14vh, 156px) 40px 64px; +} + +.home-content__inner { + width: min(100%, 880px); +} + +.clock-greeting { + text-align: center; +} + +.clock-greeting__time { + min-height: 1.2em; + margin: 0; + color: var(--text-primary); + font-size: clamp(44px, 4.2vw, 56px); + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.035em; + text-wrap: balance; +} + +.clock-greeting__date { + margin: 12px 0 0; + color: var(--text-secondary); + font-size: 16px; + line-height: 1.6; +} + +.search-box { + position: relative; + z-index: 10; + width: min(100%, 760px); + margin: 40px auto 0; +} + +.search-box__form { + display: flex; + height: 68px; + align-items: center; + border: 1px solid var(--border-glass); + border-radius: var(--radius-panel); + background: var(--surface-search); + box-shadow: var(--shadow-medium); + -webkit-backdrop-filter: blur(22px); + backdrop-filter: blur(22px); + transition: + border-color 180ms ease, + box-shadow 180ms ease, + background 180ms ease; +} + +.search-box__form:focus-within { + border-color: rgba(59, 130, 246, 0.26); + background: rgba(255, 255, 255, 0.8); + box-shadow: + 0 0 0 4px rgba(59, 130, 246, 0.09), + var(--shadow-medium); +} + +.search-provider-menu__trigger { + min-width: 126px; + height: 48px; + justify-content: flex-start; + margin-left: 10px; + padding-inline: 14px; + color: #334155; + border-radius: 16px; +} + +.search-provider-menu__trigger:hover, +.search-provider-menu__trigger[aria-expanded="true"] { + color: var(--primary-hover); + background: var(--primary-soft); +} + +.search-box__icon { + width: 20px; + height: 20px; + flex: none; + margin-left: 10px; + color: var(--text-muted); +} + +.search-box__input { + min-width: 0; + height: 100%; + flex: 1; + padding: 0 14px; + color: var(--text-primary); + font-size: 16px; + line-height: 1.5; + border: 0; + outline: 0; + background: transparent; +} + +.search-box__input::placeholder { + color: var(--text-muted); + opacity: 1; +} + +.search-box__shortcut { + display: inline-flex; + min-width: 50px; + height: 30px; + flex: none; + align-items: center; + justify-content: center; + margin-right: 16px; + padding-inline: 9px; + color: var(--text-muted); + font-family: inherit; + font-size: 12px; + line-height: 1; + border: 1px solid rgba(100, 116, 139, 0.2); + border-radius: 9px; + background: rgba(255, 255, 255, 0.42); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.68); +} + +.local-search-results { + position: absolute; + top: calc(100% + 8px); + left: 0; + z-index: 30; + width: 100%; + max-height: min(520px, calc(100vh - 220px)); + overflow-y: auto; + padding: 8px; + border: 1px solid var(--border-glass); + border-radius: 20px; + background: rgba(255, 255, 255, 0.9); + box-shadow: var(--shadow-medium); + -webkit-backdrop-filter: blur(24px); + backdrop-filter: blur(24px); +} + +.local-search-results__option { + display: flex; + min-height: 58px; + align-items: center; + gap: 12px; + padding: 9px 12px; + color: var(--text-primary); + text-decoration: none; + border: 1px solid transparent; + border-radius: 14px; + transition: + color 180ms ease, + background 180ms ease, + border-color 180ms ease; +} + +.local-search-results__option[aria-selected="true"] { + color: var(--primary-hover); + border-color: rgba(59, 130, 246, 0.08); + background: var(--primary-soft); +} + +.local-search-results__copy { + display: flex; + min-width: 0; + flex: 1; + flex-direction: column; + gap: 3px; +} + +.local-search-results__title, +.local-search-results__category { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.local-search-results__title { + font-size: 14px; + font-weight: 650; +} + +.local-search-results__category { + color: var(--text-secondary); + font-size: 12px; +} + +.local-search-results__empty { + display: grid; + min-height: 96px; + place-items: center; + margin: 0; + color: var(--text-secondary); + font-size: 14px; +} + +.quick-links { + width: min(100%, 880px); + margin-top: 44px; +} + +.quick-links__title { + margin: 0 0 18px; + color: var(--text-primary); + font-size: 16px; + font-weight: 650; +} + +.quick-links__grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px 18px; +} + +.quick-link-card { + display: flex; + min-width: 0; + height: 76px; + align-items: center; + gap: 14px; + padding: 0 20px; + color: var(--text-primary); + text-decoration: none; + border: 1px solid var(--border-glass); + border-radius: var(--radius-card); + background: var(--surface-card); + box-shadow: var(--shadow-soft); + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); + transition: + transform 180ms ease, + background 180ms ease, + border-color 180ms ease, + box-shadow 180ms ease; +} + +.quick-link-card:hover { + transform: translateY(-2px); + border-color: rgba(255, 255, 255, 0.76); + background: var(--surface-card-hover); + box-shadow: 0 18px 50px rgba(15, 23, 42, 0.14); +} + +.quick-link-card > span:last-child { + min-width: 0; + overflow: hidden; + font-size: 15px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.site-icon { + position: relative; + display: inline-flex; + overflow: hidden; + flex: none; + align-items: center; + justify-content: center; + border-radius: 11px; +} + +.site-icon svg, +.site-icon img { + display: block; + max-width: 100%; + max-height: 100%; +} + +.site-icon img { + width: 100%; + height: 100%; + object-fit: contain; + border-radius: 10px; +} + +.site-icon__image { + position: absolute; + inset: 0; +} + +.site-icon__image--loading { + opacity: 0; +} + +.site-icon--initial::before { + content: attr(data-initial); +} + +.site-icon--initial { + color: var(--primary-hover); + font-size: 15px; + font-weight: 700; + background: rgba(219, 234, 254, 0.7); + box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.08); +} + +.local-search-results, +.sidebar__nav { + scrollbar-color: rgba(100, 116, 139, 0.32) transparent; + scrollbar-width: thin; +} + +@media (max-width: 1199px) { + :root { + --sidebar-expanded: 220px; + } + + .home-content { + padding-inline: 28px; + } + + .quick-links__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} diff --git a/ts/web/nextjs/polaris/src/app/layout.tsx b/ts/web/nextjs/polaris/src/app/layout.tsx new file mode 100644 index 0000000..d2cd296 --- /dev/null +++ b/ts/web/nextjs/polaris/src/app/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata, Viewport } from "next" + +import "./globals.css" + +export const metadata: Metadata = { + title: "Polaris — 个人导航起始页", + description: "快速搜索并访问常用网站的个人导航起始页。", + icons: { + icon: "/brand/logo.svg", + }, +} + +export const viewport: Viewport = { + colorScheme: "light", + themeColor: "#dcebf5", +} + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ) +} diff --git a/ts/web/nextjs/polaris/src/app/page.tsx b/ts/web/nextjs/polaris/src/app/page.tsx new file mode 100644 index 0000000..53927a8 --- /dev/null +++ b/ts/web/nextjs/polaris/src/app/page.tsx @@ -0,0 +1,27 @@ +import { HomeContent } from "@/features/home/components/home-content" +import { AppShell } from "@/features/navigation/components/app-shell" +import { getCategories } from "@/services/categories" +import { getLinks, getPinnedLinks } from "@/services/links" +import { getSettings } from "@/services/settings" + +export default function HomePage() { + const categories = getCategories() + const links = getLinks() + const pinnedLinks = getPinnedLinks() + const settings = getSettings() + + return ( + + + + ) +} diff --git a/ts/web/nextjs/polaris/src/components/shared/glass-panel.tsx b/ts/web/nextjs/polaris/src/components/shared/glass-panel.tsx new file mode 100644 index 0000000..4a4706f --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/shared/glass-panel.tsx @@ -0,0 +1,18 @@ +import { createElement, type ComponentPropsWithoutRef } from "react" + +import { cn } from "@/lib/utils" + +type GlassPanelProps = ComponentPropsWithoutRef<"div"> & { + as?: "div" | "section" | "aside" +} + +export function GlassPanel({ + as = "div", + className, + ...props +}: GlassPanelProps) { + return createElement(as, { + className: cn("glass-panel", className), + ...props, + }) +} diff --git a/ts/web/nextjs/polaris/src/components/shared/icon-candidates.test.ts b/ts/web/nextjs/polaris/src/components/shared/icon-candidates.test.ts new file mode 100644 index 0000000..60ebc74 --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/shared/icon-candidates.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest" + +import type { NavLink } from "@/types/navigation" + +import { getIconCandidates } from "./icon-candidates" + +const baseLink: NavLink = { + id: "example", + title: "Example", + description: "Example site", + url: "https://example.com/path", + categoryId: "dev", + pinned: false, + keywords: [], + sort: 10, +} + +describe("站点图标候选链", () => { + it("local 图标后依次回退 favicon 和文字", () => { + expect(getIconCandidates({ ...baseLink, icon: "local:custom" })).toEqual([ + { kind: "image", src: "/icons/custom.svg" }, + { kind: "image", src: "https://example.com/favicon.ico" }, + { kind: "initial", value: "E" }, + ]) + }) + + it("不允许 local 图标名越出 icons 目录", () => { + expect( + getIconCandidates({ ...baseLink, icon: "local:../brand/logo" })[0], + ).toEqual({ + kind: "image", + src: "https://example.com/favicon.ico", + }) + }) + + it("已知 simple icon 优先使用本地 SVG 数据", () => { + expect( + getIconCandidates({ ...baseLink, icon: "si:github" })[0], + ).toMatchObject({ + kind: "simple", + slug: "github", + }) + }) + + it("未知 simple icon 跳过无效映射", () => { + expect(getIconCandidates({ ...baseLink, icon: "si:not-real" })[0]).toEqual({ + kind: "image", + src: "https://example.com/favicon.ico", + }) + }) + + it("去重写法不同但语义相同的 favicon URL", () => { + expect( + getIconCandidates({ + ...baseLink, + icon: "url:https://example.com:443/favicon.ico", + }), + ).toEqual([ + { kind: "image", src: "https://example.com/favicon.ico" }, + { kind: "initial", value: "E" }, + ]) + }) + + it("显式 URL 图标失败后仍回退 favicon 和文字", () => { + expect( + getIconCandidates({ + ...baseLink, + icon: "url:https://cdn.example.com/icon.png", + }), + ).toEqual([ + { kind: "image", src: "https://cdn.example.com/icon.png" }, + { kind: "image", src: "https://example.com/favicon.ico" }, + { kind: "initial", value: "E" }, + ]) + }) +}) diff --git a/ts/web/nextjs/polaris/src/components/shared/icon-candidates.ts b/ts/web/nextjs/polaris/src/components/shared/icon-candidates.ts new file mode 100644 index 0000000..d6f6228 --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/shared/icon-candidates.ts @@ -0,0 +1,62 @@ +import { SIMPLE_ICON_MAP } from "@/components/shared/simple-icon-map" +import type { NavLink } from "@/types/navigation" + +const LOCAL_ICON_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/ + +export type IconCandidate = + | { kind: "simple"; slug: string } + | { kind: "image"; src: string } + | { kind: "initial"; value: string } + +function normalizeImageUrl(value: string): string | null { + try { + return new URL(value).toString() + } catch { + return null + } +} + +function getFaviconUrl(url: string): string | null { + try { + return new URL("/favicon.ico", url).toString() + } catch { + return null + } +} + +export function getIconCandidates( + link: Pick, +): IconCandidate[] { + const candidates: IconCandidate[] = [] + const icon = link.icon?.trim() + + if (icon?.startsWith("local:")) { + const name = icon.slice("local:".length) + // 本地图标只接受单个文件名,避免路径段将请求规范化到 /icons/ 之外。 + if (LOCAL_ICON_NAME_PATTERN.test(name)) { + candidates.push({ kind: "image", src: `/icons/${name}.svg` }) + } + } else if (icon?.startsWith("si:")) { + const slug = icon.slice("si:".length).toLowerCase() + if (SIMPLE_ICON_MAP[slug]) candidates.push({ kind: "simple", slug }) + } else if (icon?.startsWith("url:")) { + const src = normalizeImageUrl(icon.slice("url:".length)) + if (src) candidates.push({ kind: "image", src }) + } + + const favicon = getFaviconUrl(link.url) + if ( + favicon && + !candidates.some( + (candidate) => candidate.kind === "image" && candidate.src === favicon, + ) + ) { + candidates.push({ kind: "image", src: favicon }) + } + + candidates.push({ + kind: "initial", + value: link.title.trim().charAt(0).toUpperCase() || "?", + }) + return candidates +} diff --git a/ts/web/nextjs/polaris/src/components/shared/simple-icon-map.ts b/ts/web/nextjs/polaris/src/components/shared/simple-icon-map.ts new file mode 100644 index 0000000..d8ae7c4 --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/shared/simple-icon-map.ts @@ -0,0 +1,44 @@ +import { + siBilibili, + siBitbucket, + siDocker, + siFigma, + siGithub, + siGitlab, + siGmail, + siGoland, + siJetbrains, + siKubernetes, + siNetlify, + siNotion, + siNpm, + siPnpm, + siPostman, + siRender, + siVercel, + siYoutube, + type SimpleIcon, +} from "simple-icons" + +export const SIMPLE_ICON_MAP: Readonly> = { + bilibili: siBilibili, + bitbucket: siBitbucket, + docker: siDocker, + figma: siFigma, + github: siGithub, + gitlab: siGitlab, + gmail: siGmail, + goland: siGoland, + jetbrains: siJetbrains, + kubernetes: siKubernetes, + netlify: siNetlify, + notion: siNotion, + npm: siNpm, + pnpm: siPnpm, + postman: siPostman, + render: siRender, + vercel: siVercel, + youtube: siYoutube, +} + +// 上游 v16 不再提供 OpenAI 与 VS Code;这两项按统一候选链回退到 favicon 和文字。 diff --git a/ts/web/nextjs/polaris/src/components/shared/site-icon.test.tsx b/ts/web/nextjs/polaris/src/components/shared/site-icon.test.tsx new file mode 100644 index 0000000..7de3f00 --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/shared/site-icon.test.tsx @@ -0,0 +1,84 @@ +import { fireEvent, render, screen } from "@testing-library/react" +import { describe, expect, it } from "vitest" + +import { SiteIcon } from "./site-icon" + +describe("SiteIcon", () => { + it("渲染已知 Simple Icon", () => { + const { container } = render( + , + ) + const icon = container.querySelector("svg") + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute("width", "28") + expect(icon).toHaveAttribute("height", "28") + }) + + it("远程图片加载完成前显示首字符占位", () => { + const { container } = render( + , + ) + + const wrapper = container.querySelector(".site-icon") + const image = container.querySelector("img") + expect(wrapper).toHaveAttribute("data-initial", "E") + expect(image).toHaveClass("site-icon__image--loading") + + fireEvent.load(image!) + expect(wrapper).not.toHaveAttribute("data-initial") + expect(image).not.toHaveClass("site-icon__image--loading") + }) + + it("链接变化后从新候选链的首项重新开始", () => { + const { container, rerender } = render( + , + ) + + fireEvent.error(container.querySelector("img")!) + rerender( + , + ) + + expect(container.querySelector("img")).toHaveAttribute( + "src", + "https://cdn.example.com/other.png", + ) + }) + + it("远程图片连续失败后显示首字符", () => { + const { container } = render( + , + ) + + fireEvent.error(container.querySelector("img")!) + fireEvent.error(container.querySelector("img")!) + expect(screen.getByText("E")).toBeInTheDocument() + }) +}) diff --git a/ts/web/nextjs/polaris/src/components/shared/site-icon.tsx b/ts/web/nextjs/polaris/src/components/shared/site-icon.tsx new file mode 100644 index 0000000..a2e748d --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/shared/site-icon.tsx @@ -0,0 +1,111 @@ +"use client" + +/* eslint-disable @next/next/no-img-element -- favicon 来源由数据动态决定,静态 Image 配置无法预枚举域名。 */ +import { useState } from "react" + +import { + getIconCandidates, + type IconCandidate, +} from "@/components/shared/icon-candidates" +import { SIMPLE_ICON_MAP } from "@/components/shared/simple-icon-map" +import { cn } from "@/lib/utils" +import type { NavLink } from "@/types/navigation" + +type SiteIconProps = { + link: Pick + size?: number + className?: string +} + +type SiteIconContentProps = SiteIconProps & { + candidates: IconCandidate[] +} + +function SiteIconContent({ + link, + candidates, + size = 36, + className, +}: SiteIconContentProps) { + const [candidateIndex, setCandidateIndex] = useState(0) + const [loadedImageSrc, setLoadedImageSrc] = useState(null) + const candidate = candidates[candidateIndex] + const initialCandidate = candidates.at(-1) + const initialValue = + initialCandidate?.kind === "initial" ? initialCandidate.value : "?" + const wrapperClassName = cn("site-icon", className) + const style = { width: size, height: size } + + if (candidate.kind === "simple") { + const icon = SIMPLE_ICON_MAP[candidate.slug] + return ( + + ) + } + + if (candidate.kind === "image") { + const imageLoaded = loadedImageSrc === candidate.src + + return ( + + ) + } + + return ( + + ) +} + +export function SiteIcon({ link, size = 36, className }: SiteIconProps) { + const candidates = getIconCandidates(link) + const candidateKey = JSON.stringify([link.icon ?? "", link.title, link.url]) + + // 链接变化时通过 key 重建候选状态,避免 effect 中同步 setState 和旧链接的失败记录泄漏。 + return ( + + ) +} diff --git a/ts/web/nextjs/polaris/src/components/ui/button.tsx b/ts/web/nextjs/polaris/src/components/ui/button.tsx new file mode 100644 index 0000000..726091e --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/ui/button.tsx @@ -0,0 +1,50 @@ +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + ghost: "hover:bg-primary/10 hover:text-primary", + outline: + "border border-border bg-transparent hover:bg-primary/10 hover:text-primary", + }, + size: { + default: "h-9 px-4 py-2", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/ts/web/nextjs/polaris/src/components/ui/dropdown-menu.tsx b/ts/web/nextjs/polaris/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..9453878 --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,194 @@ +"use client" + +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger +const DropdownMenuGroup = DropdownMenuPrimitive.Group +const DropdownMenuPortal = DropdownMenuPrimitive.Portal +const DropdownMenuSub = DropdownMenuPrimitive.Sub +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuItem({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} diff --git a/ts/web/nextjs/polaris/src/components/ui/tooltip.tsx b/ts/web/nextjs/polaris/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..be73f2d --- /dev/null +++ b/ts/web/nextjs/polaris/src/components/ui/tooltip.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import * as React from "react" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 240, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const Tooltip = TooltipPrimitive.Root +const TooltipTrigger = TooltipPrimitive.Trigger + +function TooltipContent({ + className, + sideOffset = 8, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } diff --git a/ts/web/nextjs/polaris/src/data/categories.json b/ts/web/nextjs/polaris/src/data/categories.json new file mode 100644 index 0000000..317d95e --- /dev/null +++ b/ts/web/nextjs/polaris/src/data/categories.json @@ -0,0 +1,51 @@ +[ + { + "id": "ai", + "title": "AI 工具", + "description": "精选常用 AI 工具与服务,辅助创作、开发和效率提升。", + "icon": "sparkles", + "sort": 10 + }, + { + "id": "dev", + "title": "开发工具", + "description": "高效的开发工具和资源集合,提升你的开发体验和效率。", + "icon": "code", + "sort": 20 + }, + { + "id": "design", + "title": "设计资源", + "description": "常用设计工具、素材与灵感资源集合。", + "icon": "palette", + "sort": 30 + }, + { + "id": "learning", + "title": "学习资料", + "description": "整理常用学习平台、文档、课程和知识资源。", + "icon": "book-open", + "sort": 40 + }, + { + "id": "efficiency", + "title": "效率工具", + "description": "提升日常工作流效率的工具入口。", + "icon": "zap", + "sort": 50 + }, + { + "id": "media", + "title": "娱乐媒体", + "description": "常用影音、社区和内容平台。", + "icon": "circle-play", + "sort": 60 + }, + { + "id": "life", + "title": "生活服务", + "description": "日常生活、服务查询和实用网站入口。", + "icon": "coffee", + "sort": 70 + } +] diff --git a/ts/web/nextjs/polaris/src/data/data.test.ts b/ts/web/nextjs/polaris/src/data/data.test.ts new file mode 100644 index 0000000..ef922ad --- /dev/null +++ b/ts/web/nextjs/polaris/src/data/data.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from "vitest" + +import categories from "@/data/categories.json" +import links from "@/data/links.json" +import settings from "@/data/settings.json" +import { SEARCH_PROVIDERS } from "@/types/search" + +const hasUniqueValues = (values: string[]) => + new Set(values).size === values.length + +describe("Polaris 静态数据", () => { + it("包含 7 个唯一且结构完整的分类", () => { + expect(categories).toHaveLength(7) + expect(hasUniqueValues(categories.map(({ id }) => id))).toBe(true) + + for (const category of categories) { + expect(category).toEqual({ + id: expect.any(String), + title: expect.any(String), + description: expect.any(String), + icon: expect.any(String), + sort: expect.any(Number), + }) + expect(category.id).not.toBe("") + expect(category.title).not.toBe("") + expect(Number.isFinite(category.sort)).toBe(true) + } + }) + + it("包含 20 条合法链接且分类引用均存在", () => { + const categoryIds = new Set(categories.map(({ id }) => id)) + expect(links).toHaveLength(20) + expect(hasUniqueValues(links.map(({ id }) => id))).toBe(true) + + for (const link of links) { + expect(link.id).toEqual(expect.any(String)) + expect(link.title).toEqual(expect.any(String)) + expect(link.description).toEqual(expect.any(String)) + expect(link.url).toEqual(expect.any(String)) + expect(link.categoryId).toEqual(expect.any(String)) + expect(link.pinned).toEqual(expect.any(Boolean)) + expect(link.keywords).toEqual(expect.any(Array)) + expect(link.sort).toEqual(expect.any(Number)) + expect(link.id).not.toBe("") + expect(link.title).not.toBe("") + expect(() => new URL(link.url)).not.toThrow() + // 跨分类常用链接仍必须归属于真实分类,避免未来 API 出现悬空外键。 + expect(categoryIds.has(link.categoryId)).toBe(true) + expect( + link.keywords.every((keyword) => typeof keyword === "string"), + ).toBe(true) + expect(Number.isFinite(link.sort)).toBe(true) + if (link.icon !== undefined) expect(typeof link.icon).toBe("string") + if (!link.pinned) expect(link.pinnedSort).toBeUndefined() + } + }) + + it("8 条常用链接拥有唯一 pinnedSort", () => { + const pinned = links.filter(({ pinned }) => pinned) + const pinnedSorts = pinned.map(({ pinnedSort }) => pinnedSort) + + expect(pinned).toHaveLength(8) + expect(pinnedSorts.every(Number.isFinite)).toBe(true) + expect(new Set(pinnedSorts).size).toBe(8) + }) + + it("设置包含完整且合法的嵌套契约", () => { + expect(settings).toEqual({ + siteName: "Polaris", + theme: "light-glass", + search: { + defaultProvider: "google", + enabledProviders: [...SEARCH_PROVIDERS], + }, + background: { + type: "gradient", + url: null, + blur: expect.any(Number), + overlay: expect.any(Number), + }, + sidebar: { + defaultCollapsed: expect.any(Boolean), + }, + }) + expect(SEARCH_PROVIDERS).toContain(settings.search.defaultProvider) + expect(Number.isFinite(settings.background.blur)).toBe(true) + expect(Number.isFinite(settings.background.overlay)).toBe(true) + }) +}) diff --git a/ts/web/nextjs/polaris/src/data/links.json b/ts/web/nextjs/polaris/src/data/links.json new file mode 100644 index 0000000..b4ab8a4 --- /dev/null +++ b/ts/web/nextjs/polaris/src/data/links.json @@ -0,0 +1,230 @@ +[ + { + "id": "github", + "title": "GitHub", + "description": "全球最大的代码托管平台", + "url": "https://github.com", + "icon": "si:github", + "categoryId": "dev", + "pinned": true, + "pinnedSort": 10, + "keywords": ["代码", "仓库", "开源", "git"], + "sort": 10 + }, + { + "id": "gitlab", + "title": "GitLab", + "description": "一体化 DevOps 平台", + "url": "https://gitlab.com", + "icon": "si:gitlab", + "categoryId": "dev", + "pinned": false, + "keywords": ["代码", "仓库", "devops", "ci", "cd"], + "sort": 20 + }, + { + "id": "bitbucket", + "title": "Bitbucket", + "description": "代码托管和协作开发", + "url": "https://bitbucket.org", + "icon": "si:bitbucket", + "categoryId": "dev", + "pinned": false, + "keywords": ["代码", "仓库", "git"], + "sort": 30 + }, + { + "id": "vscode", + "title": "VS Code", + "description": "强大的代码编辑器", + "url": "https://code.visualstudio.com", + "icon": "si:vscode", + "categoryId": "dev", + "pinned": false, + "keywords": ["编辑器", "ide", "代码"], + "sort": 40 + }, + { + "id": "jetbrains", + "title": "JetBrains", + "description": "智能开发工具套件", + "url": "https://www.jetbrains.com", + "icon": "si:jetbrains", + "categoryId": "dev", + "pinned": false, + "keywords": ["ide", "开发工具", "编辑器"], + "sort": 50 + }, + { + "id": "goland", + "title": "GoLand", + "description": "Go 语言 IDE", + "url": "https://www.jetbrains.com/go/", + "icon": "si:goland", + "categoryId": "dev", + "pinned": false, + "keywords": ["go", "golang", "ide"], + "sort": 60 + }, + { + "id": "postman", + "title": "Postman", + "description": "API 开发和测试工具", + "url": "https://www.postman.com", + "icon": "si:postman", + "categoryId": "dev", + "pinned": false, + "keywords": ["api", "接口", "测试"], + "sort": 70 + }, + { + "id": "docker", + "title": "Docker", + "description": "容器化平台", + "url": "https://www.docker.com", + "icon": "si:docker", + "categoryId": "dev", + "pinned": false, + "keywords": ["容器", "部署", "镜像"], + "sort": 80 + }, + { + "id": "kubernetes", + "title": "Kubernetes", + "description": "容器编排平台", + "url": "https://kubernetes.io", + "icon": "si:kubernetes", + "categoryId": "dev", + "pinned": false, + "keywords": ["k8s", "容器", "编排", "云原生"], + "sort": 90 + }, + { + "id": "vercel", + "title": "Vercel", + "description": "前端部署与托管平台", + "url": "https://vercel.com", + "icon": "si:vercel", + "categoryId": "dev", + "pinned": true, + "pinnedSort": 70, + "keywords": ["部署", "前端", "托管"], + "sort": 100 + }, + { + "id": "netlify", + "title": "Netlify", + "description": "现代化的开发平台", + "url": "https://www.netlify.com", + "icon": "si:netlify", + "categoryId": "dev", + "pinned": false, + "keywords": ["部署", "前端", "托管"], + "sort": 110 + }, + { + "id": "render", + "title": "Render", + "description": "云应用托管平台", + "url": "https://render.com", + "icon": "si:render", + "categoryId": "dev", + "pinned": false, + "keywords": ["部署", "后端", "托管", "云服务"], + "sort": 120 + }, + { + "id": "npm", + "title": "npm", + "description": "JavaScript 包管理器", + "url": "https://www.npmjs.com", + "icon": "si:npm", + "categoryId": "dev", + "pinned": false, + "keywords": ["javascript", "node", "包管理"], + "sort": 130 + }, + { + "id": "pnpm", + "title": "pnpm", + "description": "快速的包管理器", + "url": "https://pnpm.io", + "icon": "si:pnpm", + "categoryId": "dev", + "pinned": false, + "keywords": ["javascript", "node", "包管理"], + "sort": 140 + }, + { + "id": "figma", + "title": "Figma", + "description": "界面设计协作工具", + "url": "https://www.figma.com", + "icon": "si:figma", + "categoryId": "dev", + "pinned": true, + "pinnedSort": 50, + "keywords": ["设计", "ui", "协作"], + "sort": 150 + }, + { + "id": "chatgpt", + "title": "ChatGPT", + "description": "智能对话与创作助手", + "url": "https://chatgpt.com", + "icon": "si:openai", + "categoryId": "ai", + "pinned": true, + "pinnedSort": 20, + "keywords": ["ai", "人工智能", "对话", "写作"], + "sort": 10 + }, + { + "id": "gmail", + "title": "Gmail", + "description": "Google 电子邮件服务", + "url": "https://mail.google.com", + "icon": "si:gmail", + "categoryId": "efficiency", + "pinned": true, + "pinnedSort": 30, + "keywords": ["邮件", "邮箱", "google", "mail"], + "sort": 10 + }, + { + "id": "notion", + "title": "Notion", + "description": "一体化知识与协作空间", + "url": "https://www.notion.so", + "icon": "si:notion", + "categoryId": "efficiency", + "pinned": true, + "pinnedSort": 40, + "keywords": ["笔记", "知识库", "协作", "文档"], + "sort": 20 + }, + { + "id": "youtube", + "title": "YouTube", + "description": "全球视频内容平台", + "url": "https://www.youtube.com", + "icon": "si:youtube", + "categoryId": "media", + "pinned": true, + "pinnedSort": 60, + "keywords": ["视频", "影音", "直播", "google"], + "sort": 10 + }, + { + "id": "bilibili", + "title": "Bilibili", + "description": "年轻人的视频社区", + "url": "https://www.bilibili.com", + "icon": "si:bilibili", + "categoryId": "media", + "pinned": true, + "pinnedSort": 80, + "keywords": ["哔哩哔哩", "视频", "动漫", "社区"], + "sort": 20 + } +] diff --git a/ts/web/nextjs/polaris/src/data/settings.json b/ts/web/nextjs/polaris/src/data/settings.json new file mode 100644 index 0000000..46e65bb --- /dev/null +++ b/ts/web/nextjs/polaris/src/data/settings.json @@ -0,0 +1,17 @@ +{ + "siteName": "Polaris", + "theme": "light-glass", + "search": { + "defaultProvider": "google", + "enabledProviders": ["google", "bing", "github", "local"] + }, + "background": { + "type": "gradient", + "url": null, + "blur": 8, + "overlay": 0.32 + }, + "sidebar": { + "defaultCollapsed": false + } +} diff --git a/ts/web/nextjs/polaris/src/features/home/components/clock-greeting.test.tsx b/ts/web/nextjs/polaris/src/features/home/components/clock-greeting.test.tsx new file mode 100644 index 0000000..1c42d31 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/clock-greeting.test.tsx @@ -0,0 +1,60 @@ +import { act, render, screen } from "@testing-library/react" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import { ClockGreeting } from "./clock-greeting" + +describe("ClockGreeting", () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 7, 20, 21, 56, 30)) + }) + + afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() + }) + + it("挂载后显示当前问候、时间和日期", () => { + render() + + expect(screen.getByRole("timer")).toHaveTextContent("晚上好,21:56") + expect(screen.getByText("2026 年 8 月 20 日 星期四")).toBeInTheDocument() + }) + + it("在下一分钟边界刷新", () => { + render() + + act(() => { + vi.advanceTimersByTime(30_000) + }) + + expect(screen.getByRole("timer")).toHaveTextContent("21:57") + }) + + it("延迟执行后重新对齐下一个真实分钟边界", () => { + const setTimeoutSpy = vi.spyOn(window, "setTimeout") + vi.setSystemTime(new Date(2026, 7, 20, 9, 6, 30)) + render() + + const delayedTick = setTimeoutSpy.mock.calls[0]?.[0] + expect(typeof delayedTick).toBe("function") + if (typeof delayedTick !== "function") throw new Error("Missing clock tick") + + vi.setSystemTime(new Date(2026, 7, 20, 9, 7, 12)) + act(() => delayedTick()) + + expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), 48_000) + }) + + it("跨午夜同步更新日期与问候语", () => { + vi.setSystemTime(new Date(2026, 7, 20, 23, 59, 30)) + render() + + act(() => { + vi.advanceTimersByTime(30_000) + }) + + expect(screen.getByRole("timer")).toHaveTextContent("夜深了,00:00") + expect(screen.getByText("2026 年 8 月 21 日 星期五")).toBeInTheDocument() + }) +}) diff --git a/ts/web/nextjs/polaris/src/features/home/components/clock-greeting.tsx b/ts/web/nextjs/polaris/src/features/home/components/clock-greeting.tsx new file mode 100644 index 0000000..0facb33 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/clock-greeting.tsx @@ -0,0 +1,34 @@ +"use client" + +import { useClock } from "@/features/home/hooks/use-clock" +import { + formatDate, + formatTime, + getGreeting, +} from "@/features/home/lib/date-time" + +export function ClockGreeting() { + const now = useClock() + + if (!now) { + return ( +
+

+ 正在同步时间… +

+ +
+ ) + } + + return ( +
+

+ {getGreeting(now.getHours())},{formatTime(now)} +

+

{formatDate(now)}

+
+ ) +} diff --git a/ts/web/nextjs/polaris/src/features/home/components/home-content.tsx b/ts/web/nextjs/polaris/src/features/home/components/home-content.tsx new file mode 100644 index 0000000..faec043 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/home-content.tsx @@ -0,0 +1,34 @@ +import { ClockGreeting } from "@/features/home/components/clock-greeting" +import { QuickLinks } from "@/features/home/components/quick-links" +import { SearchBox } from "@/features/home/components/search-box" +import type { Category, NavLink } from "@/types/navigation" +import type { AppSettings } from "@/types/settings" + +type HomeContentProps = { + categories: Category[] + links: NavLink[] + pinnedLinks: NavLink[] + settings: AppSettings +} + +export function HomeContent({ + categories, + links, + pinnedLinks, + settings, +}: HomeContentProps) { + return ( +
+
+ + + +
+
+ ) +} diff --git a/ts/web/nextjs/polaris/src/features/home/components/local-search-results.tsx b/ts/web/nextjs/polaris/src/features/home/components/local-search-results.tsx new file mode 100644 index 0000000..720428a --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/local-search-results.tsx @@ -0,0 +1,53 @@ +import type { MutableRefObject } from "react" + +import { SiteIcon } from "@/components/shared/site-icon" +import type { LocalSearchResult } from "@/types/search" + +type LocalSearchResultsProps = { + listboxId: string + results: LocalSearchResult[] + activeIndex: number + optionRefs: MutableRefObject> + onActiveIndexChange: (index: number) => void +} + +export function LocalSearchResults({ + listboxId, + results, + activeIndex, + optionRefs, + onActiveIndexChange, +}: LocalSearchResultsProps) { + return ( + + ) +} diff --git a/ts/web/nextjs/polaris/src/features/home/components/quick-links.test.tsx b/ts/web/nextjs/polaris/src/features/home/components/quick-links.test.tsx new file mode 100644 index 0000000..94d5958 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/quick-links.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from "@testing-library/react" +import { describe, expect, it } from "vitest" + +import type { NavLink } from "@/types/navigation" + +import { QuickLinks } from "./quick-links" + +const links: NavLink[] = [ + { + id: "github", + title: "GitHub", + description: "代码托管", + url: "https://github.com", + icon: "si:github", + categoryId: "dev", + pinned: true, + pinnedSort: 10, + keywords: [], + sort: 10, + }, + { + id: "chatgpt", + title: "ChatGPT", + description: "AI 助手", + url: "https://chatgpt.com", + icon: "si:openai", + categoryId: "ai", + pinned: true, + pinnedSort: 20, + keywords: [], + sort: 10, + }, +] + +describe("QuickLinks", () => { + it("按输入顺序渲染安全的新标签页链接", () => { + render() + const renderedLinks = screen.getAllByRole("link") + + expect(renderedLinks.map((link) => link.textContent)).toEqual([ + "GitHub", + "ChatGPT", + ]) + for (const link of renderedLinks) { + expect(link).toHaveAttribute("target", "_blank") + expect(link).toHaveAttribute("rel", "noopener noreferrer") + } + }) + + it("只展示图标和名称,不展示描述", () => { + render() + expect(screen.queryByText("代码托管")).not.toBeInTheDocument() + expect(screen.queryByText("AI 助手")).not.toBeInTheDocument() + }) +}) diff --git a/ts/web/nextjs/polaris/src/features/home/components/quick-links.tsx b/ts/web/nextjs/polaris/src/features/home/components/quick-links.tsx new file mode 100644 index 0000000..8e84799 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/quick-links.tsx @@ -0,0 +1,26 @@ +import { SiteIcon } from "@/components/shared/site-icon" +import type { NavLink } from "@/types/navigation" + +export function QuickLinks({ links }: { links: NavLink[] }) { + return ( +
+ +
+ {links.map((link) => ( + + + {link.title} + + ))} +
+
+ ) +} diff --git a/ts/web/nextjs/polaris/src/features/home/components/search-box.test.tsx b/ts/web/nextjs/polaris/src/features/home/components/search-box.test.tsx new file mode 100644 index 0000000..b88324a --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/search-box.test.tsx @@ -0,0 +1,172 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import type { ComponentProps } from "react" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import type { Category, NavLink } from "@/types/navigation" + +import { SearchBox } from "./search-box" + +const categories: Category[] = [ + { id: "dev", title: "开发工具", description: "", icon: "code", sort: 10 }, +] + +const links: NavLink[] = [ + { + id: "github", + title: "GitHub", + description: "代码托管平台", + url: "https://github.com", + icon: "si:github", + categoryId: "dev", + pinned: true, + pinnedSort: 10, + keywords: ["git", "仓库"], + sort: 10, + }, + { + id: "gitlab", + title: "GitLab", + description: "DevOps 平台", + url: "https://gitlab.com", + icon: "si:gitlab", + categoryId: "dev", + pinned: false, + keywords: ["git", "ci"], + sort: 20, + }, +] + +function renderSearchBox( + overrides: Partial> = {}, +) { + const onExternalNavigate = vi.fn() + const renderResult = render( + , + ) + return { onExternalNavigate, ...renderResult } +} + +describe("SearchBox", () => { + beforeEach(() => window.localStorage.clear()) + afterEach(() => vi.restoreAllMocks()) + + it("挂载后自动聚焦,并响应 Ctrl+K 与 Meta+K", async () => { + renderSearchBox() + const input = screen.getByRole("combobox", { + name: "搜索网站、书签或关键词", + }) + await waitFor(() => expect(input).toHaveFocus()) + input.blur() + fireEvent.keyDown(window, { key: "k", ctrlKey: true }) + expect(input).toHaveFocus() + input.blur() + fireEvent.keyDown(window, { key: "k", metaKey: true }) + expect(input).toHaveFocus() + }) + + it("Google 模式回车只执行当前页外部导航", async () => { + const user = userEvent.setup() + const { onExternalNavigate } = renderSearchBox() + const input = screen.getByRole("combobox", { + name: "搜索网站、书签或关键词", + }) + await user.type(input, "Next.js 中文{Enter}") + const url = new URL(onExternalNavigate.mock.calls[0][0]) + expect(url.hostname).toBe("www.google.com") + expect(url.searchParams.get("q")).toBe("Next.js 中文") + expect(screen.queryByRole("listbox")).not.toBeInTheDocument() + }) + + it("空白查询不导航", async () => { + const user = userEvent.setup() + const { onExternalNavigate } = renderSearchBox() + await user.type(screen.getByRole("combobox"), " {Enter}") + expect(onExternalNavigate).not.toHaveBeenCalled() + }) + + it("切换 Local 后实时显示结果并持久化 Provider", async () => { + const user = userEvent.setup() + renderSearchBox() + await user.click( + screen.getByRole("button", { name: "选择搜索方式,当前为 Google" }), + ) + await user.click(screen.getByRole("menuitemradio", { name: "本地" })) + await user.type(screen.getByRole("combobox"), "git") + + expect(screen.getByRole("listbox")).toBeInTheDocument() + expect(screen.getAllByRole("option")).toHaveLength(2) + expect(window.localStorage.getItem("polaris.search.provider")).toBe("local") + }) + + it("Local 模式支持上下选择、Enter 与 Esc", async () => { + const user = userEvent.setup() + renderSearchBox({ defaultProvider: "local" }) + const input = screen.getByRole("combobox") + await user.type(input, "git") + + const options = screen.getAllByRole("option") + expect(options[0]).toHaveAttribute("aria-selected", "true") + expect(options[0]).toHaveAttribute("target", "_blank") + expect(options[0]).toHaveAttribute("rel", "noopener noreferrer") + + await user.keyboard("{ArrowUp}") + expect(options[1]).toHaveAttribute("aria-selected", "true") + await user.keyboard("{ArrowDown}") + expect(options[0]).toHaveAttribute("aria-selected", "true") + fireEvent.mouseMove(options[1]) + expect(options[1]).toHaveAttribute("aria-selected", "true") + + const clickSpy = vi + .spyOn(HTMLAnchorElement.prototype, "click") + .mockImplementation(() => {}) + await user.keyboard("{Enter}") + expect(clickSpy).toHaveBeenCalledOnce() + + await user.keyboard("{Escape}") + expect(input).toHaveValue("") + expect(screen.queryByRole("listbox")).not.toBeInTheDocument() + }) + + it("Local 无结果显示空状态", async () => { + const user = userEvent.setup() + renderSearchBox({ defaultProvider: "local" }) + await user.type(screen.getByRole("combobox"), "不存在的网站") + expect(screen.getByText("没有匹配的网站")).toBeInTheDocument() + }) + + it("恢复合法 Provider,非法或未启用值回退默认 Provider", async () => { + window.localStorage.setItem("polaris.search.provider", "local") + const { unmount } = renderSearchBox() + await waitFor(() => + expect( + screen.getByRole("button", { name: /当前为 本地/ }), + ).toBeInTheDocument(), + ) + unmount() + + window.localStorage.setItem("polaris.search.provider", "invalid") + const secondRender = renderSearchBox() + await waitFor(() => + expect( + screen.getByRole("button", { name: /当前为 Google/ }), + ).toBeInTheDocument(), + ) + secondRender.unmount() + + window.localStorage.setItem("polaris.search.provider", "local") + renderSearchBox({ enabledProviders: ["google", "bing"] }) + await waitFor(() => + expect( + screen.getByRole("button", { name: /当前为 Google/ }), + ).toBeInTheDocument(), + ) + }) +}) diff --git a/ts/web/nextjs/polaris/src/features/home/components/search-box.tsx b/ts/web/nextjs/polaris/src/features/home/components/search-box.tsx new file mode 100644 index 0000000..1969feb --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/search-box.tsx @@ -0,0 +1,217 @@ +"use client" + +import { Search } from "lucide-react" +import { + useEffect, + useId, + useMemo, + useRef, + useState, + useSyncExternalStore, + type FormEvent, + type KeyboardEvent, +} from "react" + +import { LocalSearchResults } from "@/features/home/components/local-search-results" +import { SearchProviderMenu } from "@/features/home/components/search-provider-menu" +import { searchLocalLinks } from "@/features/home/lib/local-search" +import { + parseStoredProvider, + SEARCH_PROVIDER_STORAGE_KEY, +} from "@/features/home/lib/provider-preference" +import { buildSearchUrl } from "@/features/home/lib/search-providers" +import type { Category, NavLink } from "@/types/navigation" +import type { SearchProvider } from "@/types/search" + +type SearchBoxProps = { + links: NavLink[] + categories: Category[] + defaultProvider: SearchProvider + enabledProviders: SearchProvider[] + onExternalNavigate?: (url: string) => void +} + +function subscribeHydration() { + return () => undefined +} + +function getClientHydrationSnapshot() { + return true +} + +function getServerHydrationSnapshot() { + return false +} + +function readStoredProvider( + defaultProvider: SearchProvider, + enabledProviders: SearchProvider[], +): SearchProvider { + try { + const storedProvider = parseStoredProvider( + window.localStorage.getItem(SEARCH_PROVIDER_STORAGE_KEY), + defaultProvider, + ) + return enabledProviders.includes(storedProvider) + ? storedProvider + : defaultProvider + } catch { + return defaultProvider + } +} + +export function SearchBox({ + links, + categories, + defaultProvider, + enabledProviders, + onExternalNavigate, +}: SearchBoxProps) { + const inputRef = useRef(null) + const optionRefs = useRef>([]) + const listboxId = useId() + const [query, setQuery] = useState("") + const [selectedProvider, setSelectedProvider] = + useState(null) + const [activeIndex, setActiveIndex] = useState(0) + const hydrated = useSyncExternalStore( + subscribeHydration, + getClientHydrationSnapshot, + getServerHydrationSnapshot, + ) + + // Server 与 hydration 首帧保持默认 Provider;hydration 后再容错读取浏览器偏好。 + const restoredProvider = hydrated + ? readStoredProvider(defaultProvider, enabledProviders) + : defaultProvider + const provider = selectedProvider ?? restoredProvider + + const results = useMemo( + () => + provider === "local" ? searchLocalLinks(query, links, categories, 8) : [], + [categories, links, provider, query], + ) + const panelOpen = provider === "local" && query.trim().length > 0 + const resolvedActiveIndex = + results.length > 0 ? Math.min(activeIndex, results.length - 1) : 0 + const activeOptionId = + panelOpen && results.length > 0 + ? `${listboxId}-option-${resolvedActiveIndex}` + : undefined + const shortcutHint = + hydrated && navigator.userAgent.includes("Mac") ? "⌘K" : "Ctrl K" + + useEffect(() => { + inputRef.current?.focus() + + const focusSearch = (event: globalThis.KeyboardEvent) => { + if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") { + event.preventDefault() + inputRef.current?.focus() + } + } + + window.addEventListener("keydown", focusSearch) + return () => window.removeEventListener("keydown", focusSearch) + }, []) + + const changeProvider = (nextProvider: SearchProvider) => { + setSelectedProvider(nextProvider) + setActiveIndex(0) + try { + window.localStorage.setItem(SEARCH_PROVIDER_STORAGE_KEY, nextProvider) + } catch { + // Provider 已保留在当前组件状态,存储受限时不影响本次交互。 + } + } + + const openActiveLocalResult = () => { + if (results.length === 0) return + // 复用真实 anchor 的导航语义,键盘 Enter 不另造 click-only 跳转路径。 + optionRefs.current[resolvedActiveIndex]?.click() + } + + const submitSearch = (event: FormEvent) => { + event.preventDefault() + + // Local 只打开站内结果;外部 Provider 只构建搜索 URL,两条路径严格分离。 + if (provider === "local") { + openActiveLocalResult() + return + } + + const url = buildSearchUrl(provider, query) + if (!url) return + if (onExternalNavigate) onExternalNavigate(url) + else window.location.assign(url) + } + + const handleInputKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault() + setQuery("") + setActiveIndex(0) + return + } + + if (provider !== "local") return + + if (event.key === "Enter") { + event.preventDefault() + openActiveLocalResult() + return + } + + if (results.length === 0) return + if (event.key === "ArrowDown") { + event.preventDefault() + setActiveIndex((index) => (index + 1) % results.length) + } else if (event.key === "ArrowUp") { + event.preventDefault() + setActiveIndex((index) => (index - 1 + results.length) % results.length) + } + } + + return ( +
+
+ +
+ ) +} diff --git a/ts/web/nextjs/polaris/src/features/home/components/search-provider-menu.tsx b/ts/web/nextjs/polaris/src/features/home/components/search-provider-menu.tsx new file mode 100644 index 0000000..7f9f1f3 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/components/search-provider-menu.tsx @@ -0,0 +1,85 @@ +"use client" + +import { + Database, + GitFork, + Globe2, + Search, + type LucideIcon, +} from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { isSearchProvider } from "@/features/home/lib/search-providers" +import type { SearchProvider } from "@/types/search" + +const PROVIDER_LABELS: Record = { + google: "Google", + bing: "Bing", + github: "GitHub", + local: "本地", +} + +const PROVIDER_ICONS: Record = { + google: Globe2, + bing: Search, + github: GitFork, + local: Database, +} + +type SearchProviderMenuProps = { + provider: SearchProvider + enabledProviders: SearchProvider[] + onProviderChange: (provider: SearchProvider) => void +} + +export function SearchProviderMenu({ + provider, + enabledProviders, + onProviderChange, +}: SearchProviderMenuProps) { + const CurrentIcon = PROVIDER_ICONS[provider] + + return ( + + + + + + { + if (isSearchProvider(value)) onProviderChange(value) + }} + > + {enabledProviders.map((enabledProvider) => { + const Icon = PROVIDER_ICONS[enabledProvider] + return ( + + + ) + })} + + + + ) +} diff --git a/ts/web/nextjs/polaris/src/features/home/hooks/use-clock.ts b/ts/web/nextjs/polaris/src/features/home/hooks/use-clock.ts new file mode 100644 index 0000000..8fdf767 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/hooks/use-clock.ts @@ -0,0 +1,61 @@ +"use client" + +import { useSyncExternalStore } from "react" + +import { getDelayUntilNextMinute } from "@/features/home/lib/date-time" + +type Listener = () => void + +const listeners = new Set() +let currentTime: Date | null = null +let timeoutId: number | undefined + +function emitCurrentTime(): Date { + const nextTime = new Date() + currentTime = nextTime + listeners.forEach((listener) => listener()) + return nextTime +} + +function scheduleNextTick() { + const observedTime = emitCurrentTime() + + // 每次 tick 都重新对齐;标签页休眠或主线程阻塞后不会把延迟永久带入后续分钟。 + timeoutId = window.setTimeout( + scheduleNextTick, + getDelayUntilNextMinute(observedTime), + ) +} + +function startClock() { + scheduleNextTick() +} + +function stopClock() { + if (timeoutId !== undefined) window.clearTimeout(timeoutId) + timeoutId = undefined + currentTime = null +} + +function subscribe(listener: Listener) { + listeners.add(listener) + if (listeners.size === 1) startClock() + + return () => { + listeners.delete(listener) + if (listeners.size === 0) stopClock() + } +} + +function getSnapshot() { + return currentTime +} + +function getServerSnapshot() { + return null +} + +export function useClock(): Date | null { + // 服务端与 hydration 首帧固定为空;订阅建立后再读取浏览器时间,避免首屏不一致。 + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) +} diff --git a/ts/web/nextjs/polaris/src/features/home/lib/date-time.test.ts b/ts/web/nextjs/polaris/src/features/home/lib/date-time.test.ts new file mode 100644 index 0000000..c09e52e --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/lib/date-time.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest" + +import { + formatDate, + formatTime, + getDelayUntilNextMinute, + getGreeting, +} from "./date-time" + +describe("首页日期与问候", () => { + it.each([ + [0, "夜深了"], + [4, "夜深了"], + [5, "早上好"], + [11, "早上好"], + [12, "中午好"], + [13, "中午好"], + [14, "下午好"], + [17, "下午好"], + [18, "晚上好"], + [23, "晚上好"], + ])("%i 时显示 %s", (hour, expected) => { + expect(getGreeting(hour)).toBe(expected) + }) + + it("拒绝无效小时", () => { + expect(() => getGreeting(-1)).toThrow("hour must be between 0 and 23") + expect(() => getGreeting(24)).toThrow("hour must be between 0 and 23") + }) + + it("格式化中文日期与两位时间", () => { + const date = new Date(2026, 7, 20, 9, 6, 30) + + expect(formatTime(date)).toBe("09:06") + expect(formatDate(date)).toBe("2026 年 8 月 20 日 星期四") + }) + + it("计算到下一分钟边界的延迟", () => { + expect(getDelayUntilNextMinute(new Date(2026, 7, 20, 9, 6, 30, 250))).toBe( + 29_750, + ) + }) +}) diff --git a/ts/web/nextjs/polaris/src/features/home/lib/date-time.ts b/ts/web/nextjs/polaris/src/features/home/lib/date-time.ts new file mode 100644 index 0000000..cf55039 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/lib/date-time.ts @@ -0,0 +1,34 @@ +const WEEKDAYS = [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", +] as const + +export function getGreeting(hour: number): string { + if (!Number.isInteger(hour) || hour < 0 || hour > 23) { + throw new RangeError("hour must be between 0 and 23") + } + if (hour < 5) return "夜深了" + if (hour < 12) return "早上好" + if (hour < 14) return "中午好" + if (hour < 18) return "下午好" + return "晚上好" +} + +export function formatTime(date: Date): string { + const hours = String(date.getHours()).padStart(2, "0") + const minutes = String(date.getMinutes()).padStart(2, "0") + return `${hours}:${minutes}` +} + +export function formatDate(date: Date): string { + return `${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日 ${WEEKDAYS[date.getDay()]}` +} + +export function getDelayUntilNextMinute(date: Date): number { + return 60_000 - (date.getSeconds() * 1_000 + date.getMilliseconds()) +} diff --git a/ts/web/nextjs/polaris/src/features/home/lib/local-search.test.ts b/ts/web/nextjs/polaris/src/features/home/lib/local-search.test.ts new file mode 100644 index 0000000..67d0639 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/lib/local-search.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "vitest" + +import type { Category, NavLink } from "@/types/navigation" + +import { searchLocalLinks } from "./local-search" + +const categories: Category[] = [ + { id: "dev", title: "开发工具", description: "", icon: "code", sort: 10 }, + { id: "ai", title: "AI 工具", description: "", icon: "sparkles", sort: 20 }, +] + +const links: NavLink[] = [ + { + id: "github", + title: "GitHub", + description: "代码托管平台", + url: "https://github.com", + icon: "si:github", + categoryId: "dev", + pinned: true, + pinnedSort: 10, + keywords: ["git", "仓库"], + sort: 20, + }, + { + id: "gitlab", + title: "GitLab", + description: "DevOps 平台", + url: "https://gitlab.com", + icon: "si:gitlab", + categoryId: "dev", + pinned: false, + keywords: ["git", "ci"], + sort: 10, + }, + { + id: "chatgpt", + title: "ChatGPT", + description: "智能对话与写作", + url: "https://chatgpt.com", + icon: "si:openai", + categoryId: "ai", + pinned: true, + pinnedSort: 20, + keywords: ["人工智能", "写作"], + sort: 10, + }, +] + +describe("Local 搜索", () => { + it("标题完全匹配优先于标题包含和关键词", () => { + expect(searchLocalLinks("github", links, categories)[0].link.id).toBe( + "github", + ) + }) + + it("匹配关键词、描述、分类标题和分类 ID", () => { + expect(searchLocalLinks("人工智能", links, categories)[0].link.id).toBe( + "chatgpt", + ) + expect(searchLocalLinks("DevOps", links, categories)[0].link.id).toBe( + "gitlab", + ) + expect( + searchLocalLinks("开发工具", links, categories).map( + ({ link }) => link.id, + ), + ).toEqual(["gitlab", "github"]) + expect(searchLocalLinks("ai", links, categories)[0].link.id).toBe("chatgpt") + }) + + it("同分时按 sort 再按标题稳定排序", () => { + expect( + searchLocalLinks("git", links, categories).map(({ link }) => link.id), + ).toEqual(["gitlab", "github"]) + + const titleTies: NavLink[] = [ + { + ...links[0], + id: "beta", + title: "贝塔", + keywords: ["工具"], + sort: 10, + }, + { + ...links[0], + id: "alpha", + title: "阿尔法", + keywords: ["工具"], + sort: 10, + }, + ] + + expect( + searchLocalLinks("工具", titleTies, categories).map( + ({ link }) => link.id, + ), + ).toEqual(["alpha", "beta"]) + }) + + it("规范化大小写、全角字符和首尾空白并限制结果数量", () => { + expect( + searchLocalLinks(" GitHub ", links, categories, 1).map( + ({ link }) => link.id, + ), + ).toEqual(["github"]) + }) + + it("空查询返回空数组", () => { + expect(searchLocalLinks(" ", links, categories)).toEqual([]) + }) +}) diff --git a/ts/web/nextjs/polaris/src/features/home/lib/local-search.ts b/ts/web/nextjs/polaris/src/features/home/lib/local-search.ts new file mode 100644 index 0000000..a8b464b --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/lib/local-search.ts @@ -0,0 +1,74 @@ +import type { Category, NavLink } from "@/types/navigation" +import type { LocalSearchResult } from "@/types/search" + +function normalize(value: string): string { + return value.normalize("NFKC").trim().toLocaleLowerCase("zh-CN") +} + +function textScore( + value: string, + query: string, + exact: number, + prefix: number, + contains: number, +): number { + if (value === query) return exact + if (value.startsWith(query)) return prefix + if (value.includes(query)) return contains + return 0 +} + +function scoreLink( + link: NavLink, + category: Category | undefined, + query: string, +): number { + const title = normalize(link.title) + const keywordScore = Math.max( + 0, + ...link.keywords.map((keyword) => + textScore(normalize(keyword), query, 300, 260, 240), + ), + ) + + // 各区间不重叠,保证“标题 > 关键词 > 描述 > 分类”的规则不会被字段数量反转。 + return Math.max( + textScore(title, query, 500, 400, 350), + keywordScore, + normalize(link.description).includes(query) ? 180 : 0, + category ? textScore(normalize(category.title), query, 140, 130, 120) : 0, + category ? textScore(normalize(category.id), query, 100, 90, 80) : 0, + ) +} + +export function searchLocalLinks( + query: string, + links: NavLink[], + categories: Category[], + limit = 8, +): LocalSearchResult[] { + const normalizedQuery = normalize(query) + if (!normalizedQuery || limit <= 0) return [] + + const categoryById = new Map( + categories.map((category) => [category.id, category]), + ) + + return links + .map((link) => { + const category = categoryById.get(link.categoryId) + return { + link, + categoryTitle: category?.title ?? "未分类", + score: scoreLink(link, category, normalizedQuery), + } + }) + .filter(({ score }) => score > 0) + .sort( + (a, b) => + b.score - a.score || + a.link.sort - b.link.sort || + a.link.title.localeCompare(b.link.title, "zh-CN"), + ) + .slice(0, limit) +} diff --git a/ts/web/nextjs/polaris/src/features/home/lib/provider-preference.test.ts b/ts/web/nextjs/polaris/src/features/home/lib/provider-preference.test.ts new file mode 100644 index 0000000..f90d674 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/lib/provider-preference.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest" + +import { parseStoredProvider } from "./provider-preference" + +describe("Provider 持久化值", () => { + it("返回合法存储值", () => { + expect(parseStoredProvider("local", "google")).toBe("local") + }) + + it("非法或空值回退默认值", () => { + expect(parseStoredProvider("duckduckgo", "google")).toBe("google") + expect(parseStoredProvider(null, "bing")).toBe("bing") + }) +}) diff --git a/ts/web/nextjs/polaris/src/features/home/lib/provider-preference.ts b/ts/web/nextjs/polaris/src/features/home/lib/provider-preference.ts new file mode 100644 index 0000000..1938582 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/lib/provider-preference.ts @@ -0,0 +1,11 @@ +import { isSearchProvider } from "@/features/home/lib/search-providers" +import type { SearchProvider } from "@/types/search" + +export const SEARCH_PROVIDER_STORAGE_KEY = "polaris.search.provider" + +export function parseStoredProvider( + value: string | null, + fallback: SearchProvider, +): SearchProvider { + return isSearchProvider(value) ? value : fallback +} diff --git a/ts/web/nextjs/polaris/src/features/home/lib/search-providers.test.ts b/ts/web/nextjs/polaris/src/features/home/lib/search-providers.test.ts new file mode 100644 index 0000000..d90d8af --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/lib/search-providers.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest" + +import { buildSearchUrl, isSearchProvider } from "./search-providers" + +describe("外部搜索 Provider", () => { + it.each([ + ["google", "https://www.google.com/search"], + ["bing", "https://www.bing.com/search"], + ["github", "https://github.com/search"], + ] as const)("为 %s 构建正确 URL", (provider, expectedBase) => { + const result = buildSearchUrl(provider, "Next.js 中文") + const url = new URL(result!) + expect(`${url.origin}${url.pathname}`).toBe(expectedBase) + expect(url.searchParams.get("q")).toBe("Next.js 中文") + }) + + it("对查询参数分隔符和百分号执行 URL 编码", () => { + const query = "a&next=1 %= #中文" + const url = new URL(buildSearchUrl("google", query)!) + + expect(url.searchParams.get("q")).toBe(query) + expect([...url.searchParams.keys()]).toEqual(["q"]) + }) + + it("空白查询不生成 URL", () => { + expect(buildSearchUrl("google", " ")).toBeNull() + }) + + it("识别合法 Provider", () => { + expect(isSearchProvider("local")).toBe(true) + expect(isSearchProvider("duckduckgo")).toBe(false) + expect(isSearchProvider(null)).toBe(false) + }) +}) diff --git a/ts/web/nextjs/polaris/src/features/home/lib/search-providers.ts b/ts/web/nextjs/polaris/src/features/home/lib/search-providers.ts new file mode 100644 index 0000000..896a4f4 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/home/lib/search-providers.ts @@ -0,0 +1,30 @@ +import { + SEARCH_PROVIDERS, + type ExternalSearchProvider, + type SearchProvider, +} from "@/types/search" + +const SEARCH_ENDPOINTS: Record = { + google: "https://www.google.com/search", + bing: "https://www.bing.com/search", + github: "https://github.com/search", +} + +export function isSearchProvider(value: unknown): value is SearchProvider { + return ( + typeof value === "string" && + SEARCH_PROVIDERS.includes(value as SearchProvider) + ) +} + +export function buildSearchUrl( + provider: ExternalSearchProvider, + query: string, +): string | null { + const normalizedQuery = query.trim() + if (!normalizedQuery) return null + + const url = new URL(SEARCH_ENDPOINTS[provider]) + url.searchParams.set("q", normalizedQuery) + return url.toString() +} diff --git a/ts/web/nextjs/polaris/src/features/navigation/components/app-shell.test.tsx b/ts/web/nextjs/polaris/src/features/navigation/components/app-shell.test.tsx new file mode 100644 index 0000000..bbfd630 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/navigation/components/app-shell.test.tsx @@ -0,0 +1,162 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import type { Category } from "@/types/navigation" + +import { AppShell } from "./app-shell" + +const categories: Category[] = [ + { id: "dev", title: "开发工具", description: "开发", icon: "code", sort: 10 }, +] + +describe("AppShell", () => { + beforeEach(() => window.localStorage.clear()) + afterEach(() => vi.restoreAllMocks()) + + it("切换侧栏并持久化状态", () => { + render( + +
首页内容
+
, + ) + + const toggle = screen.getByRole("button", { name: "折叠侧边栏" }) + fireEvent.click(toggle) + + expect(screen.getByTestId("app-shell")).toHaveAttribute( + "data-sidebar-collapsed", + "true", + ) + expect(window.localStorage.getItem("polaris.sidebar.collapsed")).toBe( + "true", + ) + }) + + it("hydration 后恢复持久化状态", async () => { + window.localStorage.setItem("polaris.sidebar.collapsed", "true") + render( + +
首页内容
+
, + ) + + await waitFor(() => { + expect(screen.getByTestId("app-shell")).toHaveAttribute( + "data-sidebar-collapsed", + "true", + ) + expect(screen.getByTestId("app-shell")).toHaveAttribute( + "data-hydrated", + "true", + ) + }) + }) + + it("写入失败时内存偏好优先于旧持久化值", () => { + window.localStorage.setItem("polaris.sidebar.collapsed", "false") + vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("storage unavailable") + }) + + render( + +
首页内容
+
, + ) + + fireEvent.click(screen.getByRole("button", { name: "折叠侧边栏" })) + expect(screen.getByTestId("app-shell")).toHaveAttribute( + "data-sidebar-collapsed", + "true", + ) + }) + + it("忽略 sessionStorage 的同名 storage 事件", () => { + window.localStorage.setItem("polaris.sidebar.collapsed", "false") + vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("storage unavailable") + }) + + render( + +
首页内容
+
, + ) + + fireEvent.click(screen.getByRole("button", { name: "折叠侧边栏" })) + fireEvent( + window, + new StorageEvent("storage", { + key: "polaris.sidebar.collapsed", + storageArea: window.sessionStorage, + }), + ) + + expect(screen.getByTestId("app-shell")).toHaveAttribute( + "data-sidebar-collapsed", + "true", + ) + }) + + it("存储不可用时仍保留当前会话内交互", () => { + vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { + throw new Error("storage unavailable") + }) + vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("storage unavailable") + }) + + render( + +
首页内容
+
, + ) + + fireEvent.click(screen.getByRole("button", { name: "折叠侧边栏" })) + expect(screen.getByTestId("app-shell")).toHaveAttribute( + "data-sidebar-collapsed", + "true", + ) + }) + + it("分类项可聚焦但保持禁用", () => { + render( + +
首页内容
+
, + ) + + expect( + screen.getByRole("button", { + name: "开发工具,分类页将在下一阶段提供", + }), + ).toHaveAttribute("aria-disabled", "true") + expect( + screen.queryByRole("link", { name: /开发工具/ }), + ).not.toBeInTheDocument() + }) +}) diff --git a/ts/web/nextjs/polaris/src/features/navigation/components/app-shell.tsx b/ts/web/nextjs/polaris/src/features/navigation/components/app-shell.tsx new file mode 100644 index 0000000..4014109 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/navigation/components/app-shell.tsx @@ -0,0 +1,127 @@ +"use client" + +import { useCallback, useSyncExternalStore, type ReactNode } from "react" + +import { Sidebar } from "@/features/navigation/components/sidebar" +import { + parseSidebarPreference, + SIDEBAR_STORAGE_KEY, +} from "@/features/navigation/lib/sidebar-preference" +import type { Category } from "@/types/navigation" + +type AppShellProps = { + categories: Category[] + siteName: string + defaultCollapsed: boolean + children: ReactNode +} + +type Listener = () => void + +const listeners = new Set() +let volatilePreference: boolean | undefined + +function emitPreferenceChange() { + listeners.forEach((listener) => listener()) +} + +function handleStorage(event: StorageEvent) { + if (event.storageArea !== window.localStorage) return + if (event.key !== null && event.key !== SIDEBAR_STORAGE_KEY) return + volatilePreference = undefined + emitPreferenceChange() +} + +function subscribe(listener: Listener) { + listeners.add(listener) + if (listeners.size === 1) window.addEventListener("storage", handleStorage) + + return () => { + listeners.delete(listener) + if (listeners.size === 0) { + window.removeEventListener("storage", handleStorage) + volatilePreference = undefined + } + } +} + +function readPreference(fallback: boolean): boolean { + if (volatilePreference !== undefined) return volatilePreference + + try { + return parseSidebarPreference( + window.localStorage.getItem(SIDEBAR_STORAGE_KEY), + fallback, + ) + } catch { + return fallback + } +} + +function writePreference(value: boolean) { + volatilePreference = value + try { + window.localStorage.setItem(SIDEBAR_STORAGE_KEY, String(value)) + volatilePreference = undefined + } catch { + // 存储不可用时仍保留当前会话内交互,不让隐私模式破坏页面。 + } + emitPreferenceChange() +} + +function subscribeHydration() { + return () => undefined +} + +function getClientHydrationSnapshot() { + return true +} + +function getServerHydrationSnapshot() { + return false +} + +export function AppShell({ + categories, + siteName, + defaultCollapsed, + children, +}: AppShellProps) { + const getSnapshot = useCallback( + () => readPreference(defaultCollapsed), + [defaultCollapsed], + ) + const getServerSnapshot = useCallback( + () => defaultCollapsed, + [defaultCollapsed], + ) + const collapsed = useSyncExternalStore( + subscribe, + getSnapshot, + getServerSnapshot, + ) + const hydrated = useSyncExternalStore( + subscribeHydration, + getClientHydrationSnapshot, + getServerHydrationSnapshot, + ) + + const toggleSidebar = () => writePreference(!collapsed) + + return ( +
+ +
{children}
+
+ ) +} diff --git a/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.tsx b/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.tsx new file mode 100644 index 0000000..d7db875 --- /dev/null +++ b/ts/web/nextjs/polaris/src/features/navigation/components/category-icon.tsx @@ -0,0 +1,25 @@ +import { + BookOpen, + CirclePlay, + Code2, + Coffee, + Palette, + Sparkles, + Zap, + type LucideIcon, +} from "lucide-react" + +const CATEGORY_ICONS: Readonly> = { + sparkles: Sparkles, + code: Code2, + palette: Palette, + "book-open": BookOpen, + zap: Zap, + "circle-play": CirclePlay, + coffee: Coffee, +} + +export function CategoryIcon({ name }: { name: string }) { + const Icon = CATEGORY_ICONS[name] ?? Sparkles + return