Преглед на файлове

Build API service curriculum with interactive lessons and runnable FastAPI lab

Codex преди 1 месец
ревизия
d507c649b4
променени са 91 файла, в които са добавени 23540 реда и са изтрити 0 реда
  1. 42 0
      .gitignore
  2. 5 0
      .openai/hosting.json
  3. 13 0
      .oxfmtrc.json
  4. 56 0
      .oxlintrc.json
  5. 58 0
      README.md
  6. 916 0
      app/course.tsx
  7. 407 0
      app/demos.tsx
  8. 1766 0
      app/globals.css
  9. 20 0
      app/layout.tsx
  10. 22 0
      app/lesson-types.ts
  11. 1622 0
      app/lessons.json
  12. 4 0
      app/page.tsx
  13. 1 0
      app/videos.json
  14. 25 0
      components.json
  15. 78 0
      components/ui/accordion.tsx
  16. 187 0
      components/ui/alert-dialog.tsx
  17. 76 0
      components/ui/alert.tsx
  18. 22 0
      components/ui/aspect-ratio.tsx
  19. 207 0
      components/ui/attachment.tsx
  20. 109 0
      components/ui/avatar.tsx
  21. 52 0
      components/ui/badge.tsx
  22. 122 0
      components/ui/breadcrumb.tsx
  23. 128 0
      components/ui/bubble.tsx
  24. 87 0
      components/ui/button-group.tsx
  25. 58 0
      components/ui/button.tsx
  26. 231 0
      components/ui/calendar.tsx
  27. 103 0
      components/ui/card.tsx
  28. 242 0
      components/ui/carousel.tsx
  29. 373 0
      components/ui/chart.tsx
  30. 28 0
      components/ui/checkbox.tsx
  31. 21 0
      components/ui/collapsible.tsx
  32. 300 0
      components/ui/combobox.tsx
  33. 193 0
      components/ui/command.tsx
  34. 272 0
      components/ui/context-menu.tsx
  35. 159 0
      components/ui/dialog.tsx
  36. 6 0
      components/ui/direction.tsx
  37. 228 0
      components/ui/drawer.tsx
  38. 272 0
      components/ui/dropdown-menu.tsx
  39. 104 0
      components/ui/empty.tsx
  40. 238 0
      components/ui/field.tsx
  41. 51 0
      components/ui/hover-card.tsx
  42. 158 0
      components/ui/input-group.tsx
  43. 86 0
      components/ui/input-otp.tsx
  44. 20 0
      components/ui/input.tsx
  45. 201 0
      components/ui/item.tsx
  46. 26 0
      components/ui/kbd.tsx
  47. 20 0
      components/ui/label.tsx
  48. 71 0
      components/ui/marker.tsx
  49. 284 0
      components/ui/menubar.tsx
  50. 130 0
      components/ui/message-scroller.tsx
  51. 92 0
      components/ui/message.tsx
  52. 65 0
      components/ui/native-select.tsx
  53. 171 0
      components/ui/navigation-menu.tsx
  54. 133 0
      components/ui/pagination.tsx
  55. 90 0
      components/ui/popover.tsx
  56. 83 0
      components/ui/progress.tsx
  57. 38 0
      components/ui/radio-group.tsx
  58. 50 0
      components/ui/resizable.tsx
  59. 55 0
      components/ui/scroll-area.tsx
  60. 202 0
      components/ui/select.tsx
  61. 25 0
      components/ui/separator.tsx
  62. 137 0
      components/ui/sheet.tsx
  63. 723 0
      components/ui/sidebar.tsx
  64. 13 0
      components/ui/skeleton.tsx
  65. 52 0
      components/ui/slider.tsx
  66. 16 0
      components/ui/spinner.tsx
  67. 32 0
      components/ui/switch.tsx
  68. 116 0
      components/ui/table.tsx
  69. 82 0
      components/ui/tabs.tsx
  70. 18 0
      components/ui/textarea.tsx
  71. 229 0
      components/ui/toast.tsx
  72. 89 0
      components/ui/toggle-group.tsx
  73. 45 0
      components/ui/toggle.tsx
  74. 66 0
      components/ui/tooltip.tsx
  75. 5 0
      examples/.gitignore
  76. 198 0
      examples/README.md
  77. 51 0
      examples/VERIFICATION.md
  78. 20 0
      examples/concurrency_client.py
  79. 271 0
      examples/main.py
  80. 23 0
      examples/requirements.txt
  81. 220 0
      examples/test_main.py
  82. 170 0
      examples/web-client.html
  83. 21 0
      hooks/use-mobile.ts
  84. 6 0
      lib/utils.ts
  85. 5 0
      next.config.ts
  86. 9881 0
      package-lock.json
  87. 55 0
      package.json
  88. BIN
      public/downloads/api-lab.zip
  89. 1 0
      public/favicon.svg
  90. 30 0
      tsconfig.json
  91. 61 0
      vite.config.ts

+ 42 - 0
.gitignore

@@ -0,0 +1,42 @@
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/.vinext/
+/out/
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local environment
+.env*
+
+# deployment and generated files
+.vercel
+next-env.d.ts
+/dist/
+/.wrangler/
+/outputs/
+/work/
+__pycache__/
+.pytest_cache/
+*.pyc
+*.tsbuildinfo

+ 5 - 0
.openai/hosting.json

@@ -0,0 +1,5 @@
+{
+  "d1": null,
+  "r2": null,
+  "project_id": "appgprj_6aa25688c4208191881fcae29000f06f"
+}

+ 13 - 0
.oxfmtrc.json

@@ -0,0 +1,13 @@
+{
+  "$schema": "./node_modules/oxfmt/configuration_schema.json",
+  "singleQuote": true,
+  "printWidth": 80,
+  "sortPackageJson": false,
+  "ignorePatterns": [
+    "package-lock.json",
+    "pnpm-lock.yaml",
+    "yarn.lock",
+    "bun.lock",
+    "bun.lockb"
+  ]
+}

+ 56 - 0
.oxlintrc.json

@@ -0,0 +1,56 @@
+{
+  "$schema": "./node_modules/oxlint/configuration_schema.json",
+  "plugins": [
+    "eslint",
+    "typescript",
+    "unicorn",
+    "oxc",
+    "react",
+    "import",
+    "jsx-a11y",
+    "nextjs"
+  ],
+  "categories": {
+    "correctness": "error"
+  },
+  "env": {
+    "builtin": true,
+    "browser": true,
+    "node": true
+  },
+  "options": {
+    "typeAware": true,
+    "typeCheck": true
+  },
+  "ignorePatterns": [
+    ".next/**",
+    ".vinext/**",
+    "out/**",
+    "build/**",
+    "dist/**",
+    "coverage/**",
+    "next-env.d.ts"
+  ],
+  "rules": {
+    "no-array-constructor": "error",
+    "no-var": "error",
+    "prefer-const": "error",
+    "prefer-rest-params": "error",
+    "prefer-spread": "error",
+    "import/no-anonymous-default-export": "warn",
+    "react/display-name": "error",
+    "react/jsx-no-comment-textnodes": "error",
+    "react/no-unescaped-entities": "error",
+    "react/react-compiler": "error",
+    "react/require-render-return": "error",
+    "react/rules-of-hooks": "error",
+    "typescript/ban-ts-comment": "error",
+    "typescript/no-deprecated": "error",
+    "typescript/no-empty-object-type": "error",
+    "typescript/no-explicit-any": "error",
+    "typescript/no-namespace": "error",
+    "typescript/no-require-imports": "error",
+    "typescript/no-unnecessary-type-constraint": "error",
+    "typescript/no-unsafe-function-type": "error"
+  }
+}

+ 58 - 0
README.md

@@ -0,0 +1,58 @@
+# Agent 开发 · 后端基础补修课
+
+单页课程教材。8 个模块保留独立入口,目前只完成 API 服务开发的 16 节课件。其他模块不包含预先编写的教学内容。
+
+## 本地查看
+
+项目依赖 Node.js 22.13 或更新版本。第一次运行:
+
+```bash
+npm install
+npm run dev
+```
+
+打开终端显示的地址(通常为 http://localhost:3000)。顶部「授课视图」会收起目录并放大正文和代码,适合录屏;再次点击退出。课时链接使用 `?lesson=01` 到 `?lesson=16`,可以直接定位,浏览器前进和后退可用。交互图解是浏览器内的状态模拟,真实后端需单独运行配套项目。
+
+## 修改课件
+
+- `app/lessons.json`:16 节结构化课件。每节包括目标、概念、实战步骤、验收、排错、练习和技术文档链接。
+- `app/course.tsx`:模块目录、课件展示、视频位置、授课视图。
+- `app/demos.tsx`:HTTP 请求处理、协议消息序列、异步 I/O 时间线三个教学演示。
+- `app/globals.css`:版式、移动端与录屏样式。
+- `app/videos.json`:课时编号到视频的映射,默认留空。视频尚未录制。
+- `examples/`:可运行的 FastAPI 参考项目源代码。完整项目覆盖课程的主线端点,课堂代码包含需自行加入的演进步骤和扩展实验。
+- `public/downloads/api-lab.zip`:学员下载包。解压后的入口为 `api-lab/main.py`,按包内 README 启动。
+
+修改课时标题、时长或正文后,目录会从课程数据自动生成。阶段分组目前对应 01–04、05–08、09–10、11–16;如增删课时,需要同时调整 course.tsx 内的 phases。
+
+## 接入课程视频
+
+录制完成后,编辑 `app/videos.json`。键为两位课时编号;`type: "file"` 使用网页内播放器(浏览器支持的 MP4/WebM 文件),`type: "link"` 打开已有视频平台地址。
+
+```json
+{
+  "01": { "type": "file", "url": "/videos/01.mp4" },
+  "02": {
+    "type": "link",
+    "url": "https://你的课程平台/实际视频地址",
+    "title": "读懂一次 HTTP 请求"
+  }
+}
+```
+
+本地视频放在 `public/videos/`;大视频建议使用可直接访问的 HTTPS 媒体地址。示例里的平台地址须换成真实地址。没有配置的课时显示「视频待录制」,不会生成假的播放入口。本页不上传或转码视频,也不绕过视频平台的访问限制。
+
+## 示例项目和验证
+
+进入 `examples`,按该目录的 README 创建 Python 虚拟环境、安装依赖并运行 FastAPI。课件网页与 Python API 是独立进程;网页上线不代表 Python 示例接口同时上线。
+
+```bash
+npm exec tsc -- --noEmit
+npm run build
+```
+
+示例项目含 30 项 pytest 测试,版本和真实网络验证结果见 `examples/VERIFICATION.md`。课件使用 Python 3.11+ / Pydantic 2,下载包锁定已测试版本。课程源码片段用于逐节演进,不应不加区分地全部追加到参考项目中。
+
+更新 examples 后,重新生成 `public/downloads/api-lab.zip`,只收录示例源码、依赖、说明和客户端,不包含虚拟环境、缓存或本地密钥。ZIP 内保留 `api-lab/` 顶层目录。
+
+网站的私有发布信息保存在 `.openai/hosting.json`。学生访问权限需要在正式发放教材前按实际授课范围设置。

+ 916 - 0
app/course.tsx

@@ -0,0 +1,916 @@
+'use client';
+import { useEffect, useState, type MouseEvent, type ReactNode } from 'react';
+import {
+  Terminal,
+  ArrowUpRight,
+  ArrowRight,
+  ArrowLeft,
+  Code2,
+  Database,
+  Layers3,
+  ListTodo,
+  MemoryStick,
+  Container,
+  Server,
+  Activity,
+  BookOpen,
+  Download,
+  Monitor,
+  X,
+  Check,
+  Copy,
+  Clock3,
+  Video as VideoIcon,
+  ChevronRight,
+  FileCode2,
+  ExternalLink,
+} from 'lucide-react';
+import {
+  Sidebar,
+  SidebarProvider,
+  SidebarContent,
+  SidebarHeader,
+  SidebarFooter,
+  SidebarTrigger,
+  useSidebar,
+} from '@/components/ui/sidebar';
+import { Button } from '@/components/ui/button';
+import {
+  Table,
+  TableHeader,
+  TableBody,
+  TableRow,
+  TableHead,
+  TableCell,
+} from '@/components/ui/table';
+import { ProtocolDemo, HttpDemo, AsyncDemo } from './demos';
+import type { Lesson, Video } from './lesson-types';
+import lessonData from './lessons.json';
+import videoData from './videos.json';
+
+const lessons = lessonData as Lesson[];
+const videos = videoData as Record<string, Video>;
+const modules = [
+  { name: 'API 服务开发', slug: 'api', icon: Code2 },
+  { name: '数据库开发', slug: 'database', icon: Database },
+  { name: 'API + 数据库', slug: 'api-database', icon: Layers3 },
+  { name: '异步任务开发', slug: 'tasks', icon: ListTodo },
+  { name: 'Redis 缓存', slug: 'redis', icon: MemoryStick },
+  { name: 'Docker 容器化', slug: 'docker', icon: Container },
+  { name: 'Linux 部署', slug: 'linux', icon: Server },
+  { name: '线上问题排查', slug: 'troubleshooting', icon: Activity },
+];
+const phases = [
+  {
+    name: '起步与 HTTP',
+    range: '01—04',
+    description: '让服务运行,读懂请求,约束输入与输出。',
+    from: 0,
+    to: 4,
+  },
+  {
+    name: 'REST 接口',
+    range: '05—08',
+    description: '围绕资源编写增删改查,处理错误与公共依赖。',
+    from: 4,
+    to: 8,
+  },
+  {
+    name: '异步与调用',
+    range: '09—10',
+    description: '理解 I/O 等待,完成服务调用与浏览器联调。',
+    from: 8,
+    to: 10,
+  },
+  {
+    name: '流式与实时通信',
+    range: '11—16',
+    description: '逐段读取响应,建立双向连接,完成接口验收。',
+    from: 10,
+    to: 16,
+  },
+];
+const lessonSections = [
+  ['objectives', '本节目标'],
+  ['video', '课程视频'],
+  ['concepts', '必要概念'],
+  ['steps', '动手实现'],
+  ['checks', '验收结果'],
+  ['pitfalls', '常见错误'],
+  ['exercise', '课后练习'],
+  ['references', '技术文档'],
+];
+const overviewSections = [
+  ['path', '学习路径'],
+  ['communication', '通信方式演示'],
+  ['project', '贯穿项目'],
+  ['ready', '完成标准'],
+];
+type Navigate = (module: string, lesson?: string) => void;
+function routeLink(module: string, lesson?: string) {
+  return lesson
+    ? `?lesson=${lesson}`
+    : module === 'api'
+      ? '?module=api'
+      : `?module=${module}`;
+}
+function LocalLink({
+  module = 'api',
+  lesson,
+  navigate,
+  children,
+  className,
+  ...props
+}: {
+  module?: string;
+  lesson?: string;
+  navigate: Navigate;
+  children: ReactNode;
+  className?: string;
+  'aria-current'?: 'page';
+}) {
+  return (
+    <a
+      href={routeLink(module, lesson)}
+      className={className}
+      {...props}
+      onClick={(e: MouseEvent<HTMLAnchorElement>) => {
+        if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
+          return;
+        e.preventDefault();
+        navigate(module, lesson);
+      }}
+    >
+      {children}
+    </a>
+  );
+}
+function Navigation({
+  active,
+  lesson,
+  navigate,
+}: {
+  active: string;
+  lesson?: string;
+  navigate: Navigate;
+}) {
+  const { setOpenMobile } = useSidebar();
+  const go: Navigate = (m, l) => {
+    navigate(m, l);
+    setOpenMobile(false);
+  };
+  return (
+    <Sidebar>
+      <SidebarHeader>
+        <LocalLink navigate={go} className="brand">
+          <span className="brand-symbol">
+            <Terminal size={21} />
+          </span>
+          <span>
+            Agent 开发<span className="brand-sub">后端基础 · 补修课</span>
+          </span>
+        </LocalLink>
+      </SidebarHeader>
+      <SidebarContent>
+        <div className="nav-caption">
+          课程目录 <span>08 MODULES</span>
+        </div>
+        <nav className="module-nav" aria-label="课程模块">
+          {modules.map((m) => (
+            <LocalLink
+              key={m.slug}
+              module={m.slug}
+              navigate={go}
+              className={`module-link ${active === m.slug ? 'selected' : ''}`}
+              aria-current={active === m.slug && !lesson ? 'page' : undefined}
+            >
+              <m.icon size={18} />
+              <span>{m.name}</span>
+              {m.slug === 'api' ? (
+                <span className="nav-dot" />
+              ) : (
+                <span className="soon">待编写</span>
+              )}
+            </LocalLink>
+          ))}
+        </nav>
+        {active === 'api' && lesson && (
+          <nav className="lesson-nav" aria-label="API 课时">
+            <div className="nav-caption">
+              本模块课时 <span>16 LESSONS</span>
+            </div>
+            {phases.map((p) => (
+              <div key={p.name}>
+                <p className="lesson-nav-group">{p.name}</p>
+                {lessons.slice(p.from, p.to).map((l) => (
+                  <LocalLink
+                    key={l.id}
+                    lesson={l.id}
+                    navigate={go}
+                    className={`lesson-nav-link ${lesson === l.id ? 'current' : ''}`}
+                    aria-current={lesson === l.id ? 'page' : undefined}
+                  >
+                    <span>{l.id}</span>
+                    {l.title}
+                  </LocalLink>
+                ))}
+              </div>
+            ))}
+          </nav>
+        )}
+      </SidebarContent>
+      <SidebarFooter>
+        <div className="sidebar-note">
+          <BookOpen size={17} />
+          <div>
+            从 Python 到后端服务<small>先运行,再理解,再修改。</small>
+          </div>
+        </div>
+      </SidebarFooter>
+    </Sidebar>
+  );
+}
+
+export default function Course() {
+  const [module, setModule] = useState('api');
+  const [lessonId, setLessonId] = useState<string>();
+  const [presentation, setPresentation] = useState(false);
+  const [notFound, setNotFound] = useState(false);
+  const lesson = lessons.find((l) => l.id === lessonId);
+  const current = modules.find((m) => m.slug === module)!;
+  useEffect(() => {
+    const read = () => {
+      const q = new URLSearchParams(location.search);
+      const id = q.get('lesson');
+      const mod = q.get('module') || 'api';
+      const validLesson = id ? lessons.some((l) => l.id === id) : true;
+      const validModule = modules.some((m) => m.slug === mod);
+      setNotFound(!validLesson || !validModule);
+      setLessonId(validLesson && id ? id : undefined);
+      setModule(validModule ? mod : 'api');
+      if (id && validLesson) setModule('api');
+    };
+    read();
+    window.addEventListener('popstate', read);
+    return () => window.removeEventListener('popstate', read);
+  }, []);
+  useEffect(() => {
+    document.title = `${lesson?.title || current.name} · Agent 开发补修课`;
+  }, [lesson, current.name]);
+  const navigate: Navigate = (mod, id) => {
+    setModule(mod);
+    setLessonId(id);
+    setNotFound(false);
+    history.pushState({}, '', routeLink(mod, id));
+    window.scrollTo({ top: 0, behavior: 'instant' });
+    requestAnimationFrame(() =>
+      document.querySelector<HTMLElement>('h1')?.focus({ preventScroll: true }),
+    );
+  };
+  return (
+    <SidebarProvider
+      className={presentation ? 'presentation' : ''}
+      style={{ '--sidebar-width': '258px' } as React.CSSProperties}
+    >
+      <a className="skip-link" href="#main-content">
+        跳到课件正文
+      </a>
+      <Navigation active={module} lesson={lessonId} navigate={navigate} />
+      <div className="workspace">
+        <header className="topbar">
+          <div className="breadcrumbs">
+            <SidebarTrigger aria-label="展开或收起课程目录" />
+            <LocalLink navigate={navigate}>后端基础</LocalLink>
+            <span>/</span>
+            <strong>{current.name}</strong>
+            {lesson && (
+              <>
+                <span>/</span>
+                <span className="crumb-lesson">{lesson.id}</span>
+              </>
+            )}
+          </div>
+          <div className="topbar-actions">
+            <a
+              className="quiet-link docs-link"
+              href="https://fastapi.tiangolo.com/"
+              target="_blank"
+              rel="noreferrer"
+            >
+              FastAPI 文档 <ArrowUpRight size={15} />
+            </a>
+            <Button
+              variant="ghost"
+              onClick={() => setPresentation((v) => !v)}
+              aria-pressed={presentation}
+            >
+              {presentation ? <X /> : <Monitor />}
+              <span>{presentation ? '退出授课' : '授课视图'}</span>
+            </Button>
+          </div>
+        </header>
+        <div className="document-grid">
+          <main id="main-content">
+            {notFound ? (
+              <div className="pending-module">
+                <h1 tabIndex={-1}>未找到该课件</h1>
+                <p>该地址中的模块或课时不存在,请从课程目录重新选择。</p>
+                <LocalLink navigate={navigate} className="primary-link">
+                  返回 API 服务开发 <ArrowRight size={16} />
+                </LocalLink>
+              </div>
+            ) : module !== 'api' ? (
+              <PendingModule
+                name={current.name}
+                index={modules.findIndex((m) => m.slug === module) + 1}
+                navigate={navigate}
+              />
+            ) : lesson ? (
+              <LessonPage lesson={lesson} navigate={navigate} />
+            ) : (
+              <Overview navigate={navigate} />
+            )}
+          </main>
+          <aside className="toc" aria-label="本页导航">
+            <p>{lesson ? '本节导航' : '本模块导航'}</p>
+            {(module === 'api'
+              ? lesson
+                ? lessonSections
+                : overviewSections
+              : [['reserved', '内容状态']]
+            ).map(([id, label]) => (
+              <a href={`#${id}`} key={id}>
+                {label}
+              </a>
+            ))}
+            <div className="toc-note">
+              <span className="tiny-label">
+                {lesson ? '本节实战' : '学习准备'}
+              </span>
+              <p>
+                {lesson ? (
+                  <>
+                    {lesson.minutes} 分钟建议录制时长
+                    <br />
+                    完成代码后检查验收结果
+                    <br />
+                    遇到错误先看响应与日志
+                  </>
+                ) : (
+                  <>
+                    会写 Python 函数
+                    <br />
+                    了解列表与字典
+                    <br />
+                    能在终端执行命令
+                  </>
+                )}
+              </p>
+            </div>
+            {module === 'api' && (
+              <a
+                href="/downloads/api-lab.zip"
+                download
+                className="toc-download"
+              >
+                <Download size={14} />
+                示例项目
+              </a>
+            )}
+          </aside>
+        </div>
+        <footer className="page-footer">
+          <span>Agent 开发 · 后端基础补修课</span>
+          <span>API 模块 / 课件版本 01</span>
+        </footer>
+      </div>
+    </SidebarProvider>
+  );
+}
+function Overview({ navigate }: { navigate: Navigate }) {
+  const minutes = lessons.reduce((a, l) => a + l.minutes, 0);
+  return (
+    <>
+      <div className="eyebrow">
+        MODULE 01 <span /> 服务与通信
+      </div>
+      <h1 tabIndex={-1}>
+        API 服务开发<span className="title-dot">.</span>
+      </h1>
+      <p className="lead">
+        把 Python 函数变成可以被调用的服务。
+        <br />
+        从第一个请求开始,完成 REST 接口、流式输出与双向通信。
+      </p>
+      <div className="meta-row">
+        <span>Python 3.11+</span>
+        <span>FastAPI / Pydantic 2</span>
+        <span>{lessons.length} 节实战课</span>
+        <span>
+          建议录制约 {Math.floor(minutes / 60)} 小时 {minutes % 60} 分
+        </span>
+      </div>
+      <div className="overview-actions">
+        <LocalLink lesson="01" navigate={navigate} className="primary-link">
+          开始第一节 <ArrowRight size={16} />
+        </LocalLink>
+        <a href="/downloads/api-lab.zip" download className="secondary-link">
+          <Download size={16} />
+          下载示例项目
+        </a>
+        <span className="material-state">课件可读 · 视频待录制</span>
+      </div>
+      <div className="module-intro">
+        <span className="section-index">BUILD 01</span>
+        <div>
+          <h3>贯穿项目:笔记与消息服务</h3>
+          <p>
+            创建与查询笔记,校验请求,逐段输出消息,建立实时连接。数据暂存在内存中,重启后清空。
+          </p>
+        </div>
+      </div>
+      <section id="path">
+        <div className="section-heading">
+          <h2>学习路径</h2>
+          <span>每节:概念 → 实战 → 验收</span>
+        </div>
+        <div className="curriculum">
+          {phases.map((p, i) => (
+            <div className="phase" key={p.name}>
+              <div className="phase-heading">
+                <span className="phase-marker">
+                  {String(i + 1).padStart(2, '0')}
+                </span>
+                <div>
+                  <h3>{p.name}</h3>
+                  <p>{p.description}</p>
+                </div>
+                <span className="phase-range">{p.range}</span>
+              </div>
+              <div className="lesson-rows">
+                {lessons.slice(p.from, p.to).map((l) => (
+                  <LocalLink
+                    key={l.id}
+                    lesson={l.id}
+                    navigate={navigate}
+                    className="lesson-row"
+                  >
+                    <span className="lesson-number">{l.id}</span>
+                    <div>
+                      <strong>{l.title}</strong>
+                      <p>{l.summary}</p>
+                    </div>
+                    {l.demo && <span className="interactive-badge">交互</span>}
+                    <span className="lesson-duration">{l.minutes} 分</span>
+                    <ChevronRight size={15} />
+                  </LocalLink>
+                ))}
+              </div>
+            </div>
+          ))}
+        </div>
+      </section>
+      <section id="communication">
+        <div className="section-heading">
+          <h2>HTTP、SSE 与 WebSocket 通信过程</h2>
+          <LocalLink lesson="11" navigate={navigate} className="quiet-link">
+            进入第 11 节 <ArrowUpRight size={14} />
+          </LocalLink>
+        </div>
+        <ProtocolDemo />
+        <p className="section-caption">
+          REST 描述接口设计约定;SSE 和 WebSocket
+          描述不同的通信方式。流式响应本身仍然可以基于 HTTP。
+        </p>
+      </section>
+      <section id="project">
+        <div className="section-heading">
+          <h2>示例项目接口</h2>
+          <span>api-lab / 本地运行</span>
+        </div>
+        <p>
+          一个可以通过浏览器、命令行和测试代码调用的 FastAPI
+          服务。消息由固定逻辑生成,运行示例不需要模型 API Key。
+        </p>
+        <Table className="api-table">
+          <TableHeader>
+            <TableRow>
+              <TableHead>入口</TableHead>
+              <TableHead>可观察的结果</TableHead>
+              <TableHead>对应课时</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {[
+              ['GET /health', '返回服务状态', '01'],
+              ['GET / POST /notes', '查询与创建笔记', '05—06'],
+              [
+                'GET / PUT / DELETE /notes/{id}',
+                '读取、完整替换与删除',
+                '06—07',
+              ],
+              ['GET /private', '校验 X-API-Key 请求头', '08'],
+                ['GET /slow', '并发等待 I/O', '09'],
+                ['GET /upstream-demo', '调用上游接口并处理超时', '10'],
+              ['POST /generate', '一次返回完整 JSON', '11'],
+              ['GET /stream', '连续收到 token,最后收到 done', '12—13'],
+              ['WS /ws', '收发 echo 与 ping / pong 消息', '14—15'],
+            ].map(([path, result, id]) => (
+              <TableRow key={path}>
+                <TableCell>
+                  <code>{path}</code>
+                </TableCell>
+                <TableCell>{result}</TableCell>
+                <TableCell className="mono">{id}</TableCell>
+              </TableRow>
+            ))}
+          </TableBody>
+        </Table>
+        <div className="resource-strip">
+          <FileCode2 size={25} />
+          <div>
+            <h3>配套代码与浏览器客户端</h3>
+            <p>包含 README、版本依赖、接口测试与 web-client.html。</p>
+          </div>
+          <a href="/downloads/api-lab.zip" download className="secondary-link">
+            下载 ZIP <Download size={15} />
+          </a>
+        </div>
+      </section>
+      <section id="ready">
+        <div className="section-heading">
+          <h2>模块完成标准</h2>
+          <span>用实际结果判断是否掌握</span>
+        </div>
+        <ul className="check-list">
+          {[
+            '能从空目录启动服务,并用 /docs 或 curl 发出请求。',
+            '能完成笔记的创建、查询、修改与删除,区分 201、204、401、404、422。',
+            '能解释 await 等待期间另一个请求为什么可以被处理。',
+            '能逐段显示 SSE 数据,在完成或取消时释放连接。',
+            '能建立 WebSocket 连接,收发消息并处理断开。',
+            '能运行测试,独立定位参数、方法、跨域与连接问题。',
+          ].map((s) => (
+            <li key={s}>
+              <Check size={16} />
+              {s}
+            </li>
+          ))}
+        </ul>
+        <div className="next-module">
+          <span>后续模块</span>
+          <strong>数据库开发</strong>
+          <span className="soon">待本模块完善后编写</span>
+        </div>
+      </section>
+    </>
+  );
+}
+
+function PendingModule({
+  name,
+  index,
+  navigate,
+}: {
+  name: string;
+  index: number;
+  navigate: Navigate;
+}) {
+  return (
+    <div className="pending-module" id="reserved">
+      <div className="eyebrow">
+        MODULE {String(index).padStart(2, '0')} <span /> 课程目录
+      </div>
+      <h1 tabIndex={-1}>
+        {name}
+        <span className="title-dot">.</span>
+      </h1>
+      <span className="status-label">待编写</span>
+      <p className="lead">
+        该模块已保留课程入口。
+        <br />
+        API 服务开发模块完善后,再补充本模块的课件、实战与视频。
+      </p>
+      <LocalLink navigate={navigate} className="primary-link">
+        返回 API 服务开发 <ArrowRight size={16} />
+      </LocalLink>
+    </div>
+  );
+}
+
+function CodeBlock({
+  code,
+  lang = 'text',
+  filename,
+}: {
+  code: string;
+  lang?: string;
+  filename?: string;
+}) {
+  const [copied, setCopied] = useState(false);
+  const [error, setError] = useState(false);
+  useEffect(() => {
+    if (!copied && !error) return;
+    const t = setTimeout(() => {
+      setCopied(false);
+      setError(false);
+    }, 2200);
+    return () => clearTimeout(t);
+  }, [copied, error]);
+  const copy = async () => {
+    try {
+      await navigator.clipboard.writeText(code);
+      setCopied(true);
+    } catch {
+      setError(true);
+    }
+  };
+  const highlight = (line: string) =>
+    line
+      .split(
+        /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|#[^\n]*|\b(?:from|import|async|await|def|return|class|if|else|elif|for|in|while|try|except|finally|raise|with|as|yield|True|False|None|const|let|new|function|throw)\b)/g,
+      )
+      .map((part, i) => (
+        <span
+          key={i}
+          className={
+            part.startsWith('#')
+              ? 'syntax-comment'
+              : /^['"]/.test(part)
+                ? 'syntax-string'
+                : /^(from|import|async|await|def|return|class|if|else|elif|for|in|while|try|except|finally|raise|with|as|yield|True|False|None|const|let|new|function|throw)$/.test(
+                      part,
+                    )
+                  ? 'syntax-keyword'
+                  : undefined
+          }
+        >
+          {part}
+        </span>
+      ));
+  return (
+    <div className="code-block">
+      <div className="code-toolbar">
+        <span>
+          <FileCode2 size={14} />
+          {filename || lang}
+        </span>
+        <button onClick={copy} className="copy-button" aria-label="复制代码">
+          {copied ? <Check size={13} /> : <Copy size={13} />}
+          <span aria-live="polite">
+            {copied ? '已复制' : error ? '请选择代码手动复制' : '复制'}
+          </span>
+        </button>
+      </div>
+      <pre>
+        <code>
+          {code.split('\n').map((line, i) => (
+            <span className="code-line" key={i}>
+              <span className="line-number" aria-hidden="true">
+                {i + 1}
+              </span>
+              <span>{highlight(line) || ' '}</span>
+              {'\n'}
+            </span>
+          ))}
+        </code>
+      </pre>
+    </div>
+  );
+}
+function VideoBlock({ id }: { id: string }) {
+  const video = videos[id];
+  const valid = video && /^(https?:\/\/|\/(?!\/))/.test(video.url);
+  return (
+    <section id="video" className="video-section">
+      {valid ? (
+        video.type === 'file' ? (
+          <div className="video-player">
+            <video controls preload="metadata" src={video.url} playsInline>
+              <a href={video.url}>在浏览器打开课程视频</a>
+            </video>
+            <a href={video.url} target="_blank" rel="noreferrer">
+              无法播放时,直接打开视频 <ExternalLink size={14} />
+            </a>
+          </div>
+        ) : (
+          <a
+            href={video.url}
+            target="_blank"
+            rel="noreferrer"
+            className="video-pending"
+          >
+            <VideoIcon size={23} />
+            <div>
+              <strong>{video.title || '观看本节课程视频'}</strong>
+              <span>在课程视频平台打开</span>
+            </div>
+            <ArrowUpRight size={20} />
+          </a>
+        )
+      ) : (
+        <div className="video-pending">
+          <VideoIcon size={23} />
+          <div>
+            <strong>本节视频待录制</strong>
+            <span>可先按下方课件完成实战步骤。</span>
+          </div>
+          <span className="status-label">课件已就绪</span>
+        </div>
+      )}
+    </section>
+  );
+}
+function LessonPage({
+  lesson: l,
+  navigate,
+}: {
+  lesson: Lesson;
+  navigate: Navigate;
+}) {
+  const index = lessons.findIndex((x) => x.id === l.id);
+  return (
+    <article key={l.id} className="lesson-article">
+      <LocalLink navigate={navigate} className="back-link">
+        <ArrowLeft size={14} />
+        API 服务开发 · 全部课时
+      </LocalLink>
+      <div className="eyebrow">
+        LESSON {l.id} / 16 <span />
+        {l.group}
+      </div>
+      <h1 className="lesson-title" tabIndex={-1}>
+        {l.title}
+      </h1>
+      <p className="lead">{l.summary}</p>
+      <div className="meta-row">
+        <span>
+          <Clock3 size={13} />
+          {l.minutes} 分钟建议录制
+        </span>
+        <span>实战课件</span>
+        <span>{l.demo ? '含交互演示' : '含代码与练习'}</span>
+      </div>
+      <section id="objectives" className="objectives">
+        <h2>本节目标</h2>
+        <ul>
+          {l.objectives.map((o, i) => (
+            <li key={o}>
+              <span>{String(i + 1).padStart(2, '0')}</span>
+              {o}
+            </li>
+          ))}
+        </ul>
+      </section>
+      <VideoBlock id={l.id} />
+      <section id="concepts">
+        <div className="section-heading">
+          <h2>必要概念</h2>
+          <span>先明确代码在做什么</span>
+        </div>
+        {l.concepts.map((c, i) => (
+          <div className="concept" key={c.title}>
+            <h3>
+              <span>{i + 1}.</span>
+              {c.title}
+            </h3>
+            <p>{c.body}</p>
+          </div>
+        ))}
+        {l.demo === 'http' ? (
+          <HttpDemo />
+        ) : l.demo === 'async' ? (
+          <AsyncDemo />
+        ) : l.demo === 'protocol' ? (
+          <ProtocolDemo />
+        ) : null}
+      </section>
+      <section id="steps">
+        <div className="section-heading">
+          <h2>动手实现</h2>
+          <span>{l.steps.length} 个步骤</span>
+        </div>
+        {l.steps.map((s, i) => (
+          <div className="practice-step" key={s.title}>
+            <div className="practice-title">
+              <span>{String(i + 1).padStart(2, '0')}</span>
+              <h3>{s.title}</h3>
+            </div>
+            <p>{s.body}</p>
+            {s.code && (
+              <CodeBlock code={s.code} lang={s.lang} filename={s.filename} />
+            )}
+          </div>
+        ))}
+      </section>
+      <section id="checks">
+        <div className="section-heading">
+          <h2>验收结果</h2>
+          <span>检查响应与运行行为</span>
+        </div>
+        <ul className="check-list">
+          {l.checks.map((c) => (
+            <li key={c}>
+              <Check size={16} />
+              {c}
+            </li>
+          ))}
+        </ul>
+      </section>
+      <section id="pitfalls">
+        <div className="section-heading">
+          <h2>常见错误</h2>
+          <span>现象 → 原因 → 处理</span>
+        </div>
+        <div className="pitfall-list">
+          {l.pitfalls.map((p) => (
+            <div className="pitfall" key={p.symptom}>
+              <h3>{p.symptom}</h3>
+              <p>
+                <strong>原因</strong>
+                {p.cause}
+              </p>
+              <p>
+                <strong>处理</strong>
+                {p.fix}
+              </p>
+            </div>
+          ))}
+        </div>
+      </section>
+      <section id="exercise" className="exercise">
+        <div className="section-heading">
+          <h2>扩展练习</h2>
+          <span>课后练习</span>
+        </div>
+        <p>{l.exercise.task}</p>
+        <details>
+          <summary>
+            查看提示 <ChevronRight size={15} />
+          </summary>
+          <p>{l.exercise.hint}</p>
+        </details>
+        <details>
+          <summary>
+            查看参考答案 <ChevronRight size={15} />
+          </summary>
+          <div className="answer-text">{l.exercise.answer}</div>
+        </details>
+      </section>
+      <section id="references">
+        <div className="section-heading">
+          <h2>技术文档</h2>
+          <span>对照原始定义与框架用法</span>
+        </div>
+        <div className="reference-links">
+          {l.refs.map((r) => (
+            <a key={r.url} href={r.url} target="_blank" rel="noreferrer">
+              {r.label}
+              <ArrowUpRight size={15} />
+            </a>
+          ))}
+        </div>
+      </section>
+      <div className="lesson-pagination">
+        {index > 0 ? (
+          <LocalLink navigate={navigate} lesson={lessons[index - 1].id}>
+            <span>
+              <ArrowLeft size={13} />
+              上一节
+            </span>
+            <strong>{lessons[index - 1].title}</strong>
+          </LocalLink>
+        ) : (
+          <LocalLink navigate={navigate}>
+            <span>
+              <ArrowLeft size={13} />
+              模块概览
+            </span>
+            <strong>查看完整学习路径</strong>
+          </LocalLink>
+        )}
+        {index < lessons.length - 1 ? (
+          <LocalLink
+            navigate={navigate}
+            lesson={lessons[index + 1].id}
+            className="next"
+          >
+            <span>
+              下一节
+              <ArrowRight size={13} />
+            </span>
+            <strong>{lessons[index + 1].title}</strong>
+          </LocalLink>
+        ) : (
+          <LocalLink navigate={navigate} className="next">
+            <span>
+              本模块课件结束
+              <Check size={13} />
+            </span>
+            <strong>返回模块完成标准</strong>
+          </LocalLink>
+        )}
+      </div>
+    </article>
+  );
+}

+ 407 - 0
app/demos.tsx

@@ -0,0 +1,407 @@
+'use client';
+import { useEffect, useState } from 'react';
+import {
+  ArrowRight,
+  ArrowLeftRight,
+  Laptop,
+  Server,
+  Play,
+  RotateCcw,
+  SkipForward,
+  Check,
+  Pause,
+} from 'lucide-react';
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Button } from '@/components/ui/button';
+
+const protocols = {
+  http: {
+    name: '普通 HTTP',
+    hint: '一次请求,返回一个完整 JSON 响应。',
+    steps: [
+      '客户端发送 POST /generate',
+      '服务端处理;客户端等待完整结果',
+      '完整 JSON 到达,客户端显示结果',
+      '本次响应结束;底层连接可以复用',
+    ],
+    packets: [
+      'POST /generate\n{"prompt":"解释 API"}',
+      '正在生成完整结果…',
+      'HTTP/1.1 200 OK\nContent-Type: application/json\n\n{"text":"API 是程序之间约定的调用接口。"}',
+      '响应已完成',
+    ],
+    labels: ['请求', '等待结果', '完整响应', '完成'],
+  },
+  sse: {
+    name: 'SSE 流式响应',
+    hint: '一次 HTTP 请求,服务端在同一个响应里连续发送事件。',
+    steps: [
+      '客户端发送 GET /stream,等待事件',
+      '服务端发送第一个 token 事件',
+      '服务端继续发送 token,客户端追加显示',
+      '服务端发送 done;示例客户端主动关闭 EventSource',
+    ],
+    packets: [
+      'GET /stream?prompt=hello\nAccept: text/event-stream',
+      'event: token\ndata: {"text":"API 是"}\n\n',
+      'event: token\ndata: {"text":"程序之间的接口。"}\n\n',
+      'event: done\ndata: {}\n\n',
+    ],
+    labels: ['建立请求', '第 1 个事件', '第 2 个事件', 'done / close'],
+  },
+  ws: {
+    name: 'WebSocket',
+    hint: '握手完成后,客户端和服务端都可以独立发送消息。',
+    steps: [
+      'WebSocket 通过握手建立连接',
+      '客户端发送 hello',
+      '服务端回复 echo 消息;连接继续保持',
+      '客户端发送 ping,服务端回复 pong;仍可继续通信',
+    ],
+    packets: [
+      'new WebSocket("ws://127.0.0.1:8000/ws")',
+      '客户端 → 服务端\nhello',
+      '服务端 → 客户端\n{"type":"echo","text":"hello"}',
+      '客户端 → 服务端: ping\n服务端 → 客户端: {"type":"pong"}',
+    ],
+    labels: ['连接建立', '客户端发消息', '服务端发消息', '双向持续通信'],
+  },
+};
+export function ProtocolDemo() {
+  const [mode, setMode] = useState<keyof typeof protocols>('sse');
+  const [step, setStep] = useState(-1);
+  const [playing, setPlaying] = useState(false);
+  const data = protocols[mode];
+  useEffect(() => {
+    if (!playing) return;
+    const id = window.setTimeout(() => {
+      const next = Math.min(step + 1, 3);
+      setStep(next);
+      if (next === 3) setPlaying(false);
+    }, 950);
+    return () => window.clearTimeout(id);
+  }, [playing, step]);
+  const reset = () => {
+    setPlaying(false);
+    setStep(-1);
+  };
+  return (
+    <div className="demo">
+      <div className="demo-top">
+        <span className="demo-label">通信过程演示</span>
+        <span className="simulation">浏览器内模拟 · 不发送网络请求</span>
+      </div>
+      <Tabs
+        value={mode}
+        onValueChange={(v) => {
+          reset();
+          setMode(v as keyof typeof protocols);
+        }}
+      >
+        <TabsList className="protocol-tabs">
+          {Object.entries(protocols).map(([key, value]) => (
+            <TabsTrigger value={key} key={key}>
+              {value.name}
+            </TabsTrigger>
+          ))}
+        </TabsList>
+      </Tabs>
+      <p className="demo-description">{data.hint}</p>
+      <div className="connection-scene">
+        <div className="endpoint">
+          <Laptop size={26} />
+          <strong>客户端</strong>
+          <span>Browser / Python</span>
+        </div>
+        <div className={`connection-track ${step >= 0 ? 'active' : ''}`}>
+          <span>{step < 0 ? '等待开始' : data.labels[step]}</span>
+          <div className="wire">
+            {mode === 'ws' ? (
+              <ArrowLeftRight size={20} />
+            ) : (
+              <ArrowRight
+                size={20}
+                className={step > 0 ? 'return-arrow' : ''}
+              />
+            )}
+          </div>
+          <small>
+            {mode === 'http'
+              ? 'application/json'
+              : mode === 'sse'
+                ? 'text/event-stream'
+                : 'ws:// · 双向消息'}
+          </small>
+        </div>
+        <div className="endpoint">
+          <Server size={25} />
+          <strong>API 服务</strong>
+          <span>FastAPI / Uvicorn</span>
+        </div>
+      </div>
+      <div className="demo-result" aria-live="polite">
+        <span className="step-indicator">
+          {step < 0 ? 'READY' : `0${step + 1} / 04`}
+        </span>
+        <p>
+          {step < 0 ? '点击播放或单步,观察消息的发送顺序。' : data.steps[step]}
+        </p>
+      </div>
+      <pre className="packet">
+        <code>{step < 0 ? '// 消息将在这里显示' : data.packets[step]}</code>
+      </pre>
+      <div className="demo-controls">
+        <Button
+          onClick={() => {
+            if (step === 3) setStep(-1);
+            setPlaying(!playing);
+          }}
+          variant="outline"
+        >
+          {playing ? <Pause /> : <Play />}
+          {playing ? '暂停' : '播放过程'}
+        </Button>
+        <Button
+          variant="ghost"
+          disabled={step === 3 || playing}
+          onClick={() => setStep((s) => s + 1)}
+        >
+          <SkipForward />
+          单步
+        </Button>
+        <Button variant="ghost" onClick={reset} aria-label="重置通信演示">
+          <RotateCcw />
+          重置
+        </Button>
+        <span className="step-dots" aria-hidden="true">
+          {[0, 1, 2, 3].map((s) => (
+            <i className={s <= step ? 'on' : ''} key={s} />
+          ))}
+        </span>
+      </div>
+    </div>
+  );
+}
+
+const requestParts = [
+  {
+    label: '请求行',
+    code: 'POST /notes HTTP/1.1',
+    description:
+      'POST 是请求方法,/notes 是资源路径,HTTP/1.1 是协议版本。方法与路径共同决定匹配哪个路由。',
+  },
+  {
+    label: '请求头',
+    code: 'Host: 127.0.0.1:8000\nContent-Type: application/json\nAccept: application/json',
+    description:
+      'Content-Type 描述发送的正文格式;Accept 表示希望接收的响应格式。它们属于请求元数据。',
+  },
+  {
+    label: '请求体',
+    code: '{"title":"第一条笔记","content":"hello","pinned":false}',
+    description:
+      'JSON 是传输格式。FastAPI 读取正文后,由 Pydantic 按字段类型和约束进行校验,再把数据交给函数。',
+  },
+];
+export function HttpDemo() {
+  const [part, setPart] = useState(0);
+  const [stage, setStage] = useState(-1);
+  const [invalid, setInvalid] = useState(false);
+  const steps = ['匹配路由', '校验输入', '执行函数', '序列化响应'];
+  const done = stage === 3 || (invalid && stage === 1);
+  return (
+    <div className="demo">
+      <div className="demo-top">
+        <span className="demo-label">拆开一次 HTTP 请求</span>
+        <span className="simulation">请求模型演示</span>
+      </div>
+      <Tabs value={String(part)} onValueChange={(v) => setPart(Number(v))}>
+        <TabsList className="protocol-tabs">
+          {requestParts.map((p, i) => (
+            <TabsTrigger value={String(i)} key={p.label}>
+              {p.label}
+            </TabsTrigger>
+          ))}
+        </TabsList>
+      </Tabs>
+      <pre className="packet">
+        <code>
+          {invalid && part === 2
+            ? '{"content":"hello","pinned":false}'
+            : requestParts[part].code}
+        </code>
+      </pre>
+      <p className="demo-description">{requestParts[part].description}</p>
+      <div className="request-path">
+        {steps.map((s, i) => (
+          <div key={s} className={stage >= i ? 'reached' : ''}>
+            <span>{stage > i ? <Check size={13} /> : i + 1}</span>
+            <small>{s}</small>
+            {i < 3 && <ArrowRight size={13} />}
+          </div>
+        ))}
+      </div>
+      <Tabs
+        value={invalid ? 'invalid' : 'valid'}
+        onValueChange={(v) => {
+          setInvalid(v === 'invalid');
+          setStage(-1);
+        }}
+      >
+        <TabsList>
+          <TabsTrigger value="valid">合法请求</TabsTrigger>
+          <TabsTrigger value="invalid">缺少 title</TabsTrigger>
+        </TabsList>
+      </Tabs>
+      <div className="demo-result" aria-live="polite">
+        <p>
+          {stage < 0
+            ? '单步查看服务端如何处理这个请求。'
+            : invalid && stage === 1
+              ? '422 Unprocessable Entity:缺少 title;路由函数没有执行。'
+              : stage === 3
+                ? '201 Created:返回新笔记的 JSON,响应中包含 id。'
+                : `当前:${steps[stage]}。继续查看下一步。`}
+        </p>
+      </div>
+      <div className="demo-controls">
+        <Button
+          variant="outline"
+          disabled={done}
+          onClick={() => setStage((s) => s + 1)}
+        >
+          <SkipForward />
+          {stage < 0 ? '发送请求' : '下一步'}
+        </Button>
+        <Button variant="ghost" onClick={() => setStage(-1)}>
+          <RotateCcw />
+          重置
+        </Button>
+      </div>
+    </div>
+  );
+}
+
+export function AsyncDemo() {
+  const [tick, setTick] = useState(0);
+  const [playing, setPlaying] = useState(false);
+  useEffect(() => {
+    if (!playing) return;
+    const id = setTimeout(() => {
+      const next = Math.min(6, tick + 1);
+      setTick(next);
+      if (next === 6) setPlaying(false);
+    }, 750);
+    return () => clearTimeout(id);
+  }, [playing, tick]);
+  return (
+    <div className="demo">
+      <div className="demo-top">
+        <span className="demo-label">两个请求,各等待 3 秒 I/O</span>
+        <span className="simulation">简化时间线 · 忽略其他开销</span>
+      </div>
+      <p className="demo-description">
+        假设两个请求同时到达同一个事件循环。阻塞调用占用线程;可等待的 I/O
+        会让出事件循环,让另一个请求先开始等待。
+      </p>
+      <div className="async-lanes">
+        {[
+          {
+            label: '在 async 路由中阻塞',
+            total: 6,
+            rows: [
+              { n: 'A', start: 0 },
+              { n: 'B', start: 3 },
+            ],
+          },
+          {
+            label: '使用 await 等待 I/O',
+            total: 3,
+            rows: [
+              { n: 'A', start: 0 },
+              { n: 'B', start: 0 },
+            ],
+          },
+        ].map((l) => (
+          <div key={l.label}>
+            <div className="lane-label">
+              <strong>{l.label}</strong>
+              <span>总耗时约 {l.total}s</span>
+            </div>
+            {l.rows.map((r) => (
+              <div className="timeline-row" key={r.n}>
+                <span>{r.n}</span>
+                <div className="timeline">
+                  {Array.from({ length: 6 }, (_, i) => (
+                    <i
+                      key={i}
+                      className={
+                        i >= r.start && i < r.start + 3
+                          ? tick > i
+                            ? 'elapsed'
+                            : 'pending'
+                          : ''
+                      }
+                    >
+                      {i >= r.start && i < r.start + 3
+                        ? tick > i
+                          ? 'I/O'
+                          : '等待'
+                        : ''}
+                    </i>
+                  ))}
+                </div>
+                <span>
+                  {tick >= r.start + 3 ? (
+                    <Check size={14} />
+                  ) : tick >= r.start ? (
+                    '…'
+                  ) : (
+                    '—'
+                  )}
+                </span>
+              </div>
+            ))}
+          </div>
+        ))}
+      </div>
+      <div className="demo-controls">
+        <Button
+          variant="outline"
+          onClick={() => {
+            if (tick === 6) setTick(0);
+            setPlaying(!playing);
+          }}
+        >
+          {playing ? <Pause /> : <Play />}
+          {playing ? '暂停' : '播放时间线'}
+        </Button>
+        <Button
+          variant="ghost"
+          disabled={tick === 6 || playing}
+          onClick={() => setTick((t) => t + 1)}
+        >
+          <SkipForward />
+          +1 秒
+        </Button>
+        <Button
+          variant="ghost"
+          onClick={() => {
+            setPlaying(false);
+            setTick(0);
+          }}
+          aria-label="重置异步演示"
+        >
+          <RotateCcw />
+        </Button>
+        <span className="time-readout" aria-live="polite">
+          t = {tick}s
+        </span>
+      </div>
+      <p className="demo-footnote">
+        这里比较 I/O 等待。CPU 密集计算不会因为添加 async 而变快;FastAPI 的普通
+        def 路由会在线程池中执行。
+      </p>
+    </div>
+  );
+}

+ 1766 - 0
app/globals.css

@@ -0,0 +1,1766 @@
+@import 'tailwindcss';
+@import 'tw-animate-css';
+@import 'shadcn/tailwind.css';
+@custom-variant dark (&:is(.dark *));
+@theme inline {
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --color-card: var(--card);
+  --color-card-foreground: var(--foreground);
+  --color-popover: var(--background);
+  --color-popover-foreground: var(--foreground);
+  --color-primary: var(--primary);
+  --color-primary-foreground: #fff;
+  --color-secondary: var(--muted);
+  --color-secondary-foreground: var(--foreground);
+  --color-muted: var(--muted);
+  --color-muted-foreground: var(--muted-foreground);
+  --color-accent: var(--accent);
+  --color-accent-foreground: var(--primary);
+  --color-destructive: #b74435;
+  --color-border: var(--border);
+  --color-input: var(--border);
+  --color-ring: var(--primary);
+  --color-sidebar: var(--sidebar);
+  --color-sidebar-foreground: var(--foreground);
+  --color-sidebar-primary: var(--primary);
+  --color-sidebar-primary-foreground: #fff;
+  --color-sidebar-accent: var(--accent);
+  --color-sidebar-accent-foreground: var(--primary);
+  --color-sidebar-border: var(--border);
+  --color-sidebar-ring: var(--primary);
+  --font-sans: var(--font-geist-sans);
+  --font-mono: var(--font-geist-mono);
+  --radius-sm: 4px;
+  --radius-md: 6px;
+  --radius-lg: 8px;
+  --radius-xl: 12px;
+}
+:root {
+  --background: #fff;
+  --foreground: #242c2a;
+  --card: #fff;
+  --primary: #167457;
+  --muted: #f4f6f5;
+  --muted-foreground: #65716c;
+  --accent: #eaf4ef;
+  --border: #e4e9e6;
+  --sidebar: #f7f9f8;
+  --radius: 8px;
+}
+* {
+  box-sizing: border-box;
+}
+html {
+  scroll-behavior: smooth;
+  scroll-padding-top: 98px;
+}
+body {
+  margin: 0;
+  font-family:
+    var(--font-geist-sans), 'Microsoft YaHei', 'PingFang SC', sans-serif;
+  color: var(--foreground);
+  background: var(--background);
+  font-size: 16px;
+  line-height: 1.75;
+}
+a {
+  color: inherit;
+  text-decoration: none;
+}
+button,
+input,
+select,
+textarea {
+  font: inherit;
+}
+button,
+a {
+  -webkit-tap-highlight-color: transparent;
+}
+button:not(:disabled),
+summary {
+  cursor: pointer;
+}
+button:disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+button:focus-visible,
+a:focus-visible,
+summary:focus-visible {
+  outline: 2px solid var(--primary);
+  outline-offset: 4px;
+}
+p {
+  margin: 0 0 1em;
+}
+h1,
+h2,
+h3,
+h4 {
+  font-weight: 600;
+  letter-spacing: -0.03em;
+}
+h1 {
+  font-size: 44px;
+  line-height: 1.3;
+  margin: 13px 0 20px;
+}
+h2 {
+  font-size: 24px;
+  line-height: 1.45;
+  margin: 0;
+}
+h3 {
+  font-size: 17px;
+  margin: 0 0 7px;
+  line-height: 1.5;
+}
+h4 {
+  font-size: 16px;
+  margin: 0 0 5px;
+}
+code,
+pre,
+.mono {
+  font-family: var(--font-geist-mono), Consolas, monospace;
+}
+p code,
+li code {
+  font-size: 0.88em;
+  padding: 2px 5px;
+  background: var(--muted);
+  border: 1px solid var(--border);
+  border-radius: 4px;
+}
+section {
+  scroll-margin-top: 85px;
+  margin: 42px 0;
+}
+svg {
+  flex-shrink: 0;
+}
+.brand {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 23px 16px 29px;
+  font-size: 17px;
+  font-weight: 650;
+  line-height: 1.45;
+}
+.brand-symbol {
+  width: 36px;
+  height: 36px;
+  border-radius: 8px;
+  background: #245b47;
+  color: #fff;
+  display: grid;
+  place-items: center;
+}
+.brand-sub {
+  display: block;
+  font-size: 12px;
+  font-weight: 400;
+  letter-spacing: 1px;
+  color: #76817b;
+  margin-top: 4px;
+}
+.nav-caption {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 12px;
+  color: #7d8981;
+  padding: 6px 25px 16px;
+}
+.nav-caption span {
+  font-family: var(--font-geist-mono);
+  font-size: 10px;
+  letter-spacing: 0.7px;
+}
+.module-nav {
+  padding: 0 12px;
+}
+.module-link {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  width: 100%;
+  padding: 11px 13px;
+  margin: 3px 0;
+  border-radius: 6px;
+  font-size: 14px;
+  color: #626e66;
+  text-align: left;
+  background: transparent;
+  border: 0;
+}
+.module-link:hover {
+  background: #edf1ee;
+}
+.module-link.selected {
+  background: #e5f0e9;
+  color: #246248;
+  font-weight: 600;
+}
+.module-link > span:nth-child(2) {
+  flex: 1;
+}
+.nav-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: var(--primary);
+}
+.soon {
+  font-size: 11px;
+  color: #7d8882;
+  font-weight: 400;
+}
+.sidebar-note {
+  display: flex;
+  gap: 11px;
+  align-items: flex-start;
+  color: #55665b;
+  padding: 19px 15px;
+  font-size: 13px;
+  border-top: 1px solid var(--border);
+}
+.sidebar-note small {
+  display: block;
+  color: #89928d;
+  margin-top: 4px;
+  font-size: 12px;
+}
+.workspace {
+  min-width: 0;
+  flex: 1;
+}
+.topbar {
+  height: 69px;
+  border-bottom: 1px solid var(--border);
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 44px;
+  position: sticky;
+  top: 0;
+  background: rgba(255, 255, 255, 0.96);
+  z-index: 20;
+  gap: 20px;
+}
+.breadcrumbs {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  font-size: 13px;
+  color: #7b857e;
+  min-width: 0;
+}
+.breadcrumbs strong {
+  font-weight: 500;
+  color: #45544a;
+}
+.quiet-link {
+  display: inline-flex;
+  align-items: center;
+  gap: 7px;
+  font-size: 13px;
+  color: #67736b;
+  white-space: nowrap;
+}
+.quiet-link:hover {
+  color: var(--primary);
+}
+.document-grid {
+  display: grid;
+  grid-template-columns: minmax(0, 860px) 160px;
+  gap: 55px;
+  max-width: 1210px;
+  margin: auto;
+  padding: 49px 48px 70px;
+}
+.document-grid > main {
+  min-width: 0;
+}
+.eyebrow {
+  display: flex;
+  align-items: center;
+  gap: 11px;
+  font-size: 11px;
+  letter-spacing: 1.4px;
+  color: #657b6b;
+  font-family: var(--font-geist-mono), 'Microsoft YaHei', sans-serif;
+}
+.eyebrow span {
+  width: 17px;
+  height: 1px;
+  background: #a1b2a8;
+}
+.title-dot {
+  color: var(--primary);
+  margin-left: 3px;
+}
+.lead {
+  color: #657269;
+  font-size: 17px;
+  line-height: 1.9;
+  margin-bottom: 22px;
+}
+.meta-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 20px;
+  font-size: 12px;
+  color: #778178;
+}
+.meta-row > span {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.meta-row > span + span:before {
+  content: '';
+  width: 3px;
+  height: 3px;
+  background: #adb6af;
+  margin-right: 7px;
+  border-radius: 50%;
+}
+.module-intro {
+  display: flex;
+  gap: 24px;
+  border-top: 1px solid var(--border);
+  border-bottom: 1px solid var(--border);
+  padding: 24px 0;
+  margin-top: 30px;
+}
+.module-intro p {
+  font-size: 14px;
+  color: #6e7a71;
+  margin: 0;
+  line-height: 1.85;
+}
+.section-index {
+  white-space: nowrap;
+  color: var(--primary);
+  font-size: 13px;
+  font-family: var(--font-geist-mono);
+  padding-top: 2px;
+}
+.section-heading {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20px;
+  margin-bottom: 19px;
+}
+.section-heading > span {
+  color: #89938c;
+  font-size: 12px;
+}
+.path-row {
+  display: flex;
+  align-items: center;
+  gap: 24px;
+  padding: 21px 2px;
+  border-bottom: 1px solid var(--border);
+}
+.path-row > div {
+  flex: 1;
+}
+.path-row p {
+  font-size: 14px;
+  color: #78857b;
+  margin: 0;
+}
+.path-row svg {
+  color: #8d9c91;
+}
+.path-row:hover h3 {
+  color: var(--primary);
+}
+.path-number {
+  color: #7e9285;
+  font-size: 13px;
+  font-family: var(--font-geist-mono);
+}
+.toc {
+  font-size: 13px;
+  position: sticky;
+  top: 111px;
+  align-self: start;
+  padding-top: 4px;
+  color: #7a867d;
+}
+.toc > p {
+  font-size: 12px;
+  color: #3f5345;
+  margin-bottom: 15px;
+}
+.toc > a {
+  display: block;
+  border-left: 1px solid var(--border);
+  padding: 7px 0 7px 15px;
+}
+.toc > a:hover {
+  color: var(--primary);
+  border-color: var(--primary);
+}
+.toc-note {
+  margin-top: 35px;
+  padding-top: 22px;
+  border-top: 1px solid var(--border);
+  font-size: 12px;
+  line-height: 2;
+}
+.toc-note p {
+  margin-top: 12px;
+}
+.tiny-label {
+  font-size: 11px;
+  letter-spacing: 1px;
+  color: #8b968e;
+}
+.code {
+  font-size: 13px;
+  line-height: 1.85;
+  padding: 22px 24px;
+  background: #f5f7f6;
+  color: #2e493c;
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  overflow: auto;
+  tab-size: 4;
+  white-space: pre;
+  margin: 15px 0 23px;
+}
+.code code {
+  font: inherit;
+}
+@media (min-width: 1440px) {
+  .document-grid {
+    padding-top: 60px;
+    gap: 70px;
+  }
+  .topbar {
+    padding: 0 52px;
+  }
+}
+@media (max-width: 1200px) {
+  .toc {
+    display: none;
+  }
+  .document-grid {
+    grid-template-columns: minmax(0, 1fr);
+    padding: 38px 38px 60px;
+    max-width: 920px;
+  }
+  .topbar {
+    padding: 0 32px;
+  }
+}
+@media (max-width: 767px) {
+  .document-grid {
+    padding: 30px 23px 60px;
+  }
+  .topbar {
+    padding: 0 17px;
+    height: 60px;
+  }
+  .quiet-link {
+    font-size: 12px;
+  }
+  .breadcrumbs {
+    gap: 9px;
+    font-size: 12px;
+  }
+  h1 {
+    font-size: 33px;
+  }
+  h2 {
+    font-size: 22px;
+  }
+  .lead {
+    font-size: 16px;
+  }
+  .module-intro {
+    gap: 16px;
+  }
+  .path-row {
+    gap: 16px;
+  }
+  .section-heading {
+    align-items: flex-start;
+  }
+  .section-heading > span {
+    max-width: 125px;
+    text-align: right;
+  }
+  .meta-row {
+    font-size: 11px;
+    gap: 8px 11px;
+  }
+}
+.skip-link {
+  position: fixed;
+  top: -80px;
+  left: 20px;
+  padding: 10px 20px;
+  background: var(--primary);
+  color: white;
+  z-index: 100;
+  border-radius: 6px;
+}
+.skip-link:focus {
+  top: 12px;
+}
+h1:focus {
+  outline: none;
+}
+.topbar-actions {
+  display: flex;
+  align-items: center;
+  gap: 23px;
+}
+.topbar-actions button {
+  font-size: 12px;
+  color: #5e7064;
+}
+.overview-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+  margin-top: 27px;
+}
+.primary-link,
+.secondary-link {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 9px;
+  border-radius: 6px;
+  padding: 9px 15px;
+  font-size: 13px;
+  line-height: 1.6;
+  transition: background 0.16s;
+}
+.primary-link {
+  background: var(--primary);
+  color: #fff;
+  border: 1px solid var(--primary);
+}
+.primary-link:hover {
+  background: #105b43;
+}
+.secondary-link {
+  background: #fff;
+  color: #596a60;
+  border: 1px solid var(--border);
+}
+.secondary-link:hover {
+  background: #f2f6f3;
+}
+.material-state {
+  font-size: 11px;
+  color: #89928d;
+  margin-left: 3px;
+}
+.phase {
+  margin: 23px 0 29px;
+}
+.phase-heading {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  padding: 13px 16px;
+  background: #f4f7f5;
+  border: 1px solid var(--border);
+  border-radius: 7px;
+}
+.phase-heading > div {
+  flex: 1;
+}
+.phase-heading h3 {
+  font-size: 15px;
+  margin: 0 0 3px;
+}
+.phase-heading p {
+  font-size: 12px;
+  color: #7a867d;
+  margin: 0;
+}
+.phase-marker {
+  font-size: 16px;
+  color: #829c8d;
+  font-family: var(--font-geist-mono);
+  align-self: flex-start;
+  line-height: 1.6;
+}
+.phase-range {
+  font-size: 11px;
+  color: #8d9891;
+  white-space: nowrap;
+  font-family: var(--font-geist-mono);
+}
+.lesson-rows {
+  padding-left: 13px;
+}
+.lesson-row {
+  display: flex;
+  gap: 15px;
+  align-items: center;
+  padding: 17px 8px;
+  border-bottom: 1px solid #edf0ee;
+}
+.lesson-row > div {
+  flex: 1;
+  min-width: 0;
+}
+.lesson-row strong {
+  font-size: 14px;
+  font-weight: 500;
+  display: block;
+  line-height: 1.6;
+}
+.lesson-row p {
+  font-size: 12px;
+  margin: 4px 0 0;
+  color: #7d8880;
+  line-height: 1.65;
+}
+.lesson-number {
+  color: #94a297;
+  font-family: var(--font-geist-mono);
+  font-size: 12px;
+  min-width: 22px;
+}
+.lesson-row:hover {
+  background: #fbfdfb;
+}
+.lesson-row:hover strong {
+  color: var(--primary);
+}
+.lesson-row > svg {
+  color: #9ba99f;
+}
+.lesson-duration {
+  font-size: 11px;
+  white-space: nowrap;
+  color: #87948b;
+}
+.interactive-badge {
+  border: 1px solid #d8e7de;
+  border-radius: 4px;
+  background: #f7fbf8;
+  color: #5d8d72;
+  font-size: 10px;
+  padding: 0 5px;
+  white-space: nowrap;
+}
+.section-caption {
+  font-size: 13px;
+  color: #728176;
+  line-height: 1.8;
+  margin-top: 16px;
+}
+.api-table {
+  margin: 22px 0;
+  font-size: 13px;
+}
+.api-table th {
+  color: #7d8d81;
+  font-size: 12px;
+  font-weight: 400;
+  background: #f7f9f7;
+}
+.api-table td {
+  padding: 13px 10px;
+  white-space: normal;
+  color: #56685b;
+  font-size: 13px;
+  line-height: 1.8;
+}
+.api-table th:last-child,
+.api-table td:last-child {
+  white-space: nowrap;
+}
+.api-table code {
+  font-size: 11px;
+  color: #35694c;
+}
+.resource-strip {
+  padding: 23px;
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  background: #f7faf8;
+  border: 1px solid var(--border);
+  border-radius: 7px;
+  margin-top: 20px;
+  color: #56705e;
+}
+.resource-strip > div {
+  flex: 1;
+}
+.resource-strip h3 {
+  font-size: 15px;
+}
+.resource-strip p {
+  font-size: 12px;
+  margin: 0;
+  color: #7b8b80;
+}
+.resource-strip a {
+  white-space: nowrap;
+  font-size: 12px;
+}
+.check-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+.check-list li {
+  display: flex;
+  gap: 13px;
+  padding: 10px 0;
+  font-size: 15px;
+  line-height: 1.85;
+}
+.check-list svg {
+  margin-top: 6px;
+  color: var(--primary);
+}
+.next-module {
+  border-top: 1px solid var(--border);
+  padding-top: 25px;
+  margin-top: 35px;
+  display: flex;
+  gap: 14px;
+  align-items: center;
+  font-size: 13px;
+}
+.next-module > span:first-child {
+  color: #819086;
+  font-size: 12px;
+}
+.next-module strong {
+  font-weight: 500;
+}
+.next-module .soon {
+  margin-left: auto;
+}
+.page-footer {
+  display: flex;
+  justify-content: space-between;
+  gap: 20px;
+  font-size: 11px;
+  color: #8b968e;
+  margin: 0 44px;
+  padding: 21px 0;
+  border-top: 1px solid var(--border);
+}
+.toc-download {
+  display: flex !important;
+  align-items: center;
+  gap: 8px;
+  border: 0 !important;
+  padding-left: 0 !important;
+  color: #527c60 !important;
+  margin-top: 15px;
+}
+.pending-module {
+  min-height: 67vh;
+  padding: 20px 0;
+}
+.pending-module .status-label {
+  display: inline-block;
+  margin: 8px 0 25px;
+}
+.pending-module .lead {
+  margin-bottom: 32px;
+}
+.status-label {
+  font-size: 11px;
+  color: #718275;
+  background: #eef3ef;
+  border: 1px solid #e2e9e4;
+  padding: 3px 8px;
+  border-radius: 4px;
+  white-space: nowrap;
+}
+.lesson-nav {
+  margin: 18px 0 10px;
+  padding-top: 22px;
+  border-top: 1px solid var(--border);
+}
+.lesson-nav .nav-caption {
+  padding-bottom: 6px;
+}
+.lesson-nav-group {
+  font-size: 11px;
+  color: #8b998e;
+  padding: 15px 24px 7px;
+  margin: 0;
+}
+.lesson-nav-link {
+  display: flex;
+  gap: 9px;
+  padding: 8px 15px 8px 24px;
+  font-size: 12px;
+  color: #6f7f73;
+  line-height: 1.6;
+  border-left: 2px solid transparent;
+}
+.lesson-nav-link span {
+  font-family: var(--font-geist-mono);
+  font-size: 10px;
+  color: #a0aaa3;
+  padding-top: 2px;
+}
+.lesson-nav-link.current {
+  color: var(--primary);
+  background: #edf4ef;
+  border-left-color: var(--primary);
+}
+.lesson-nav-link:hover {
+  color: var(--primary);
+  background: #edf4ef;
+}
+.back-link {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  font-size: 12px;
+  color: #798b7d;
+  margin-bottom: 26px;
+  width: fit-content;
+}
+.back-link:hover {
+  color: var(--primary);
+}
+.lesson-title {
+  font-size: 35px;
+  margin-top: 15px;
+  line-height: 1.5;
+}
+.lesson-article > .lead {
+  font-size: 16px;
+}
+.objectives {
+  background: #f4f8f5;
+  border-left: 3px solid #a7c5af;
+  padding: 21px 23px;
+  margin: 29px 0 18px;
+  border-radius: 0 6px 6px 0;
+}
+.objectives h2 {
+  font-size: 15px;
+  margin-bottom: 12px;
+}
+.objectives ul {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+.objectives li {
+  display: flex;
+  gap: 12px;
+  font-size: 14px;
+  line-height: 1.8;
+  padding: 5px 0;
+}
+.objectives li span {
+  color: #81a48d;
+  font-size: 11px;
+  font-family: var(--font-geist-mono);
+  padding-top: 3px;
+}
+.video-section {
+  margin: 18px 0 37px;
+}
+.video-pending {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  padding: 23px 20px;
+  border: 1px solid var(--border);
+  border-radius: 7px;
+  color: #85958a;
+  background: #fafcfb;
+}
+.video-pending > div {
+  flex: 1;
+}
+.video-pending strong {
+  display: block;
+  font-weight: 500;
+  font-size: 14px;
+  color: #677d6e;
+}
+.video-pending div span {
+  display: block;
+  font-size: 12px;
+  margin-top: 5px;
+  color: #94a197;
+}
+.video-player video {
+  width: 100%;
+  aspect-ratio: 16/9;
+  background: #18291f;
+  border-radius: 8px;
+}
+.video-player > a {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  font-size: 12px;
+  margin-top: 8px;
+  color: var(--primary);
+}
+.concept {
+  margin: 23px 0 26px;
+}
+.concept h3 {
+  display: flex;
+  gap: 9px;
+  font-size: 17px;
+}
+.concept h3 > span {
+  color: #90a394;
+  font-size: 14px;
+  padding-top: 2px;
+}
+.concept p,
+.practice-step > p {
+  font-size: 15px;
+  line-height: 1.95;
+  color: #5e6e63;
+  white-space: pre-line;
+  overflow-wrap: anywhere;
+}
+.practice-step {
+  margin: 26px 0 34px;
+}
+.practice-title {
+  display: flex;
+  align-items: flex-start;
+  gap: 13px;
+  margin: 15px 0 12px;
+}
+.practice-title > span {
+  font-family: var(--font-geist-mono);
+  font-size: 12px;
+  width: 27px;
+  height: 27px;
+  display: grid;
+  place-items: center;
+  flex-shrink: 0;
+  border: 1px solid #dce8df;
+  background: #f5faf6;
+  border-radius: 5px;
+  color: #5f846c;
+}
+.practice-title h3 {
+  margin: 0;
+  padding-top: 1px;
+  font-size: 17px;
+}
+.code-block {
+  border: 1px solid #dfe7e1;
+  background: #f7f9f7;
+  border-radius: 8px;
+  overflow: hidden;
+  margin: 18px 0 25px;
+}
+.code-toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 15px;
+  border-bottom: 1px solid #e5ece6;
+  background: #f0f4f1;
+  font-size: 12px;
+  color: #718376;
+}
+.code-toolbar > span {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  font-family: var(--font-geist-mono);
+}
+.copy-button {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 11px;
+  color: #6e8174;
+  background: transparent;
+  border: 0;
+  padding: 0 4px;
+}
+.copy-button:hover {
+  color: var(--primary);
+}
+.code-block pre {
+  font-size: 12px;
+  line-height: 1.9;
+  overflow: auto;
+  margin: 0;
+  padding: 19px 21px 21px 0;
+  color: #364d3d;
+  tab-size: 4;
+}
+.code-line {
+  white-space: pre;
+}
+.line-number {
+  display: inline-block;
+  min-width: 46px;
+  padding: 0 13px 0 8px;
+  text-align: right;
+  user-select: none;
+  pointer-events: none;
+  color: #b0bbb3;
+  font-size: 11px;
+}
+.syntax-string {
+  color: #477856;
+}
+.syntax-keyword {
+  color: #247081;
+  font-weight: 500;
+}
+.syntax-comment {
+  color: #93a398;
+}
+.pitfall {
+  padding: 22px 0;
+  border-bottom: 1px solid var(--border);
+}
+.pitfall:first-child {
+  padding-top: 4px;
+}
+.pitfall h3 {
+  font-size: 16px;
+  margin-bottom: 12px;
+}
+.pitfall p {
+  display: flex;
+  gap: 13px;
+  font-size: 14px;
+  color: #708074;
+  line-height: 1.9;
+  margin-bottom: 7px;
+  white-space: pre-line;
+}
+.pitfall p strong {
+  font-size: 11px;
+  font-weight: 400;
+  background: #f0f4f1;
+  padding: 2px 5px;
+  color: #7b9282;
+  flex-shrink: 0;
+  align-self: flex-start;
+  border-radius: 3px;
+}
+.exercise {
+  padding: 26px;
+  border: 1px solid #dce7df;
+  border-radius: 8px;
+  background: #fbfdfb;
+}
+.exercise h2 {
+  font-size: 22px;
+}
+.exercise > p {
+  font-size: 15px;
+  line-height: 1.9;
+  color: #586d5e;
+}
+.exercise details {
+  border-top: 1px solid #e5ede7;
+  padding-top: 15px;
+  margin-top: 15px;
+  font-size: 14px;
+}
+.exercise summary {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  list-style: none;
+  color: #63886e;
+}
+.exercise summary::-webkit-details-marker {
+  display: none;
+}
+.exercise details[open] summary svg {
+  transform: rotate(90deg);
+}
+.exercise details p,
+.answer-text {
+  white-space: pre-wrap;
+  overflow-wrap: anywhere;
+  margin: 17px 0 4px;
+  font-size: 14px;
+  line-height: 1.9;
+  color: #647a6b;
+}
+.reference-links {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 12px;
+}
+.reference-links a {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 14px;
+  color: #4f8662;
+  border-bottom: 1px solid transparent;
+}
+.reference-links a:hover {
+  border-color: #91b59c;
+}
+.lesson-pagination {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 24px;
+  padding-top: 30px;
+  margin-top: 44px;
+  border-top: 1px solid var(--border);
+}
+.lesson-pagination a {
+  padding: 4px 0;
+}
+.lesson-pagination span {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  color: #8a9b8e;
+  font-size: 12px;
+  margin-bottom: 9px;
+}
+.lesson-pagination strong {
+  font-weight: 500;
+  font-size: 14px;
+  color: #56775f;
+  line-height: 1.7;
+}
+.lesson-pagination a.next {
+  text-align: right;
+}
+.lesson-pagination a.next span {
+  justify-content: flex-end;
+}
+.presentation [data-slot='sidebar'],
+.presentation .toc,
+.presentation [data-slot='sidebar-trigger'] {
+  display: none;
+}
+.presentation .document-grid {
+  max-width: 1150px;
+  grid-template-columns: minmax(0, 1fr);
+  padding: 48px 65px;
+}
+.presentation .code-block pre {
+  font-size: 16px;
+  line-height: 1.9;
+}
+.presentation .concept p,
+.presentation .practice-step > p,
+.presentation .check-list li {
+  font-size: 19px;
+  line-height: 2;
+}
+.presentation .lesson-title {
+  font-size: 44px;
+}
+.demo {
+  border: 1px solid #dbe7df;
+  border-radius: 9px;
+  background: #fcfefd;
+  overflow: hidden;
+  padding: 21px 24px;
+  margin: 22px 0;
+}
+.demo-top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 14px;
+  margin-bottom: 18px;
+}
+.demo-label {
+  font-size: 13px;
+  font-weight: 500;
+  color: #3b684b;
+}
+.simulation {
+  font-size: 10px;
+  color: #8da494;
+}
+.protocol-tabs {
+  padding: 4px !important;
+  background: #eef4f0 !important;
+  border: 1px solid #e6ede7 !important;
+  height: auto !important;
+  border-radius: 6px !important;
+  gap: 3px !important;
+}
+.protocol-tabs button {
+  font-size: 12px !important;
+  min-height: 30px !important;
+  padding: 5px 13px !important;
+  border-radius: 4px !important;
+}
+.protocol-tabs button[data-active] {
+  color: #276643 !important;
+  box-shadow: 0 1px 3px #234e3c15 !important;
+  background: #fff !important;
+}
+.demo-description {
+  font-size: 13px;
+  color: #6f8879;
+  line-height: 1.9;
+  margin: 18px 0;
+}
+.connection-scene {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 22px;
+  padding: 23px 15px 27px;
+}
+.endpoint {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  min-width: 108px;
+  gap: 5px;
+  color: #7c9887;
+}
+.endpoint > svg {
+  margin-bottom: 7px;
+}
+.endpoint strong {
+  font-size: 13px;
+  font-weight: 500;
+  color: #426f51;
+}
+.endpoint span {
+  font-family: var(--font-geist-mono);
+  font-size: 10px;
+  color: #94a89b;
+  white-space: nowrap;
+}
+.connection-track {
+  flex: 1;
+  max-width: 280px;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  gap: 9px;
+  font-size: 11px;
+  color: #819c8b;
+}
+.connection-track small {
+  font-family: var(--font-geist-mono);
+  font-size: 10px;
+  color: #9aad9f;
+  white-space: nowrap;
+}
+.wire {
+  height: 22px;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+.wire::before {
+  content: '';
+  border-top: 1px dashed #bacfc1;
+  position: absolute;
+  width: 100%;
+}
+.wire svg {
+  background: #fcfefd;
+  z-index: 1;
+  width: 36px;
+  color: #92b69e;
+}
+.active .wire::before {
+  border-top: 1px solid #7ca88a;
+}
+.active .wire svg {
+  color: #2d8450;
+}
+.return-arrow {
+  transform: rotate(180deg);
+}
+.demo-result {
+  display: flex;
+  gap: 11px;
+  align-items: flex-start;
+  background: #eff6f1;
+  border: 1px solid #e4eee6;
+  border-radius: 5px;
+  padding: 12px 14px;
+  color: #698775;
+  min-height: 50px;
+  margin: 12px 0;
+}
+.demo-result p {
+  font-size: 12px;
+  line-height: 1.9;
+  margin: 0;
+}
+.step-indicator {
+  font-family: var(--font-geist-mono);
+  font-size: 10px;
+  white-space: nowrap;
+  color: #548a66;
+  margin-top: 2px;
+}
+.packet {
+  background: #f4f8f5;
+  border: 1px solid #e5ede7;
+  border-radius: 5px;
+  padding: 16px;
+  font-size: 12px;
+  line-height: 1.85;
+  color: #5c7f66;
+  overflow: auto;
+  min-height: 116px;
+  margin: 14px 0;
+  white-space: pre-wrap;
+  overflow-wrap: anywhere;
+}
+.demo-controls {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  margin-top: 17px;
+}
+.demo-controls button {
+  font-size: 12px !important;
+  gap: 5px !important;
+  color: #4c7d5c;
+  min-height: 33px;
+}
+.demo-controls button svg {
+  width: 13px !important;
+  height: 13px !important;
+}
+.step-dots {
+  margin-left: auto;
+  display: flex;
+  gap: 5px;
+}
+.step-dots i {
+  width: 5px;
+  height: 5px;
+  border-radius: 50%;
+  background: #d9e5dd;
+}
+.step-dots .on {
+  background: #6aa37c;
+}
+.request-path {
+  display: flex;
+  gap: 6px;
+  justify-content: space-between;
+  margin: 26px 0;
+}
+.request-path > div {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  color: #9bab9f;
+}
+.request-path > div > span {
+  width: 20px;
+  height: 20px;
+  display: grid;
+  place-items: center;
+  border: 1px solid #dbe7df;
+  border-radius: 50%;
+  font-size: 11px;
+}
+.request-path small {
+  font-size: 11px;
+}
+.request-path .reached {
+  color: var(--primary);
+}
+.request-path .reached > span {
+  background: #e3f0e7;
+  border-color: #a9c8b3;
+}
+.async-lanes {
+  margin: 25px 0;
+}
+.async-lanes > div + div {
+  margin-top: 23px;
+}
+.lane-label {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 12px;
+  margin-bottom: 12px;
+  color: #5d7d69;
+}
+.lane-label strong {
+  font-weight: 500;
+}
+.lane-label > span {
+  font-size: 11px;
+  color: #95a99c;
+}
+.timeline-row {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+  margin: 7px 0;
+  font-family: var(--font-geist-mono);
+  font-size: 11px;
+  color: #7d9687;
+}
+.timeline-row > span {
+  width: 15px;
+}
+.timeline {
+  display: grid;
+  grid-template-columns: repeat(6, 1fr);
+  gap: 4px;
+  flex: 1;
+}
+.timeline i {
+  height: 28px;
+  background: #f0f5f1;
+  border-radius: 3px;
+  font-size: 9px;
+  font-style: normal;
+  display: grid;
+  place-items: center;
+  border: 1px solid #e5eee8;
+  color: #91ab9c;
+}
+.timeline i.pending {
+  border: 1px dashed #aac8b5;
+  background: #f7fbf8;
+}
+.timeline i.elapsed {
+  background: #a4cdb2;
+  border-color: #91bda0;
+  color: #356145;
+}
+.time-readout {
+  margin-left: auto;
+  font-family: var(--font-geist-mono);
+  font-size: 12px;
+  color: #6b8c77;
+  white-space: nowrap;
+}
+.demo-footnote {
+  font-size: 11px;
+  color: #91a899;
+  margin: 19px 0 0;
+  border-top: 1px solid #e7eee9;
+  padding-top: 15px;
+}
+@media (max-width: 1300px) {
+  .material-state {
+    display: none;
+  }
+  .topbar-actions {
+    gap: 12px;
+  }
+  .document-grid {
+    gap: 37px;
+  }
+}
+@media (max-width: 1000px) {
+  .docs-link {
+    display: none;
+  }
+  .lesson-row p {
+    display: none;
+  }
+  .demo {
+    padding: 19px;
+  }
+  .simulation {
+    font-size: 9px;
+  }
+  .phase-range {
+    display: none;
+  }
+  .endpoint {
+    min-width: 95px;
+  }
+  .connection-scene {
+    gap: 12px;
+    padding: 20px 0;
+  }
+  .resource-strip {
+    flex-wrap: wrap;
+  }
+  .resource-strip > a {
+    margin-left: 41px;
+  }
+}
+@media (max-width: 767px) {
+  .lesson-title {
+    font-size: 28px;
+    line-height: 1.6;
+  }
+  .lesson-nav-link {
+    font-size: 14px;
+  }
+  .lesson-nav-group {
+    font-size: 12px;
+  }
+  .module-link {
+    font-size: 15px;
+  }
+  .topbar-actions button {
+    font-size: 11px;
+    padding: 0 5px;
+  }
+  .topbar-actions button svg {
+    width: 14px !important;
+  }
+  .phase-heading {
+    gap: 12px;
+  }
+  .phase-heading p {
+    font-size: 11px;
+  }
+  .lesson-row {
+    gap: 9px;
+    padding: 16px 2px;
+  }
+  .lesson-row strong {
+    font-size: 13px;
+  }
+  .lesson-duration {
+    font-size: 10px;
+  }
+  .interactive-badge {
+    font-size: 9px;
+  }
+  .lesson-rows {
+    padding-left: 4px;
+  }
+  .primary-link,
+  .secondary-link {
+    font-size: 12px;
+  }
+  .overview-actions {
+    gap: 9px;
+  }
+  .phase-marker {
+    font-size: 14px;
+  }
+  .resource-strip {
+    padding: 18px;
+  }
+  .next-module {
+    flex-wrap: wrap;
+  }
+  .next-module .soon {
+    margin-left: 0;
+  }
+  .objectives {
+    padding: 18px;
+  }
+  .objectives li {
+    font-size: 13px;
+  }
+  .video-pending {
+    padding: 19px 15px;
+  }
+  .video-pending .status-label {
+    display: none;
+  }
+  .concept p,
+  .practice-step > p {
+    font-size: 15px;
+  }
+  .exercise {
+    padding: 22px 18px;
+  }
+  .code-block pre {
+    font-size: 11px;
+  }
+  .line-number {
+    min-width: 33px;
+    padding: 0 8px 0 4px;
+  }
+  .lesson-pagination {
+    gap: 16px;
+  }
+  .lesson-pagination strong {
+    font-size: 13px;
+  }
+  .demo {
+    padding: 16px;
+  }
+  .demo-top {
+    align-items: flex-start;
+    gap: 8px;
+  }
+  .demo-label {
+    font-size: 12px;
+  }
+  .simulation {
+    font-size: 9px;
+    max-width: 120px;
+    text-align: right;
+  }
+  .protocol-tabs {
+    max-width: 100%;
+  }
+  .protocol-tabs button {
+    font-size: 11px !important;
+    padding: 4px 8px !important;
+  }
+  .endpoint {
+    min-width: 65px;
+  }
+  .endpoint span {
+    font-size: 8px;
+  }
+  .endpoint strong {
+    font-size: 12px;
+  }
+  .connection-scene {
+    gap: 9px;
+  }
+  .connection-track {
+    font-size: 10px;
+  }
+  .connection-track small {
+    font-size: 8px;
+    white-space: normal;
+    text-align: center;
+  }
+  .demo-result {
+    padding: 10px;
+  }
+  .demo-result p {
+    font-size: 11px;
+  }
+  .demo-controls {
+    gap: 1px;
+  }
+  .demo-controls button {
+    font-size: 11px !important;
+    padding: 0 8px !important;
+  }
+  .packet {
+    font-size: 11px;
+    padding: 12px;
+  }
+  .request-path {
+    gap: 3px;
+    flex-wrap: wrap;
+  }
+  .request-path > div {
+    gap: 4px;
+  }
+  .request-path small {
+    font-size: 10px;
+  }
+  .request-path > div > svg {
+    display: none;
+  }
+  .demo-footnote {
+    font-size: 11px;
+  }
+  .page-footer {
+    margin: 0 23px;
+    font-size: 10px;
+    flex-direction: column;
+    gap: 3px;
+  }
+  .presentation .document-grid {
+    padding: 30px 23px;
+  }
+  .presentation .lesson-title {
+    font-size: 31px;
+  }
+  .presentation .code-block pre {
+    font-size: 13px;
+  }
+  .presentation .concept p,
+  .presentation .practice-step > p,
+  .presentation .check-list li {
+    font-size: 17px;
+  }
+}
+@media print {
+  [data-slot='sidebar'],
+  .topbar,
+  .toc,
+  .page-footer,
+  .demo-controls,
+  .video-section,
+  .lesson-pagination,
+  .overview-actions,
+  .copy-button,
+  .back-link {
+    display: none !important;
+  }
+  .document-grid,
+  .presentation .document-grid {
+    display: block;
+    max-width: none;
+    padding: 0;
+  }
+  .workspace {
+    width: 100%;
+  }
+  .code-block pre {
+    white-space: pre-wrap;
+    overflow: visible;
+    font-size: 10px;
+  }
+  .code-line {
+    white-space: pre-wrap;
+  }
+  .code-block {
+    break-inside: avoid;
+  }
+  .lesson-title {
+    font-size: 27px;
+  }
+  .concept p,
+  .practice-step > p {
+    font-size: 12px;
+  }
+  .exercise details:not([open]) {
+    display: none;
+  }
+  section {
+    margin: 25px 0;
+  }
+}
+@media (prefers-reduced-motion: reduce) {
+  html {
+    scroll-behavior: auto;
+  }
+  *,
+  *::before,
+  *::after {
+    animation-duration: 0.01ms !important;
+    transition-duration: 0.01ms !important;
+  }
+}

+ 20 - 0
app/layout.tsx

@@ -0,0 +1,20 @@
+import type { Metadata } from 'next';
+import { Geist, Geist_Mono } from 'next/font/google';
+import './globals.css';
+const sans = Geist({ variable: '--font-geist-sans', subsets: ['latin'] });
+const mono = Geist_Mono({ variable: '--font-geist-mono', subsets: ['latin'] });
+export const metadata: Metadata = {
+  title: 'API 服务开发 · Agent 开发补修课',
+  description:
+    '面向具有 Python 基础的开发者,用 FastAPI 完成 REST 接口、异步调用、SSE 流式响应与 WebSocket 通信。配套实战步骤、代码与练习。',
+  icons: { icon: '/favicon.svg' },
+};
+export default function RootLayout({
+  children,
+}: Readonly<{ children: React.ReactNode }>) {
+  return (
+    <html lang="zh-CN">
+      <body className={`${sans.variable} ${mono.variable}`}>{children}</body>
+    </html>
+  );
+}

+ 22 - 0
app/lesson-types.ts

@@ -0,0 +1,22 @@
+export type Lesson = {
+  id: string;
+  group: string;
+  title: string;
+  summary: string;
+  minutes: number;
+  objectives: string[];
+  concepts: { title: string; body: string }[];
+  steps: {
+    title: string;
+    body: string;
+    code?: string;
+    lang?: string;
+    filename?: string;
+  }[];
+  checks: string[];
+  pitfalls: { symptom: string; cause: string; fix: string }[];
+  exercise: { task: string; hint: string; answer: string };
+  refs: { label: string; url: string }[];
+  demo?: 'http' | 'async' | 'protocol';
+};
+export type Video = { type: 'file' | 'link'; url: string; title?: string };

+ 1622 - 0
app/lessons.json

@@ -0,0 +1,1622 @@
+[
+  {
+    "id": "01",
+    "group": "起步与 HTTP",
+    "title": "启动第一个 FastAPI 服务",
+    "summary": "建立 api-lab 项目,让 /health 返回 JSON,并通过交互文档发出第一次请求。",
+    "minutes": 20,
+    "objectives": [
+      "为项目创建独立的 Python 环境",
+      "解释 main:app、端口与 --reload 的作用",
+      "分别在浏览器和 /docs 验证同一个接口"
+    ],
+    "concepts": [
+      {
+        "title": "服务进程与路由函数",
+        "body": "普通脚本运行结束后会退出。API 服务由 Uvicorn 持续监听本机端口,收到 HTTP 请求后调用 FastAPI 中匹配的方法与路径。@app.get('/health') 注册的是 GET /health;函数的返回值会被序列化为 JSON 响应。"
+      },
+      {
+        "title": "虚拟环境决定从哪里导入依赖",
+        "body": "同一台电脑可以有多个 Python。虚拟环境把当前项目的解释器入口和第三方包放在 .venv 中。用 python -m pip 安装,能使 pip 与当前 python 指向同一环境;编辑器也应选择 .venv 中的解释器。"
+      },
+      {
+        "title": "开发服务器的几个参数",
+        "body": "main:app 表示从当前目录的 main.py 导入 app 对象。127.0.0.1 是本机地址,8000 是默认端口。--reload 会在代码变化后重启开发进程,重启也会清空之后课程中的内存数据。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "建立目录和虚拟环境",
+        "body": "准备 Python 3.11 或更新版本。以下两组命令按系统选择一组。激活只影响当前终端;以后重新打开终端时,要先切换到 api-lab 再激活。",
+        "lang": "bash",
+        "code": "# Windows PowerShell\nmkdir api-lab\ncd api-lab\npy -3 -m venv .venv\n.\\.venv\\Scripts\\Activate.ps1\n\n# macOS / Linux\nmkdir api-lab\ncd api-lab\npython3 -m venv .venv\nsource .venv/bin/activate\n\n# 激活后检查解释器路径,结果应包含 api-lab/.venv\npython -c \"import sys; print(sys.executable)\""
+      },
+      {
+        "title": "安装依赖并记录环境",
+        "body": "创建 requirements.txt,写入下面三行。然后在激活后的终端运行 python -m pip install -r requirements.txt。本模块使用 Pydantic v2 的 model_dump() 写法。安装成功后运行 python -m pip freeze > requirements-lock.txt,保留本次录课实际使用的版本;另一个环境可从这个锁定文件安装。 如果直接运行下载包,请使用包内已经锁定版本的 requirements.txt。",
+        "filename": "requirements.txt",
+        "lang": "bash",
+        "code": "fastapi>=0.115,<1\nuvicorn[standard]>=0.30,<1\npydantic>=2,<3"
+      },
+      {
+        "title": "编写健康检查接口",
+        "body": "在 api-lab 根目录创建 main.py。FastAPI 实例只创建一次。路由装饰器紧接在函数定义上方,返回值使用 Python 字典,JSON 编码由框架完成。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from fastapi import FastAPI\n\napp = FastAPI(title=\"API Lab\", version=\"0.1.0\")\n\n@app.get(\"/health\", tags=[\"system\"])\ndef health():\n    return {\"status\": \"ok\"}"
+      },
+      {
+        "title": "启动服务并观察终端",
+        "body": "在 main.py 所在目录运行以下命令。终端保持运行状态是正常现象;看到 Application startup complete 后再访问浏览器。服务运行期间用另一个终端执行请求,停止服务使用 Ctrl+C。",
+        "lang": "bash",
+        "code": "python -m uvicorn main:app --reload\n# 默认访问地址:http://127.0.0.1:8000\n# 8000 被占用时,可加 --port 8001"
+      },
+      {
+        "title": "通过浏览器和交互文档验收",
+        "body": "先打开 http://127.0.0.1:8000/health,确认显示 {\"status\":\"ok\"}。再打开 /docs,展开 GET /health → Try it out → Execute,查看状态码、响应头和响应体。最后把 main.py 中的 ok 改为 ready 并保存,观察终端重载日志和新响应,完成后改回 ok。",
+        "lang": "http",
+        "code": "GET /health HTTP/1.1\nHost: 127.0.0.1:8000\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n{\"status\":\"ok\"}"
+      }
+    ],
+    "checks": [
+      "终端的 sys.executable 路径包含 api-lab/.venv。",
+      "GET /health 返回 200 和 {\"status\":\"ok\"};访问未定义的 / 返回 404,这不表示服务未启动。",
+      "/docs 中出现 GET /health,点击 Execute 后能看到真实响应。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "ModuleNotFoundError: No module named 'fastapi'",
+        "cause": "安装依赖和启动服务使用了不同解释器。",
+        "fix": "激活 .venv,检查 sys.executable,再执行 python -m pip install -r requirements.txt。"
+      },
+      {
+        "symptom": "Could not import module 'main'",
+        "cause": "当前目录没有 main.py,或者文件中存在语法错误。",
+        "fix": "切换到 api-lab 根目录,确认文件名为 main.py,阅读终端中最早出现的 Python 错误。"
+      },
+      {
+        "symptom": "PowerShell 阻止 Activate.ps1 运行",
+        "cause": "当前执行策略限制脚本运行。",
+        "fix": "无需修改全局策略,可直接使用 .\\.venv\\Scripts\\python.exe -m pip install -r requirements.txt,以及 .\\.venv\\Scripts\\python.exe -m uvicorn main:app --reload。"
+      }
+    ],
+    "exercise": {
+      "task": "添加 GET /version,返回 {\"service\":\"api-lab\",\"version\":\"0.1.0\"},并在 /docs 中执行一次。",
+      "hint": "保留已有 app,在另一个函数上方注册新的 GET 路由。",
+      "answer": "在 main.py 末尾添加:\n@app.get('/version')\ndef version():\n    return {'service': 'api-lab', 'version': '0.1.0'}\n\n保存后访问 /version,应返回 200;刷新 /docs 后应同时看到 /health 与 /version。"
+    },
+    "refs": [
+      {
+        "label": "Python:venv",
+        "url": "https://docs.python.org/3/library/venv.html"
+      },
+      {
+        "label": "FastAPI:First Steps",
+        "url": "https://fastapi.tiangolo.com/tutorial/first-steps/"
+      },
+      {
+        "label": "FastAPI:手动运行服务器",
+        "url": "https://fastapi.tiangolo.com/deployment/manually/"
+      }
+    ]
+  },
+  {
+    "id": "02",
+    "group": "起步与 HTTP",
+    "title": "读懂一次 HTTP 请求与响应",
+    "summary": "拆开方法、路径、查询参数、请求头和请求体,亲手发送 GET 与 POST。",
+    "minutes": 18,
+    "demo": "http",
+    "objectives": [
+      "在请求中指出方法、URL、请求头和请求体的位置",
+      "区分 Content-Type、Accept 与 HTTP 状态码",
+      "用 /docs 和命令行复现同一条请求"
+    ],
+    "concepts": [
+      {
+        "title": "请求由几个独立部分组成",
+        "body": "以 POST /notes?source=lesson 为例:POST 是方法,/notes 是路径,source=lesson 是查询参数。请求头携带内容类型等元信息,请求体携带笔记数据。下面的 HTTP/1.1 文本用于辨认字段;浏览器也可能使用其他 HTTP 版本,业务含义一致。"
+      },
+      {
+        "title": "Content-Type 描述实际内容",
+        "body": "请求头 Content-Type: application/json 表示请求体按 JSON 编码;Accept: application/json 表示客户端期望的响应格式。服务器仍按接口定义产生响应,并非只改 Accept 就会自动得到另一种格式。JSON 键和字符串使用双引号,布尔值使用 true / false。"
+      },
+      {
+        "title": "响应状态与业务字段分开读取",
+        "body": "响应先给出 HTTP 状态码,再给出响应头和响应体。200 表示本次 HTTP 操作成功,422 表示请求数据未通过当前接口校验。把 {\"code\":404} 放在一个 200 响应里,不会把 HTTP 状态变成 404。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "观察最小 GET 请求",
+        "body": "保持第 01 课服务运行。在 /docs 执行 /health,同时查看本页的请求拆解演示。把方法、路径、Host 和响应体分别定位;GET /health 不需要请求体。",
+        "lang": "http",
+        "code": "GET /health HTTP/1.1\nHost: 127.0.0.1:8000\nAccept: application/json\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n{\"status\":\"ok\"}"
+      },
+      {
+        "title": "添加临时的 POST 回显接口",
+        "body": "把以下代码追加到 main.py。这个临时接口只读取请求体并返回,不保存笔记,也不宣称创建成功。第 03 课会用带类型的模型替换它。这里先借助 Request 观察 HTTP 数据,异步语法在后续课程集中练习。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from fastapi import Request\n\n@app.post(\"/notes\")\nasync def inspect_note(request: Request):\n    data = await request.json()\n    return {\"received\": data}"
+      },
+      {
+        "title": "准备 JSON 文件并发送 POST",
+        "body": "在 api-lab 中创建 note.json,使用 UTF-8 保存。macOS / Linux 执行 curl -i -X POST http://127.0.0.1:8000/notes -H \"Content-Type: application/json\" --data-binary @note.json;Windows PowerShell 使用 curl.exe,并把 @note.json 写成引号包围的 \"@note.json\"。-i 同时显示响应头。",
+        "filename": "note.json",
+        "lang": "json",
+        "code": "{\n  \"title\": \"first note\",\n  \"content\": \"learn HTTP\",\n  \"pinned\": false\n}"
+      },
+      {
+        "title": "对照原始请求与服务器结果",
+        "body": "命令行应返回 200 和 received 对象。改动 note.json 中的 title 后再次发送,确认响应随之变化。在浏览器开发者工具 Network 中展开 /docs 发出的请求,也能查看 Method、Request Headers、Payload 和 Response。",
+        "lang": "http",
+        "code": "POST /notes HTTP/1.1\nHost: 127.0.0.1:8000\nContent-Type: application/json\n\n{\"title\":\"first note\",\"content\":\"learn HTTP\",\"pinned\":false}\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n{\"received\":{\"title\":\"first note\",\"content\":\"learn HTTP\",\"pinned\":false}}"
+      }
+    ],
+    "checks": [
+      "能说明查询参数属于 URL,请求体位于请求头之后,两者不是同一块数据。",
+      "命令行 POST 返回 200,并在 received 中保留 note.json 的三个字段。",
+      "浏览器直接打开 /notes 会发出 GET;当前只注册了 POST,因此应返回 405 Method Not Allowed。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "PowerShell 提示无法识别 -H 参数",
+        "cause": "使用了旧版 PowerShell 的 curl 别名。",
+        "fix": "明确执行 curl.exe;或者在 /docs 中操作。"
+      },
+      {
+        "symptom": "请求体解析失败",
+        "cause": "把 Python 字典写法当成 JSON,例如使用单引号或 False。",
+        "fix": "使用 note.json 文件,字符串和字段名用双引号,布尔值写 true / false。"
+      },
+      {
+        "symptom": "浏览器地址栏打不开 POST 接口",
+        "cause": "地址栏导航通常发送 GET,没有发送预期的 POST 和 JSON 请求体。",
+        "fix": "使用 curl、/docs 或能够配置 HTTP 方法的客户端。"
+      }
+    ],
+    "exercise": {
+      "task": "把请求体的 title 改成 second note,再发送一次。记录响应的 HTTP 状态码、Content-Type 和 received.title;最后说明 Accept 和 Content-Type 的区别。",
+      "hint": "使用 -i 查看响应头,不要只看最后一行 JSON。",
+      "answer": "状态码为 200;响应 Content-Type 为 application/json;received.title 为 second note。请求 Content-Type 描述这次发送的数据格式,Accept 描述客户端希望接收的格式。"
+    },
+    "refs": [
+      {
+        "label": "RFC 9110:HTTP 消息与内容语义",
+        "url": "https://www.rfc-editor.org/rfc/rfc9110.html#section-6"
+      },
+      {
+        "label": "RFC 8259:JSON 语法",
+        "url": "https://www.rfc-editor.org/rfc/rfc8259"
+      },
+      {
+        "label": "FastAPI:直接使用 Request",
+        "url": "https://fastapi.tiangolo.com/advanced/using-request-directly/"
+      }
+    ]
+  },
+  {
+    "id": "03",
+    "group": "起步与 HTTP",
+    "title": "从路径、查询参数和 JSON 中接收数据",
+    "summary": "为笔记接口声明参数类型,让 FastAPI 完成解析、默认值填充和基础校验。",
+    "minutes": 22,
+    "objectives": [
+      "将 note_id 放在路径中,将筛选条件放在查询参数中",
+      "用 Pydantic 模型接收 JSON 请求体",
+      "通过 422 响应定位错误参数"
+    ],
+    "concepts": [
+      {
+        "title": "路径标识对象,查询参数控制读取方式",
+        "body": "/notes/12 中的 12 表示要操作的笔记;/notes?limit=10&pinned=true 中的参数表示读取条件。参数名出现在路由路径中时,FastAPI 将其识别为路径参数。未出现在路径中的简单类型参数通常来自查询字符串。"
+      },
+      {
+        "title": "类型声明参与运行时解析",
+        "body": "HTTP 路径和查询字符串首先都是文本。note_id: int 要求它能解析为整数;limit: int = 10 提供默认值。无法转换的值会在进入路由函数前产生 422 响应,因此函数里不用重复写 int() 和大量类型判断。"
+      },
+      {
+        "title": "请求模型决定必填与默认值",
+        "body": "继承 BaseModel 的 NoteCreate 声明 JSON 对象的结构。title 没有默认值,所以必须发送;content 默认空字符串,pinned 默认 false。str | None 表示允许 null,不自动表示可以省略;若要允许省略,还需写默认值,例如 = None。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "声明创建笔记的输入模型",
+        "body": "在 main.py 的路由定义之前添加模型。删除第 02 课的 inspect_note 路由函数及其装饰器,随后用第 04 步的新 POST /notes 替换,避免同一方法和路径出现多个处理函数。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from pydantic import BaseModel\n\nclass NoteCreate(BaseModel):\n    title: str\n    content: str = \"\"\n    pinned: bool = False"
+      },
+      {
+        "title": "添加路径参数",
+        "body": "添加 GET /notes/{note_id}。本节先回显解析后的编号,暂不查询存储。依次请求 /notes/12 与 /notes/abc,比较状态码;整数解析失败时,错误位置会包含 path 和 note_id。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "@app.get(\"/notes/{note_id}\")\ndef inspect_note_id(note_id: int):\n    return {\"note_id\": note_id}"
+      },
+      {
+        "title": "添加查询参数和范围",
+        "body": "添加 GET /notes。Annotated 把 Python 类型和 FastAPI 的参数规则写在一起。limit 默认为 10,只允许 1 到 100;pinned 未传时为 None,传 false 时是明确的 False,后续筛选中要区分这两种情况。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from typing import Annotated\nfrom fastapi import Query\n\n@app.get(\"/notes\")\ndef inspect_filters(\n    limit: Annotated[int, Query(ge=1, le=100)] = 10,\n    pinned: bool | None = None,\n):\n    return {\"limit\": limit, \"pinned\": pinned}"
+      },
+      {
+        "title": "用类型模型替换原始请求体",
+        "body": "定义新的 POST /notes。在 /docs 中仅发送 {\"title\":\"first note\"},观察缺省字段被补齐。本节仍是参数练习,不写入存储。model_dump() 把 Pydantic 模型转换为 Python 字典。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "@app.post(\"/notes\")\ndef inspect_note_body(note: NoteCreate):\n    return note.model_dump()"
+      },
+      {
+        "title": "建立有效值和无效值对照",
+        "body": "在 /docs 连续完成以下四次调用,记录状态码与响应中的字段位置。缺少 title 的 POST 应在路由运行前失败。下一课会继续收紧 title 的长度与多余字段规则。",
+        "lang": "http",
+        "code": "GET /notes/12\n# 200: {\"note_id\":12}\n\nGET /notes?limit=5&pinned=false\n# 200: {\"limit\":5,\"pinned\":false}\n\nGET /notes?limit=0\n# 422,错误位置包含 [\"query\",\"limit\"]\n\nPOST /notes\nContent-Type: application/json\n\n{}\n# 422,错误位置包含 [\"body\",\"title\"]"
+      }
+    ],
+    "checks": [
+      "GET /notes/12 返回数字 12,/notes/abc 返回 422。",
+      "GET /notes 返回 {\"limit\":10,\"pinned\":null};?pinned=false 返回 false。",
+      "POST /notes 只传 title 时补齐 content 和 pinned;缺少 title 返回 422。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "把 note_id 放进查询字符串,得到列表而不是单条笔记",
+        "cause": "GET /notes?note_id=12 匹配的是 GET /notes。本课列表路由没有声明 note_id,FastAPI 默认忽略这个额外查询参数,因此返回 200 和列表;它不会匹配 /notes/{note_id}。",
+        "fix": "读取单条笔记时改为 GET /notes/12。先检查方法和路径,再检查查询参数;只有确实没有匹配路由或资源不存在时才会得到相应的 404。"
+      },
+      {
+        "symptom": "写了 title: str | None 后,省略 title 仍然报错",
+        "cause": "允许 null 和允许省略是两个条件。",
+        "fix": "如果业务允许省略,显式写默认值 = None;本例 title 应保持必填字符串。"
+      },
+      {
+        "symptom": "修改 POST /notes 后响应还像上一课",
+        "cause": "旧路由没有移除,重复注册了相同方法和路径。",
+        "fix": "搜索 @app.post('/notes'),只保留当前一个定义,保存并确认服务完成重载。"
+      }
+    ],
+    "exercise": {
+      "task": "为 GET /notes 添加 offset 查询参数,默认 0,只允许非负整数,并把解析结果加进响应。验证 offset=3 与 offset=-1。",
+      "hint": "参考 limit 的 Annotated[int, Query(...)],使用 ge=0。",
+      "answer": "在函数参数中加入 offset: Annotated[int, Query(ge=0)] = 0,并在返回字典中加入 'offset': offset。?offset=3 返回 200 且 offset 为 3;?offset=-1 返回 422,loc 包含 query、offset。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:路径参数",
+        "url": "https://fastapi.tiangolo.com/tutorial/path-params/"
+      },
+      {
+        "label": "FastAPI:查询参数",
+        "url": "https://fastapi.tiangolo.com/tutorial/query-params/"
+      },
+      {
+        "label": "FastAPI:请求体",
+        "url": "https://fastapi.tiangolo.com/tutorial/body/"
+      }
+    ]
+  },
+  {
+    "id": "04",
+    "group": "起步与 HTTP",
+    "title": "用 Pydantic 定义输入和输出契约",
+    "summary": "限制标题长度、拒绝拼错的字段,并通过 response_model 控制客户端能看到的数据。",
+    "minutes": 22,
+    "objectives": [
+      "用 Field 声明范围,用 extra='forbid' 发现字段拼写错误",
+      "分开定义创建输入与接口输出模型",
+      "区分请求校验失败与响应校验失败"
+    ],
+    "concepts": [
+      {
+        "title": "模型把接口规则放到一起",
+        "body": "类型描述数据形状,Field 描述长度或数值约束。本例 title 的长度为 1 到 120。extra='forbid' 会拒绝模型中未声明的字段,能及时发现 pinnd 这样的拼写错误;Pydantic 默认会忽略多余字段,因此需要显式配置。"
+      },
+      {
+        "title": "输入模型与输出模型承担不同职责",
+        "body": "创建时客户端只提交 title、content、pinned;id 由服务端产生。NoteOut 在输入字段之外增加 id。response_model 会校验并筛选普通返回数据,未声明的内部字段不会进入响应;这也会出现在 /docs 的响应结构中。"
+      },
+      {
+        "title": "校验成功不代表全部业务规则成立",
+        "body": "长度为 1 的字符串可能只是一个空格,类型与长度规则不会自动决定标题是否有意义。输入校验失败通常返回 422;如果路由返回的数据不符合输出模型,例如漏掉 id,则属于服务端代码错误,默认返回 500,并在终端记录具体原因。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "收紧 NoteCreate 的输入规则",
+        "body": "用下面定义替换原来的 NoteCreate,放在路由之前。保留其余路由。随后在 /docs 中分别发送空标题和拼错的 pinnd 字段,确认都得到 422。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from pydantic import BaseModel, ConfigDict, Field\n\nclass NoteCreate(BaseModel):\n    model_config = ConfigDict(extra=\"forbid\")\n\n    title: str = Field(min_length=1, max_length=120)\n    content: str = \"\"\n    pinned: bool = False"
+      },
+      {
+        "title": "增加响应模型",
+        "body": "在 NoteCreate 后添加 NoteOut。输出字段单独声明,避免把输入的 extra='forbid' 规则带入本节的输出过滤实验。id 在输出里必填;客户端创建时不能自行指定 id。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "class NoteOut(BaseModel):\n    id: int\n    title: str\n    content: str\n    pinned: bool"
+      },
+      {
+        "title": "用 response_model 约束 POST 响应",
+        "body": "替换 POST /notes 的装饰器和函数。本节的 id=1 仍是演示值,下一阶段才生成真实编号。返回字典故意增加 internal_source,查看 HTTP 响应时该字段应被过滤。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "@app.post(\"/notes\", response_model=NoteOut)\ndef inspect_note_body(note: NoteCreate):\n    return {\n        \"id\": 1,\n        **note.model_dump(),\n        \"internal_source\": \"validation-lesson\",\n    }"
+      },
+      {
+        "title": "分别制造输入与输出错误",
+        "body": "先发送下面的空标题请求,确认 422 的错误位置包含 body、title。再临时删除返回字典中的 id,发送正常请求,观察 500 和终端中的响应校验异常。恢复 id 后再继续;不要把服务端输出错误改成客户端的 422。",
+        "lang": "json",
+        "code": "{\n  \"title\": \"\",\n  \"content\": \"title must not be empty\",\n  \"pinned\": false\n}"
+      },
+      {
+        "title": "检查文档中的契约",
+        "body": "刷新 /docs。展开 POST /notes 的 Schema,检查 title 的 minLength、maxLength,以及 NoteOut 的 id。发送 {\"title\":\"schema check\"} 后应返回下面四个字段。保存一份响应作为后续 CRUD 的验收样本。",
+        "lang": "json",
+        "code": "{\n  \"id\": 1,\n  \"title\": \"schema check\",\n  \"content\": \"\",\n  \"pinned\": false\n}"
+      }
+    ],
+    "checks": [
+      "空 title 或额外 pinnd 字段均返回 422。",
+      "正常创建演示返回 id、title、content、pinned 四个字段,不包含 internal_source。",
+      "故意漏掉输出 id 时出现 500;恢复后为 200,当前尚未设置创建成功的 201。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "空格标题通过了 min_length=1",
+        "cause": "空格也是字符串字符,长度校验不执行去空格。",
+        "fix": "根据业务要求加入 strip_whitespace 或字段校验器,去空格后再检查是否为空。"
+      },
+      {
+        "symptom": "response_model 没有过滤某个自定义响应",
+        "cause": "直接返回 JSONResponse 等 Response 对象时,框架不会按普通返回值继续执行响应模型转换。",
+        "fix": "本节返回字典或模型对象,让 FastAPI 使用 response_model 处理。"
+      },
+      {
+        "symptom": "把模型直接拼到字典时提示类型不匹配",
+        "cause": "Pydantic 模型对象不等于普通字典。",
+        "fix": "需要字典时调用 note.model_dump();不要使用旧教材中的 .dict() 作为本模块标准写法。"
+      }
+    ],
+    "exercise": {
+      "task": "拒绝只有空白的标题,并将 '  first note  ' 保存为 'first note'。保持 120 字符上限。",
+      "hint": "为 title 添加 mode='before' 的 field_validator,先去除字符串两端空白,再交给 Field 长度校验。",
+      "answer": "从 pydantic 导入 field_validator,在 NoteCreate 内添加:\n@field_validator('title', mode='before')\n@classmethod\ndef strip_title(cls, value):\n    return value.strip() if isinstance(value, str) else value\n\n只含空格会变为空字符串并返回 422;'  first note  ' 经解析后为 'first note'。"
+    },
+    "refs": [
+      {
+        "label": "Pydantic:Field 与字段约束",
+        "url": "https://docs.pydantic.dev/latest/concepts/fields/"
+      },
+      {
+        "label": "Pydantic:验证器",
+        "url": "https://docs.pydantic.dev/latest/concepts/validators/"
+      },
+      {
+        "label": "FastAPI:响应模型",
+        "url": "https://fastapi.tiangolo.com/tutorial/response-model/"
+      }
+    ]
+  },
+  {
+    "id": "05",
+    "group": "REST 接口",
+    "title": "设计笔记资源与 HTTP 行为",
+    "summary": "确定集合、单条资源、全量替换和删除的接口约定,再用请求验证方法与状态码。",
+    "minutes": 18,
+    "objectives": [
+      "用资源路径表达笔记集合和单条笔记",
+      "区分 POST、PUT、PATCH、DELETE 的语义",
+      "解释 200、201、204、404、405、422 与幂等性"
+    ],
+    "concepts": [
+      {
+        "title": "先确定资源,再确定操作",
+        "body": "本项目使用 /notes 表示笔记集合,/notes/{note_id} 表示一条笔记。读取用 GET,创建用 POST,完整替换用 PUT,删除用 DELETE。方法和路径共同确定接口,因此 GET /notes 与 POST /notes 可以共用路径。"
+      },
+      {
+        "title": "PUT 的完整替换需要明确输入规则",
+        "body": "本课程的 PUT 要求 title、content、pinned 全部出现,替换指定笔记可编辑字段,id 保持不变。PATCH 通常表达局部更新,本模块暂不实现。若把创建模型直接用于 PUT,省略 content 会触发默认空字符串,容易被误认为保留旧值。"
+      },
+      {
+        "title": "幂等描述重复请求后的目标状态",
+        "body": "重复相同 PUT,目标笔记应保持相同内容;重复 DELETE,目标笔记都处于不存在状态,第一次可以返回 204,第二次可以返回 404。幂等不要求每次响应完全相同。POST 创建通常每次产生新资源,不能假设自动重复发送不会多建一条。"
+      },
+      {
+        "title": "状态码也是接口契约",
+        "body": "读取与更新成功使用 200,创建成功使用 201,删除成功且无响应体使用 204。找不到资源使用 404,路径存在但方法不支持使用 405,输入校验失败使用 422。4xx、5xx 响应不能只靠 JSON 业务字段表达,HTTP 状态也必须正确。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "写下接口清单",
+        "body": "以下是第 06 课要实现的目标契约。当前服务还处于参数练习阶段,不应把回显结果视为已保存。录课时先在纸上或请求文件中列出方法、路径、输入和成功状态,后续逐项实现。",
+        "filename": "notes-contract.http",
+        "lang": "http",
+        "code": "GET    /notes              # 查询集合,200,响应为数组\nPOST   /notes              # 创建,201,响应包含新 id\nGET    /notes/{note_id}    # 查询单条,200 或 404\nPUT    /notes/{note_id}    # 完整替换,200 或 404\nDELETE /notes/{note_id}    # 删除,204 或 404"
+      },
+      {
+        "title": "用已有服务区分 404 与 405",
+        "body": "在 /docs 执行 GET /health,应返回 200。然后命令行发送 POST /health,应返回 405,并检查 Allow 响应头;再 GET /missing-path,应返回 404。Windows 将下面 curl 改为 curl.exe。这两类错误都与 JSON 字段校验无关。",
+        "lang": "bash",
+        "code": "curl -i http://127.0.0.1:8000/health\ncurl -i -X POST http://127.0.0.1:8000/health\ncurl -i http://127.0.0.1:8000/missing-path"
+      },
+      {
+        "title": "为完整替换定义独立输入",
+        "body": "在 main.py 的模型区添加 NoteReplace。三个字段均无默认值,确保调用方明确提交完整内容。在 Python 终端或单独脚本中使用下面示例验证;此时只定义模型,尚未注册 PUT 路由。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "class NoteReplace(BaseModel):\n    model_config = ConfigDict(extra=\"forbid\")\n\n    title: str = Field(min_length=1, max_length=120)\n    content: str\n    pinned: bool\n\n# 在独立 Python 终端中测试:\n# from main import NoteReplace\n# NoteReplace(title=\"updated\", content=\"\", pinned=False)  # 通过\n# NoteReplace(title=\"updated\")  # ValidationError:缺少两项"
+      },
+      {
+        "title": "准备替换与删除的验收请求",
+        "body": "把完整请求体保存为 replace.json。下一课先创建一条笔记,记录真实 id,再执行两次同样 PUT 和两次 DELETE。不要写死假定 id 永远为 1;服务重载会影响当前内存。204 响应后不要继续解析 JSON。",
+        "filename": "replace.json",
+        "lang": "json",
+        "code": "{\n  \"title\": \"updated note\",\n  \"content\": \"replace all editable fields\",\n  \"pinned\": true\n}"
+      }
+    ],
+    "checks": [
+      "GET /health 是 200;POST /health 是 405;不存在的路径是 404。",
+      "NoteReplace 缺少 content 或 pinned 时验证失败,空 content 和 false 本身都合法。",
+      "能解释 DELETE 第二次返回 404 仍然满足幂等,以及 204 为什么不能带 {\"ok\":true}。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "PUT 请求只传 title,意外把 content 清空",
+        "cause": "复用了带默认值的创建模型,未明确全量替换规则。",
+        "fix": "使用所有可编辑字段均必填的 NoteReplace;如果需要局部更新,应另行设计 PATCH。"
+      },
+      {
+        "symptom": "客户端对成功 DELETE 调用 response.json() 后报错",
+        "cause": "204 响应没有响应体。",
+        "fix": "先判断状态码;遇到 204 直接按成功处理,不执行 JSON 解码。"
+      },
+      {
+        "symptom": "网络超时后重复 POST,出现两条笔记",
+        "cause": "客户端没收到响应,并不等于服务器没创建资源。",
+        "fix": "本地实验先查询列表核对;生产创建接口需根据业务设计幂等键等重复提交控制,不能直接假定 POST 可安全重试。"
+      }
+    ],
+    "exercise": {
+      "task": "评审三个接口:GET /delete-note?id=1、PUT /notes/1 只接收 pinned、DELETE /notes/1 返回 200 和 {\"deleted\":true}。给出本课程约定下的修改方案,并说明第三种设计是否违反 HTTP。",
+      "hint": "分清 HTTP 协议允许的行为与课程选择的统一接口约定。",
+      "answer": "第一种改为 DELETE /notes/1,GET 不用于用户请求的数据删除。第二种若只做局部修改应定义 PATCH,或者要求 PUT 传齐 title/content/pinned。第三种返回 200 和结果体在 HTTP 中可以成立;本课程统一选择 204 空体,因此需同时调整状态码和响应内容。"
+    },
+    "refs": [
+      {
+        "label": "RFC 9110:请求方法",
+        "url": "https://www.rfc-editor.org/rfc/rfc9110.html#section-9"
+      },
+      {
+        "label": "RFC 9110:204 No Content",
+        "url": "https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.5"
+      },
+      {
+        "label": "FastAPI:更新请求体",
+        "url": "https://fastapi.tiangolo.com/tutorial/body-updates/"
+      }
+    ]
+  },
+  {
+    "id": "06",
+    "group": "REST 接口",
+    "title": "完成内存版笔记 CRUD",
+    "summary": "把接口契约变成可运行程序,按创建、查询、替换、删除的顺序完成一次完整操作。",
+    "minutes": 25,
+    "objectives": [
+      "用字典保存笔记并生成不重复的进程内编号",
+      "实现带校验与响应模型的笔记 REST 接口",
+      "按真实创建结果串联后续请求并验证失败路径"
+    ],
+    "concepts": [
+      {
+        "title": "请求之间需要共享状态",
+        "body": "局部变量在函数调用结束后不用于保存下一次请求的数据。这里用模块级 notes 字典,键为整数 id,值为笔记字段。它只存在于当前 Python 进程内;重启、重载都会清空,多个 worker 也不会共享这份字典。"
+      },
+      {
+        "title": "编号与列表位置不是同一个概念",
+        "body": "使用单独递增的 next_id 分配编号,删除笔记后不回退。不能用 len(notes)+1 生成 id,否则删掉中间一条后可能复用仍存在的编号。客户端应从创建响应读取 id,再构造单条资源路径。"
+      },
+      {
+        "title": "演示存储的运行条件",
+        "body": "本节路由使用 async def,内部没有 await,字典读写在一次事件循环执行段中完成。以单个 Uvicorn worker 运行本地实验;不要在更新共享状态的中间加入等待操作。异步函数与等待的细节将在后面的并发课程中单独演示。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "重建 main.py 的公共部分",
+        "body": "先备份前几课的实验文件,再将 main.py 替换为本步骤代码,并按顺序追加第 2、3、4 步。不要同时保留临时回显路由。全部代码完成后再启动;本节不含 /version 等附加练习接口。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from typing import Annotated\n\nfrom fastapi import FastAPI, HTTPException, Query, Response\nfrom pydantic import BaseModel, ConfigDict, Field\n\napp = FastAPI(title=\"API Lab\")\n\nclass NoteCreate(BaseModel):\n    model_config = ConfigDict(extra=\"forbid\")\n    title: str = Field(min_length=1, max_length=120)\n    content: str = \"\"\n    pinned: bool = False\n\nclass NoteReplace(BaseModel):\n    model_config = ConfigDict(extra=\"forbid\")\n    title: str = Field(min_length=1, max_length=120)\n    content: str\n    pinned: bool\n\nclass NoteOut(BaseModel):\n    id: int\n    title: str\n    content: str\n    pinned: bool\n\nnotes: dict[int, dict] = {}\nnext_id = 1\n\n@app.get(\"/health\", tags=[\"system\"])\nasync def health():\n    return {\"status\": \"ok\"}"
+      },
+      {
+        "title": "实现列表与创建",
+        "body": "追加两个集合路由。pinned is not None 保证 false 也会触发筛选。创建成功后返回 201,并通过 Location 响应头给出新资源路径。next_id 在当前进程递增,保存时写入所有输入字段。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "@app.get(\"/notes\", response_model=list[NoteOut], tags=[\"notes\"])\nasync def list_notes(\n    limit: Annotated[int, Query(ge=1, le=100)] = 10,\n    pinned: bool | None = None,\n):\n    rows = list(notes.values())\n    if pinned is not None:\n        rows = [row for row in rows if row[\"pinned\"] == pinned]\n    return rows[:limit]\n\n@app.post(\"/notes\", response_model=NoteOut, status_code=201, tags=[\"notes\"])\nasync def create_note(note: NoteCreate, response: Response):\n    global next_id\n    note_id = next_id\n    next_id += 1\n    row = {\"id\": note_id, **note.model_dump()}\n    notes[note_id] = row\n    response.headers[\"Location\"] = f\"/notes/{note_id}\"\n    return row"
+      },
+      {
+        "title": "实现单条读取与全量替换",
+        "body": "先定义统一查找函数。不存在时 raise HTTPException 会立即中断本次请求,下一课再统一其错误格式。PUT 创建全新的字段字典并保留路径 id,不把旧 content 或 pinned 合并进去。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "def require_note(note_id: int) -> dict:\n    if note_id not in notes:\n        raise HTTPException(status_code=404, detail=\"笔记不存在\")\n    return notes[note_id]\n\n@app.get(\"/notes/{note_id}\", response_model=NoteOut, tags=[\"notes\"])\nasync def get_note(note_id: int):\n    return require_note(note_id)\n\n@app.put(\"/notes/{note_id}\", response_model=NoteOut, tags=[\"notes\"])\nasync def replace_note(note_id: int, note: NoteReplace):\n    require_note(note_id)\n    row = {\"id\": note_id, **note.model_dump()}\n    notes[note_id] = row\n    return row"
+      },
+      {
+        "title": "实现无响应体的删除",
+        "body": "删除前先检查是否存在;返回 Response(status_code=204) 明确表示空响应。全部代码写好后运行 python -m uvicorn main:app --reload,打开 /docs,确认一个健康检查加五个笔记操作,共六个路由方法。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "@app.delete(\"/notes/{note_id}\", status_code=204, tags=[\"notes\"])\nasync def delete_note(note_id: int):\n    require_note(note_id)\n    del notes[note_id]\n    return Response(status_code=204)"
+      },
+      {
+        "title": "按顺序完成一轮 CRUD",
+        "body": "使用 /docs:先 GET /notes 确認 [];POST 下列对象并记下实际 id;GET /notes/{id};PUT 提交 title='updated'、content=''、pinned=true;再 GET 确认内容已替换;DELETE 后再次 GET,确认 404。操作过程中不要修改代码触发重载。最后重新创建一条,检查编号没有因删除而回退。",
+        "lang": "json",
+        "code": "{\n  \"title\": \"first CRUD\",\n  \"content\": \"create, read, replace, delete\",\n  \"pinned\": false\n}"
+      }
+    ],
+    "checks": [
+      "空列表返回 200 和 [];创建返回 201,响应含整数 id,Location 指向 /notes/{id}。",
+      "PUT 传齐字段时返回 200,id 保持不变;仅传 title 时返回 422。",
+      "删除返回 204 且响应体为空;再 GET 或 DELETE 相同 id 均返回 404。",
+      "创建两条并删除其中一条后,再创建不会覆盖另一条;服务重启后列表重新为空。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "保存代码后刚创建的笔记找不到了",
+        "cause": "--reload 重启了服务进程,内存字典被重新初始化。",
+        "fix": "完成代码编辑后重新准备数据;录制一轮 CRUD 时不要中途保存服务端文件。"
+      },
+      {
+        "symptom": "筛选 pinned=false 返回全部数据",
+        "cause": "使用 if pinned 判断是否筛选,False 被当成未提供条件。",
+        "fix": "使用 if pinned is not None,再比较字段的布尔值。"
+      },
+      {
+        "symptom": "删除一条后,新笔记覆盖了旧笔记",
+        "cause": "用 len(notes)+1 计算编号导致重复。",
+        "fix": "独立维护递增 next_id,不因删除回退;本地练习保持单进程运行。"
+      }
+    ],
+    "exercise": {
+      "task": "在列表接口加入 offset>=0,先按 pinned 筛选,再返回 rows[offset:offset+limit]。创建三条笔记,第二条和第三条设为 pinned=true,验证 pinned=true&offset=1&limit=1 只返回第三条。",
+      "hint": "分页应该作用在筛选后的结果上;把切片放在筛选后,limit 和 offset 均添加参数约束。",
+      "answer": "添加 offset: Annotated[int, Query(ge=0)] = 0;保持 pinned 筛选顺序;把 return rows[:limit] 改为 return rows[offset:offset + limit]。按题目创建顺序,返回数组长度为 1,元素为第三条笔记;offset=-1 返回 422。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:响应状态码",
+        "url": "https://fastapi.tiangolo.com/tutorial/response-status-code/"
+      },
+      {
+        "label": "FastAPI:直接返回 Response",
+        "url": "https://fastapi.tiangolo.com/advanced/response-directly/"
+      },
+      {
+        "label": "FastAPI:处理错误",
+        "url": "https://fastapi.tiangolo.com/tutorial/handling-errors/"
+      }
+    ]
+  },
+  {
+    "id": "07",
+    "group": "REST 接口",
+    "title": "返回可定位、格式一致的错误",
+    "summary": "用 HTTPException 表达已知失败,统一 404、405 与 422 的响应结构,并保留字段位置。",
+    "minutes": 22,
+    "objectives": [
+      "在业务失败时抛出 HTTPException",
+      "为 HTTPException 和 RequestValidationError 注册处理器",
+      "保持 HTTP 状态码,同时返回稳定的错误结构"
+    ],
+    "concepts": [
+      {
+        "title": "已知失败应在对应位置结束请求",
+        "body": "查不到笔记是可以预期的业务结果,用 raise HTTPException(status_code=404, detail='笔记不存在') 结束当前处理。return HTTPException(...) 只是在返回一个对象,不能表达相同控制流程。未知编程错误需要修复,不能一律包装成 200。"
+      },
+      {
+        "title": "统一外层结构,保留不同错误的细节",
+        "body": "本项目约定 {error:{code,message,details}}。HTTP 错误使用 code='http_error';请求校验使用 code='validation_error'。HTTP 状态码仍保留 404、405、422 等差异。details 中只返回定位所需的字段路径、错误类型和说明,不回显整个请求体。"
+      },
+      {
+        "title": "校验失败可能发生在路由执行之前",
+        "body": "参数解析失败会抛出 RequestValidationError,不会进入路由里的 try/except。因此在应用级处理器统一处理。注册 Starlette 的 HTTPException 基类还能覆盖路由不存在和方法不支持等框架错误;处理时保留 Allow 等原有响应头。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "确认已有业务错误入口",
+        "body": "保留第 06 课代码,找到 require_note。确认它使用 raise 而非 return。请求一个不存在的 id,此时默认响应通常为 {\"detail\":\"笔记不存在\"},接下来统一其外层结构。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "def require_note(note_id: int) -> dict:\n    if note_id not in notes:\n        raise HTTPException(status_code=404, detail=\"笔记不存在\")\n    return notes[note_id]"
+      },
+      {
+        "title": "统一 HTTP 错误处理",
+        "body": "在 main.py 中添加导入与处理器。这里演示业务 detail 为字符串;若后续传入字典,应单独约定字段映射,不把任意内部对象直接返回。保留 exc.headers,确保例如 405 的 Allow 头仍然存在。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from fastapi import Request\nfrom fastapi.responses import JSONResponse\nfrom starlette.exceptions import HTTPException as StarletteHTTPException\n\n@app.exception_handler(StarletteHTTPException)\nasync def handle_http_error(request: Request, exc: StarletteHTTPException):\n    return JSONResponse(\n        status_code=exc.status_code,\n        headers=exc.headers,\n        content={\n            \"error\": {\n                \"code\": \"http_error\",\n                \"message\": str(exc.detail),\n                \"details\": [],\n            }\n        },\n    )"
+      },
+      {
+        "title": "统一 422 并保留字段位置",
+        "body": "添加请求校验处理器。不要直接把 exc.errors() 中的所有对象交给 JSON 序列化;自定义校验器的 ctx 可能包含异常对象。这里只提取 JSON 可序列化的 loc、type、msg,也避免在响应中回显原始输入。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from fastapi.exceptions import RequestValidationError\n\n@app.exception_handler(RequestValidationError)\nasync def handle_validation_error(request: Request, exc: RequestValidationError):\n    details = [\n        {\n            \"loc\": list(error[\"loc\"]),\n            \"type\": error[\"type\"],\n            \"msg\": error[\"msg\"],\n        }\n        for error in exc.errors()\n    ]\n    return JSONResponse(\n        status_code=422,\n        content={\n            \"error\": {\n                \"code\": \"validation_error\",\n                \"message\": \"请求参数不符合接口要求\",\n                \"details\": details,\n            }\n        },\n    )"
+      },
+      {
+        "title": "用三类失败验证处理器",
+        "body": "先 GET /notes/999999,确认 404 且 error.code='http_error'。再 GET /notes/abc 和 POST /notes 的空对象,确认 422;前者定位 path.note_id,后者定位 body.title。最后 POST /health,检查 405、http_error 以及 Allow 头。具体英文 msg 可能随依赖版本变化,客户端不要解析这段文本作业务判断。",
+        "lang": "json",
+        "code": "{\n  \"error\": {\n    \"code\": \"validation_error\",\n    \"message\": \"请求参数不符合接口要求\",\n    \"details\": [\n      {\n        \"loc\": [\"body\", \"title\"],\n        \"type\": \"missing\",\n        \"msg\": \"Field required\"\n      }\n    ]\n  }\n}"
+      }
+    ],
+    "checks": [
+      "不存在的笔记返回 404、http_error 和“笔记不存在”。",
+      "非法 note_id 与缺失 title 都返回 422、validation_error,details.loc 能指出不同位置。",
+      "POST /health 仍返回 405,Allow 响应头被保留。",
+      "成功的笔记响应不被包在 error 字段中;204 删除仍然没有响应体。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "错误响应有 message,但 HTTP 状态一直是 200",
+        "cause": "只返回了错误字典,没有设置 JSONResponse 的 status_code。",
+        "fix": "在异常处理器中显式设置对应状态,HTTP 错误使用 exc.status_code。"
+      },
+      {
+        "symptom": "注册 HTTPException 处理器后,未知路径仍是默认格式",
+        "cause": "只处理 FastAPI 的异常子类,没有覆盖框架抛出的 Starlette 基类异常。",
+        "fix": "导入并为 StarletteHTTPException 注册处理器,路由仍可抛出 FastAPI 的 HTTPException。"
+      },
+      {
+        "symptom": "自定义标题校验失败后出现 JSON 序列化异常",
+        "cause": "把 exc.errors() 的 ctx 异常对象直接放进 JSONResponse。",
+        "fix": "只提取 loc、type、msg 等必要字段,不直接返回任意异常对象或原始请求内容。"
+      }
+    ],
+    "exercise": {
+      "task": "对 GET /notes?limit=0、GET /notes/abc、POST /notes 的 {} 逐一记录 status、error.code 和第一条 details.loc,写出客户端应如何定位表单或请求参数错误。",
+      "hint": "三次都是 422,但错误位置的来源不同:query、path、body。",
+      "answer": "三次 status 都是 422,code 都是 validation_error。loc 分别为 ['query','limit']、['path','note_id']、['body','title']。客户端先按 status/code 判断错误类别,再用 loc 映射对应输入位置;msg 用于显示,不作为稳定的判断条件。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:覆盖异常处理器",
+        "url": "https://fastapi.tiangolo.com/tutorial/handling-errors/"
+      },
+      {
+        "label": "Starlette:Exceptions",
+        "url": "https://github.com/Kludex/starlette/blob/main/docs/exceptions.md"
+      },
+      {
+        "label": "Pydantic:错误处理",
+        "url": "https://docs.pydantic.dev/latest/errors/errors/"
+      }
+    ]
+  },
+  {
+    "id": "08",
+    "group": "REST 接口",
+    "title": "复用依赖与拆分路由文件",
+    "summary": "用 X-API-Key 练习依赖执行顺序,把 /private 放入独立 APIRouter,并通过环境变量配置课堂密钥。",
+    "minutes": 25,
+    "objectives": [
+      "用 Depends 在路由执行前完成可复用校验",
+      "从请求头读取 X-API-Key,从环境变量读取预期值",
+      "用 APIRouter 拆分文件并注册到同一个应用"
+    ],
+    "concepts": [
+      {
+        "title": "Depends 声明请求处理的前置步骤",
+        "body": "FastAPI 收到请求后先解析依赖需要的参数,执行依赖函数,再进入路由。依赖抛出 HTTPException 时,路由不会执行。同一套校验可以挂在多个接口或整个 APIRouter 上,避免每个路由重复读取请求头。"
+      },
+      {
+        "title": "Header 与查询参数的来源不同",
+        "body": "x_api_key: str 这种普通参数默认会作为查询参数。使用 Header(alias='X-API-Key') 才从请求头读取。这里给缺失值默认 None,再由依赖统一返回课堂约定的 401;缺少请求头不会先变成默认必填参数的 422。401 响应携带 WWW-Authenticate: APIKey,说明本实验使用的凭据类型。"
+      },
+      {
+        "title": "配置值与身份系统分开理解",
+        "body": "DEMO_API_KEY 从启动终端的环境变量读取,网页教程中的 classroom-key 仅用于本地实验。这个共享字符串只演示请求校验,不能区分用户,也没有密钥轮换或权限管理。本节只保护 /private,公开的笔记接口用于本地练习。"
+      },
+      {
+        "title": "APIRouter 负责组织,不会自动启动服务",
+        "body": "独立文件创建 router 并注册其路由,主入口再 app.include_router(router)。依赖模块不导入 main,避免循环导入。多个文件最终仍组成同一个 FastAPI 应用,共享第 07 课注册的异常处理器。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "建立依赖函数文件",
+        "body": "在 api-lab 根目录新增 dependencies.py,不修改前面的笔记接口。用 secrets.compare_digest 比较字符串。未配置环境变量返回 503,缺失或错误请求头返回 401,这两种失败帮助区分服务配置和调用参数。",
+        "filename": "dependencies.py",
+        "lang": "python",
+        "code": "import os\nimport secrets\nfrom typing import Annotated\n\nfrom fastapi import Header, HTTPException\n\ndef require_demo_key(\n    x_api_key: Annotated[str | None, Header(alias=\"X-API-Key\")] = None,\n) -> None:\n    expected = os.getenv(\"DEMO_API_KEY\")\n    if not expected:\n        raise HTTPException(status_code=503, detail=\"服务未配置 DEMO_API_KEY\")\n    if x_api_key is None or not secrets.compare_digest(\n        x_api_key.encode(\"utf-8\"), expected.encode(\"utf-8\")\n    ):\n        raise HTTPException(\n            status_code=401,\n            detail=\"X-API-Key 无效\",\n            headers={\"WWW-Authenticate\": \"APIKey\"},\n        )"
+      },
+      {
+        "title": "创建独立的私有路由",
+        "body": "新增 routers 目录,其中创建空的 __init__.py 和下面的 private.py。把依赖挂在 APIRouter 上,当前文件中的接口都会执行相同校验。装饰器使用 @router.get,路径保持 /private。",
+        "filename": "routers/private.py",
+        "lang": "python",
+        "code": "from fastapi import APIRouter, Depends\n\nfrom dependencies import require_demo_key\n\nrouter = APIRouter(\n    tags=[\"private\"],\n    dependencies=[Depends(require_demo_key)],\n)\n\n@router.get(\"/private\")\ndef read_private():\n    return {\"message\": \"API key accepted\"}"
+      },
+      {
+        "title": "在主入口注册 router",
+        "body": "在 main.py 导入路由模块,并在 app 创建之后调用 include_router。保留原有 /health、笔记路由和两个异常处理器。刷新 /docs,确认出现 private 分组及 X-API-Key 参数;如果没有出现,先检查是否真正注册。",
+        "filename": "main.py",
+        "lang": "python",
+        "code": "from routers import private\n\n# 放在 app = FastAPI(...) 之后\napp.include_router(private.router)\n\n# 目录结构\n# api-lab/\n#   main.py\n#   dependencies.py\n#   routers/\n#     __init__.py\n#     private.py"
+      },
+      {
+        "title": "设置当前终端环境并重新启动",
+        "body": "先 Ctrl+C 停止旧服务,在同一个终端按系统选择一组命令。新启动的进程继承该终端环境;在另一窗口设置变量不会修改正在运行的服务。classroom-key 是公开教学校验值。",
+        "lang": "bash",
+        "code": "# Windows PowerShell\n$env:DEMO_API_KEY = \"classroom-key\"\npython -m uvicorn main:app --reload\n\n# macOS / Linux\nexport DEMO_API_KEY=\"classroom-key\"\npython -m uvicorn main:app --reload"
+      },
+      {
+        "title": "验证缺失、错误与正确密钥",
+        "body": "在 /docs 连续执行 GET /private:不填写头部、填写 wrong-key、填写 classroom-key。前两次返回 401,第三次返回 200。第 07 课错误处理器会把失败转换为统一 error 对象。再访问 /notes,确认该本地练习接口未要求密钥。",
+        "lang": "bash",
+        "code": "# macOS / Linux;Windows PowerShell 使用 curl.exe\ncurl -i http://127.0.0.1:8000/private\ncurl -i http://127.0.0.1:8000/private -H \"X-API-Key: wrong-key\"\ncurl -i http://127.0.0.1:8000/private -H \"X-API-Key: classroom-key\""
+      }
+    ],
+    "checks": [
+      "/docs 显示 /private 和 X-API-Key 请求头输入项。",
+      "配置环境变量后,缺失或错误密钥返回 401 和 http_error;正确密钥返回 200 和 {\"message\":\"API key accepted\"}。",
+      "未设置 DEMO_API_KEY 的新服务进程返回 503,而不会因为请求头与配置都为空而通过。",
+      "拆分文件后 /health、原有笔记 CRUD 与错误结构仍然可用。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "已在客户端写 X-API-Key,却显示密钥无效",
+        "cause": "服务读取的头部来源声明不正确,或者客户端把值放到了查询字符串。",
+        "fix": "依赖明确使用 Header(alias='X-API-Key');在 Network 或 curl 输出中核对 Request Headers。"
+      },
+      {
+        "symptom": "环境变量已设置,接口仍返回 503",
+        "cause": "变量设置在另一个终端,或者旧服务进程没有重启。",
+        "fix": "在设置变量的同一个终端停止并重启 Uvicorn。"
+      },
+      {
+        "symptom": "新增 private.py 后 /private 返回 404",
+        "cause": "只创建了 router,没有在主 app 上 include_router,或启动时目录不正确。",
+        "fix": "确认 main.py 注册 private.router,并从 api-lab 根目录启动 main:app。"
+      }
+    ],
+    "exercise": {
+      "task": "在 routers/private.py 中新增 GET /private/status,返回 {\"enabled\":true}。复用 router 上的依赖,分别验证无头部与正确头部。再说明为什么这仍不能判断是哪位学生发出的请求。",
+      "hint": "只添加 @router.get('/private/status') 的函数,不必复制校验代码。",
+      "answer": "添加:\n@router.get('/private/status')\ndef private_status():\n    return {'enabled': True}\n\n无 X-API-Key 返回 401;classroom-key 返回 200 和 {'enabled':true}。两个接口自动复用 router 依赖。所有人使用同一公开密钥,服务只能判断是否匹配,无法识别具体用户。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:依赖注入",
+        "url": "https://fastapi.tiangolo.com/tutorial/dependencies/"
+      },
+      {
+        "label": "FastAPI:Header 参数",
+        "url": "https://fastapi.tiangolo.com/tutorial/header-params/"
+      },
+      {
+        "label": "FastAPI:多文件应用",
+        "url": "https://fastapi.tiangolo.com/tutorial/bigger-applications/"
+      },
+      {
+        "label": "Python:compare_digest",
+        "url": "https://docs.python.org/3/library/secrets.html#secrets.compare_digest"
+      }
+    ]
+  },
+  {
+    "id": "09",
+    "group": "异步与调用",
+    "title": "使用 async / await 处理 I/O 等待",
+    "summary": "用两个并发请求复现阻塞,掌握 async / await 在接口中的实际作用。",
+    "minutes": 22,
+    "objectives": [
+      "写出不阻塞事件循环的 /slow 接口",
+      "测量串行与并发请求的耗时差异",
+      "判断同步 I/O 应改用异步客户端,还是移交线程"
+    ],
+    "concepts": [
+      {
+        "title": "async def 定义协程函数",
+        "body": "调用后得到可等待对象;await 等待结果,并在操作尚未完成时让出执行机会。仅给函数添加 async 不会把内部的阻塞代码变为异步。"
+      },
+      {
+        "title": "等待与计算分开判断",
+        "body": "网络响应、磁盘访问属于 I/O;大量 Python 计算属于 CPU 工作。异步可以交错等待,本课不通过 async 加速计算,也不在接口里引入后台任务队列。"
+      },
+      {
+        "title": "FastAPI 的执行位置",
+        "body": "FastAPI 调用普通 def 路由时使用线程池;async def 路由在事件循环中执行。你在 async def 内直接调用普通辅助函数时,FastAPI 不会自动把辅助函数移到线程。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "增加一个可测量的等待接口",
+        "body": "开始前,在已激活的虚拟环境中运行 python -m pip install \"httpx>=0.27,<1\",第 09 节的客户端脚本会导入它。下载包的 requirements.txt 已包含此依赖。\n\n在 api-lab 的应用中加入路由。delay 限制在 0–5 秒,避免误输很大数值;运行单个服务进程进行本课实验。",
+        "code": "import asyncio\nfrom fastapi import Query\n\n@app.get(\"/slow\")\nasync def slow(delay: float = Query(default=1, ge=0, le=5)):\n    await asyncio.sleep(delay)\n    return {\"delay\": delay}",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "同时发出两次请求",
+        "body": "新建脚本,保持服务已启动后执行 python concurrent_check.py。先建立一个客户端,再让两次请求同时等待;打印状态码和总耗时。页面演示是浏览器模拟,真实耗时以此脚本为准。",
+        "code": "import asyncio\nfrom time import perf_counter\nimport httpx\n\nasync def main():\n    async with httpx.AsyncClient(base_url=\"http://127.0.0.1:8000\") as client:\n        started = perf_counter()\n        results = await asyncio.gather(\n            client.get(\"/slow\", params={\"delay\": 1}),\n            client.get(\"/slow\", params={\"delay\": 1}),\n        )\n        print([r.status_code for r in results])\n        print(f\"elapsed={perf_counter() - started:.2f}s\")\n\nasyncio.run(main())",
+        "lang": "python",
+        "filename": "concurrent_check.py"
+      },
+      {
+        "title": "复现阻塞,再恢复",
+        "body": "暂时把 await asyncio.sleep(delay) 换成 time.sleep(delay),并 import time。保存后等服务完成重载,再运行相同脚本。单进程时两次等待通常接近 2 秒;恢复 async sleep 后通常接近 1 秒。另开终端访问 /health,观察阻塞时它也要等待。结果会受机器和调度开销影响,不以毫秒误差判断对错。",
+        "code": "curl.exe \"http://127.0.0.1:8000/health\"",
+        "lang": "bash"
+      },
+      {
+        "title": "为无法替换的同步 I/O 选择线程",
+        "body": "优先使用库提供的异步方法;若必须在 async def 路由中调用同步 I/O,用 asyncio.to_thread。此处 slow_sync 用 sleep 模拟同步 I/O,实际替换为该库的函数;线程不是本课 CPU 计算加速方案。",
+        "code": "import time\n\ndef slow_sync(delay: float):\n    time.sleep(delay)\n    return {\"delay\": delay}\n\n# 放在 async def 路由内:\n# result = await asyncio.to_thread(slow_sync, delay)\n# return result",
+        "lang": "python"
+      }
+    ],
+    "checks": [
+      "异步版本两个 delay=1 请求均返回 200,总耗时通常接近 1 秒。",
+      "阻塞版本在单进程下耗时明显增加,恢复后 /health 不再等待 slow 的 sleep 结束。",
+      "/slow?delay=-1 与 /slow?delay=6 都返回 422。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "写了 async def,其他请求仍卡住",
+        "cause": "路由内使用 time.sleep、同步 HTTP 客户端或长时间计算,占住事件循环。",
+        "fix": "定位耗时调用;I/O 改用 await 支持的 API,必要时把同步 I/O 放到线程。"
+      },
+      {
+        "symptom": "两次 await 请求仍耗时约 2 秒",
+        "cause": "先 await 第一次,再创建第二次,代码仍是串行等待。",
+        "fix": "用 asyncio.gather 同时调度两个请求,再比较结果。"
+      },
+      {
+        "symptom": "出现 coroutine was never awaited",
+        "cause": "调用异步函数但没有 await,也没有交给任务调度。",
+        "fix": "在 async 函数内 await 结果;脚本最外层用 asyncio.run(main())。"
+      }
+    ],
+    "exercise": {
+      "task": "把脚本改成同时请求 3 个 delay=1 的 /slow,再增加一次 /health,记录总耗时及各状态码。",
+      "hint": "把第三个 /slow 和 /health 一并放进 gather;不要依次 await。",
+      "answer": "四个请求应全部返回 200。使用非阻塞等待时,总耗时通常仍接近最长的一次等待,即约 1 秒;若接近 3 秒,先检查服务是否恢复了 asyncio.sleep。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:并发与 async / await",
+        "url": "https://fastapi.tiangolo.com/async/"
+      },
+      {
+        "label": "Python:协程、gather 与 to_thread",
+        "url": "https://docs.python.org/3/library/asyncio-task.html"
+      }
+    ],
+    "demo": "async"
+  },
+  {
+    "id": "10",
+    "group": "异步与调用",
+    "title": "使用 HTTPX 调用服务与调试 CORS",
+    "summary": "复用 HTTPX 客户端,处理超时与上游错误,并定位浏览器跨域问题。",
+    "minutes": 26,
+    "objectives": [
+      "在应用生命周期中创建和关闭 AsyncClient",
+      "区分超时、连接失败和非成功状态码",
+      "说明为什么 curl 正常而浏览器请求失败"
+    ],
+    "concepts": [
+      {
+        "title": "一次请求有两个方向",
+        "body": "浏览器到 FastAPI 是入站请求;FastAPI 到另一个服务是出站请求。调用失败时,先确定错误发生在哪一段。"
+      },
+      {
+        "title": "客户端与连接池",
+        "body": "AsyncClient 可以复用连接。应用启动时创建,退出时关闭;不要在每次高频请求内重新创建客户端。connect、read、write、pool 是不同阶段的超时,read 通常是等待下一块数据的上限,不是请求总时限。"
+      },
+      {
+        "title": "CORS 由浏览器检查",
+        "body": "Origin 由协议、主机和端口组成。http://localhost:5500 与 http://127.0.0.1:5500 是两个来源;curl 和服务端 HTTPX 不执行浏览器的 CORS 检查。CORS 也不替代 API 认证。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "把客户端交给 lifespan 管理",
+        "body": "安装 httpx。把现有 FastAPI() 初始化改为使用 lifespan;若应用已有 lifespan,把下面的客户端管理合并进去,不要再创建第二个 app。上游使用本机已完成的 /slow,让超时实验不依赖公网。",
+        "code": "from contextlib import asynccontextmanager\nimport httpx\nfrom fastapi import FastAPI\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    timeout = httpx.Timeout(3.0, connect=1.0)\n    async with httpx.AsyncClient(\n        base_url=\"http://127.0.0.1:8000\", timeout=timeout\n    ) as client:\n        app.state.http = client\n        yield\n\napp = FastAPI(lifespan=lifespan)",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "把出站失败转换为可判断的响应",
+        "body": "增加 /upstream-demo。默认 delay=1 会成功,delay=4 超过 read 超时。这里调用本机接口只是本地实验;正式业务中使用服务方的固定地址,避免让用户任意指定出站 URL。raise_for_status 会把非 2xx 响应转为 HTTPStatusError。",
+        "code": "from fastapi import HTTPException, Query, Request\n\n@app.get(\"/upstream-demo\")\nasync def upstream_demo(\n    request: Request,\n    delay: float = Query(default=1, ge=0, le=5),\n):\n    try:\n        response = await request.app.state.http.get(\n            \"/slow\", params={\"delay\": delay}\n        )\n        response.raise_for_status()\n        return {\"upstream\": response.json()}\n    except httpx.TimeoutException as exc:\n        raise HTTPException(504, \"上游响应超时\") from exc\n    except httpx.HTTPStatusError as exc:\n        raise HTTPException(502, \"上游返回非成功状态\") from exc\n    except httpx.RequestError as exc:\n        raise HTTPException(502, \"无法连接上游服务\") from exc",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "在两个页面来源之间复现 CORS",
+        "body": "API 保持运行在 8000 端口。把下面中间件加入现有 app;已有 CORSMiddleware 时修改原配置,不要重复添加。本实验只允许 http://127.0.0.1:5500。如果使用下载的参考项目,先暂时从 allow_origins 中移除 http://localhost:5500,才能复现后面来源不匹配的情况。",
+        "code": "from fastapi.middleware.cors import CORSMiddleware\n\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"http://127.0.0.1:5500\"],\n    allow_credentials=False,\n    allow_methods=[\"GET\", \"POST\", \"PUT\", \"DELETE\"],\n    allow_headers=[\"Content-Type\", \"X-API-Key\"],\n)",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "建立前端页面并从 5500 访问 API",
+        "body": "在 api-lab 中新建 cors-demo.html,粘贴下面内容。另开一个终端,在 api-lab 目录执行 python -m http.server 5500,保留 API 的 8000 终端。打开 http://127.0.0.1:5500/cors-demo.html 并点击按钮,在 Network 中观察 OPTIONS 预检和 POST;JSON 的 Content-Type 会触发本次跨域预检。再通过 http://localhost:5500/cors-demo.html 打开同一文件,对照来源不匹配时的浏览器错误。此时简单的 GET /health 不一定产生 OPTIONS。",
+        "code": "<!doctype html>\n<html lang=\"zh-CN\">\n<meta charset=\"utf-8\">\n<title>CORS 实验</title>\n<button id=\"send\">向 API 创建笔记</button>\n<pre id=\"result\"></pre>\n<script>\ndocument.querySelector(\"#send\").onclick = async () => {\n  const output = document.querySelector(\"#result\");\n  try {\n    const response = await fetch(\"http://127.0.0.1:8000/notes\", {\n      method: \"POST\",\n      headers: {\"Content-Type\": \"application/json\"},\n      body: JSON.stringify({title: \"CORS 实验\"}),\n    });\n    output.textContent = response.status + \"\\n\" + await response.text();\n  } catch (error) {\n    output.textContent = \"浏览器未能读取响应;检查 Network 和 Console:\" + error.message;\n  }\n};\n</script>\n</html>",
+        "lang": "html",
+        "filename": "cors-demo.html"
+      },
+      {
+        "title": "读预检请求和响应头",
+        "body": "在浏览器 Network 面板查看 Origin、OPTIONS 和 Access-Control-Allow-Origin。curl.exe 可手动查看预检响应,但它不会替浏览器决定是否允许页面读取响应。把来源改为 localhost:5500 后复现不匹配,再恢复准确来源。 macOS / Linux 把 curl.exe 改为 curl。实验完成后,如需同时使用两个前端域名,再把 localhost:5500 加回白名单。",
+        "code": "curl.exe -i -X OPTIONS \"http://127.0.0.1:8000/notes\" -H \"Origin: http://127.0.0.1:5500\" -H \"Access-Control-Request-Method: POST\" -H \"Access-Control-Request-Headers: content-type\"\ncurl.exe -i \"http://127.0.0.1:8000/upstream-demo?delay=4\"",
+        "lang": "bash"
+      }
+    ],
+    "checks": [
+      "/upstream-demo?delay=1 返回 200,包含上游的 delay 字段。",
+      "/upstream-demo?delay=4 返回 504;把上游地址临时改为未监听端口可观察 502,随后恢复。",
+      "允许来源的预检响应包含 Access-Control-Allow-Origin: http://127.0.0.1:5500,浏览器可以读取响应。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "上游返回 404,接口却继续执行正常处理",
+        "cause": "HTTP 请求收到响应不等于业务成功,代码没有检查状态。",
+        "fix": "调用 raise_for_status 并单独处理 HTTPStatusError。"
+      },
+      {
+        "symptom": "curl 成功,网页报 CORS 错误",
+        "cause": "浏览器页面来源与 allow_origins 不匹配,或预检方法、请求头未被允许。",
+        "fix": "逐项核对 Origin、方法和自定义请求头,再看浏览器 Network 中的 OPTIONS 响应。"
+      },
+      {
+        "symptom": "用 timeout=3 就以为整个请求最多 3 秒",
+        "cause": "把阶段性超时误认为总请求截止时间,流式响应每次及时发送数据时可能持续更久。",
+        "fix": "先明确要限制连接、块间等待还是总耗时;本课实验只验证读超时。"
+      }
+    ],
+    "exercise": {
+      "task": "让 /upstream-demo 在等待超过 0.2 秒时返回 504,同时保持直接访问 /slow?delay=1 能正常返回。",
+      "hint": "只覆盖 client.get 的 timeout 参数,保留异步 /slow。",
+      "answer": "给该次请求设置 timeout=httpx.Timeout(0.2, connect=1.0)。请求 /upstream-demo?delay=1 会在等待上游数据时触发超时;直接请求 /slow?delay=1 仍约 1 秒后返回 200。"
+    },
+    "refs": [
+      {
+        "label": "HTTPX:异步客户端",
+        "url": "https://www.python-httpx.org/async/"
+      },
+      {
+        "label": "HTTPX:超时阶段",
+        "url": "https://www.python-httpx.org/advanced/timeouts/"
+      },
+      {
+        "label": "HTTPX:状态码与异常",
+        "url": "https://www.python-httpx.org/quickstart/"
+      },
+      {
+        "label": "FastAPI:lifespan",
+        "url": "https://fastapi.tiangolo.com/advanced/events/"
+      },
+      {
+        "label": "FastAPI:CORS",
+        "url": "https://fastapi.tiangolo.com/tutorial/cors/"
+      }
+    ]
+  },
+  {
+    "id": "11",
+    "group": "流式与实时通信",
+    "title": "完整响应、流式响应、SSE 与 WebSocket",
+    "summary": "从通信方向与消息格式选择接口形态,用同一份文本比较接收过程。",
+    "minutes": 18,
+    "objectives": [
+      "分清 REST 风格、HTTP 响应方式和 WebSocket 协议",
+      "根据发送方向与数据频率选择实现",
+      "观察首段到达时间与全部完成时间"
+    ],
+    "concepts": [
+      {
+        "title": "REST 不规定必须一次返回全部数据",
+        "body": "REST 主要讨论资源及 HTTP 操作方式。普通 JSON 响应先得到完整结果再返回;HTTP 流式响应逐段发送同一个响应体。流式发送不自动定义每段的业务含义。"
+      },
+      {
+        "title": "SSE 为文本流定义事件边界",
+        "body": "SSE 在 HTTP 响应体中使用 text/event-stream,以空行分隔事件。连接内数据由服务端推向客户端;浏览器可用 EventSource 解析。SSE 是 HTTP 流式响应的一种约定。"
+      },
+      {
+        "title": "WebSocket 保持双向通信",
+        "body": "连接建立后,双方可以独立发送消息,不必每条消息新建 HTTP 请求。业务仍需定义消息类型、结束、错误与连接关闭后的行为。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "先跑一次完整结果接口",
+        "body": "用延时代表逐步产生内容,POST /generate 等待全部内容完成后返回一个 JSON 对象。PromptIn 限制长度,本课使用固定文本拼接,便于观察协议而不引入模型服务。",
+        "code": "import asyncio\nfrom pydantic import BaseModel, Field\n\nclass PromptIn(BaseModel):\n    prompt: str = Field(min_length=1, max_length=200)\n\n@app.post(\"/generate\")\nasync def generate(payload: PromptIn):\n    parts = []\n    for text in [\"收到:\", payload.prompt, \"。处理完成。\"]:\n        await asyncio.sleep(0.4)\n        parts.append(text)\n    return {\"text\": \"\".join(parts)}",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "记录完整响应的等待",
+        "body": "在 /docs 对 POST /generate 输入下面 JSON。结果约在所有步骤完成后出现。区分响应头到达与可用业务内容到达;录屏关注用户何时看到第一段内容。",
+        "code": "{\"prompt\":\"hello\"}",
+        "lang": "json"
+      },
+      {
+        "title": "对照三条连接过程",
+        "body": "在本页协议演示中依次选择完整 HTTP、SSE、WebSocket:完整响应在完成后显示文本;SSE 逐条显示 token 事件;WebSocket 可在连接保持期间发送新消息。此交互是浏览器模拟;第 12–15 课会用 api-lab 进行真实收发验证。"
+      },
+      {
+        "title": "把场景写成协议选择题",
+        "body": "保存笔记通常使用 HTTP 的 POST /notes;一段处理结果要边产生边显示,可使用 HTTP 流式响应,文本事件采用 SSE;前后端要频繁互发消息,可使用 WebSocket。若只需偶尔查询状态,也可以定期 HTTP 查询,不需要先维护长连接。"
+      }
+    ],
+    "checks": [
+      "能说清 SSE 使用 HTTP,且不是 WebSocket 的别名。",
+      "/generate 返回单个 {text:...} 对象;不会在每个步骤结束时返回一个新 HTTP 响应。",
+      "能为保存笔记、逐步显示文本、双向实时消息分别选择方案并说明原因。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "为了逐字显示文字,直接把所有接口都改成 WebSocket",
+        "cause": "把流式接收和双向消息通信混为一谈。",
+        "fix": "先确定客户端是否需要在同一条连接上持续主动发送;单向文本结果通常先考虑 HTTP 流式响应。"
+      },
+      {
+        "symptom": "把服务器 yield 的次数当成客户端 read 的次数",
+        "cause": "网络、服务器和浏览器可能合并或拆开数据块。",
+        "fix": "依赖协议的事件分隔符解析消息,不依赖网络块边界。"
+      }
+    ],
+    "exercise": {
+      "task": "用户提交一次短文本后只接收生成结果,偶尔点击“停止”。是否必须使用 WebSocket?",
+      "hint": "思考取消 HTTP 请求是否已经满足停止读取的需要。",
+      "answer": "不必须。可以提交 HTTP 请求并读取流式响应,通过取消请求停止读取;服务端同时处理断开并释放资源。如果还需要在同一会话持续双向发送独立消息,再评估 WebSocket。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:StreamingResponse",
+        "url": "https://fastapi.tiangolo.com/advanced/custom-response/"
+      },
+      {
+        "label": "MDN:SSE 事件流",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events"
+      },
+      {
+        "label": "FastAPI:WebSocket",
+        "url": "https://fastapi.tiangolo.com/advanced/websockets/"
+      }
+    ],
+    "demo": "protocol"
+  },
+  {
+    "id": "12",
+    "group": "流式与实时通信",
+    "title": "用 StreamingResponse 输出 SSE 事件",
+    "summary": "实现有限长度文本流,定义 token / done 事件,并处理客户端断开。",
+    "minutes": 25,
+    "objectives": [
+      "用异步生成器逐段发送 text/event-stream",
+      "使用 JSON 编码 data 字段并保留空行边界",
+      "区分正常完成、连接断开和流内错误"
+    ],
+    "concepts": [
+      {
+        "title": "yield 交出一段响应体",
+        "body": "异步生成器可在产生下一段数据前 await。StreamingResponse 消费生成器并发送响应体;生成器必须有明确的正常结束条件。"
+      },
+      {
+        "title": "消息格式属于双方约定",
+        "body": "本课每个 token 事件的 data 是 {text:...},最后发送 done 事件且 data 为 {}。JSON 编码处理引号和换行,空行负责分隔事件。不能直接把任意用户输入拼接成 SSE 行。"
+      },
+      {
+        "title": "响应开始后不能改写 HTTP 状态",
+        "body": "参数验证应在开始流式响应前完成。发送响应头后若处理中断,应通过约定的流内错误事件或连接结束通知客户端;不能再把已经开始的 200 改成 500。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "定义一处统一的事件编码",
+        "body": "把事件名限制为代码中定义的固定值;用 json.dumps 编码数据。返回字符串末尾是两个换行字符,即一个空行。ensure_ascii=False 保留中文可读性。",
+        "code": "import json\n\ndef encode_sse(event: str, data: dict) -> str:\n    payload = json.dumps(data, ensure_ascii=False)\n    return f\"event: {event}\\ndata: {payload}\\n\\n\"",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "产生有限长度 token 流",
+        "body": "每隔约 0.4 秒发送一个 token,完成后发 done。使用 finally 观察连接关闭后的清理路径;真实上游迭代器、文件等资源应在这里或上下文管理器内关闭。不要吞掉取消异常。",
+        "code": "import asyncio\nfrom fastapi import Request\n\nasync def token_events(request: Request, prompt: str):\n    try:\n        for text in [\"收到:\", prompt, \"。处理完成。\"]:\n            await asyncio.sleep(0.4)\n            if await request.is_disconnected():\n                return\n            yield encode_sse(\"token\", {\"text\": text})\n        yield encode_sse(\"done\", {})\n    finally:\n        print(\"stream closed\")",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "在 /stream 返回 StreamingResponse",
+        "body": "GET 查询参数适合 EventSource 演示;不要在 URL 中传密钥或敏感正文。prompt 先经过长度验证,再进入生成器。已有相同路由时替换定义,避免重复注册。",
+        "code": "from fastapi import Query\nfrom fastapi.responses import StreamingResponse\n\n@app.get(\"/stream\")\nasync def stream(\n    request: Request,\n    prompt: str = Query(default=\"hello\", min_length=1, max_length=200),\n):\n    return StreamingResponse(\n        token_events(request, prompt),\n        media_type=\"text/event-stream\",\n        headers={\"Cache-Control\": \"no-cache\"},\n    )",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "从终端逐段观察,再主动取消",
+        "body": "使用 curl.exe 的 -N 禁用它自己的输出缓冲。应先后看到三条 token 与一条 done,每条后有空行。第二次请求在生成中按 Ctrl+C,服务端最终应打印 stream closed;断开通常通过取消路径或断开检查被观察到。",
+        "code": "curl.exe -N -i \"http://127.0.0.1:8000/stream?prompt=hello\"",
+        "lang": "bash"
+      },
+      {
+        "title": "核对线上实际字节格式",
+        "body": "以下是事件体格式展示,其中空行是协议的一部分。data 内的 JSON 字符串若有换行,应由 JSON 编码为转义序列,不能破坏事件边界。",
+        "code": "event: token\ndata: {\"text\": \"hello\"}\n\nevent: done\ndata: {}\n\n",
+        "lang": "http"
+      }
+    ],
+    "checks": [
+      "响应 Content-Type 以 text/event-stream 开头;终端能在流结束前看到 token。",
+      "正常结束只有一个 done,三个 token 的 text 拼接后与 /generate 的 text 相同。",
+      "prompt 为空返回 422;请求中途取消后清理日志出现,服务仍能处理 /health。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "客户端一直等不到事件回调",
+        "cause": "发送了 data 行却漏了事件末尾的空行。",
+        "fix": "每条事件统一通过 encode_sse 编码,并确认末尾为 \\n\\n。"
+      },
+      {
+        "symptom": "输入含换行后事件解析失败",
+        "cause": "手工拼接 JSON 或把原始输入直接放进 data 行。",
+        "fix": "先 json.dumps,再写入 SSE data 字段。"
+      },
+      {
+        "symptom": "断开后仍持续执行,或者没有清理资源",
+        "cause": "无限循环未处理断开、没有 await 让取消生效,或遗漏 finally 清理。",
+        "fix": "在迭代中检查断开,使用可等待的 I/O,并在 finally 中释放资源;保持本课生成器有限长度。"
+      }
+    ],
+    "exercise": {
+      "task": "在三个 token 后、done 前增加一个 stats 事件,返回 token_count:3;保持已有客户端仍能完成。",
+      "hint": "stats 是独立事件类型,不需要把它伪装成 token。",
+      "answer": "在 done 前增加 yield encode_sse(\"stats\", {\"token_count\": 3})。原客户端只监听 token 和 done 时会忽略 stats;新增监听器后可以显示统计值。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:自定义与流式响应",
+        "url": "https://fastapi.tiangolo.com/advanced/custom-response/"
+      },
+      {
+        "label": "MDN:SSE 消息格式",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events"
+      },
+      {
+        "label": "FastAPI:Request 类与断开检查",
+        "url": "https://fastapi.tiangolo.com/reference/request/"
+      }
+    ]
+  },
+  {
+    "id": "13",
+    "group": "流式与实时通信",
+    "title": "在浏览器中读取流,并在中途停止",
+    "summary": "分别使用 EventSource 与 fetch,处理 UTF-8 分块、事件边界和取消请求。",
+    "minutes": 30,
+    "objectives": [
+      "用 EventSource 监听命名事件并主动关闭",
+      "用 ReadableStream 解析可能被拆开的 SSE 数据",
+      "通过 AbortController 停止请求并区分取消与错误"
+    ],
+    "concepts": [
+      {
+        "title": "EventSource 简单,但请求形态有限",
+        "body": "原生 EventSource 发起 GET 请求,自动解析 SSE 并具备重连行为;构造参数不能任意设置请求体和自定义请求头。收到本课有限流的 done 后应主动 close,防止结束后重新请求。"
+      },
+      {
+        "title": "fetch 能发送 POST,再读取响应流",
+        "body": "POST 请求不妨碍响应逐段返回。需要 JSON 请求体或自定义头时可用 fetch,但必须自己解析响应体。调用 response.json() 或 response.text() 会等到响应体读完。"
+      },
+      {
+        "title": "网络块不等于事件",
+        "body": "一个中文字符的 UTF-8 字节、一个 JSON 或空行都可能跨越两次 read;一次 read 也可能包含多条事件。先连续解码字节,再缓存文本并按事件边界提取。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "先用 EventSource 接收命名事件",
+        "body": "打开本地 http://127.0.0.1:8000/docs,在开发者工具 Console 运行。URL 与页面同源,先排除 CORS 干扰。控制台应逐条输出文本,done 后主动关闭;本课把网络失败也关闭,避免自动重连重复执行。",
+        "code": "{\n  const source = new EventSource(\"/stream?prompt=hello\");\n  source.addEventListener(\"token\", (event) => {\n    console.log(JSON.parse(event.data).text);\n  });\n  source.addEventListener(\"done\", () => {\n    console.log(\"完成\");\n    source.close();\n  });\n  source.onerror = () => {\n    console.error(\"流连接失败,已停止\");\n    source.close();\n  };\n  window.stopSse = () => source.close();\n}",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "把事件解析与网络读取分开",
+        "body": "先在 Console 定义解析函数。它为本课 LF / CRLF 事件格式处理跨块与多条 data 行,且要求正常收到 done;不实现完整 EventSource 的 id、retry 与自动重连功能。TextDecoder 的 stream:true 会暂存未完整的 UTF-8 字节,最后再刷新。",
+        "code": "async function consumeSse(response, onEvent) {\n  if (!response.ok) throw new Error(`HTTP ${response.status}`);\n  if (!response.headers.get(\"content-type\")?.includes(\"text/event-stream\")) {\n    throw new Error(\"响应不是 SSE\");\n  }\n  if (!response.body) throw new Error(\"没有响应体\");\n  const reader = response.body.getReader();\n  const decoder = new TextDecoder(\"utf-8\");\n  let buffer = \"\";\n  let completed = false;\n  try {\n    while (!completed) {\n      const { value, done } = await reader.read();\n      buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });\n      let boundary;\n      while ((boundary = /\\r?\\n\\r?\\n/.exec(buffer)) !== null) {\n        const block = buffer.slice(0, boundary.index);\n        buffer = buffer.slice(boundary.index + boundary[0].length);\n        const lines = block.split(/\\r?\\n/);\n        const name = lines.find(line => line.startsWith(\"event:\"))\n          ?.slice(6).trim() || \"message\";\n        const data = lines.filter(line => line.startsWith(\"data:\"))\n          .map(line => line.slice(5).replace(/^ /, \"\")).join(\"\\n\");\n        if (data) onEvent(name, JSON.parse(data));\n        if (name === \"done\") { completed = true; break; }\n      }\n      if (done && !completed) throw new Error(\"连接结束,但没有收到 done\");\n    }\n    await reader.cancel();\n  } catch (error) {\n    await reader.cancel().catch(() => {});\n    throw error;\n  } finally {\n    reader.releaseLock();\n  }\n}",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "用 fetch 读取同一个 GET 接口",
+        "body": "在同一 Console 定义 startStream,然后执行 startStream()。紧接着执行 stopStream() 可中途取消;AbortError 代表用户主动取消,不应显示为服务故障。新的请求使用新的 AbortController。",
+        "code": "async function startStream() {\n  const controller = new AbortController();\n  window.stopStream = () => controller.abort();\n  try {\n    const response = await fetch(\"/stream?prompt=hello\", {\n      signal: controller.signal,\n    });\n    await consumeSse(response, (name, data) => {\n      if (name === \"token\") console.log(data.text);\n      if (name === \"done\") console.log(\"完成\");\n    });\n  } catch (error) {\n    if (error.name === \"AbortError\") console.log(\"已取消\");\n    else console.error(error);\n  }\n}\nstartStream();",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "扩展:需要 POST 请求体时",
+        "body": "先在 main.py 额外加入以下 POST /stream 路由,复用第 11 课 PromptIn 与第 12 课 token_events。然后把上一步 fetch 的 URL 改为 /stream,并在配置对象中加入 method:\"POST\"、headers:{\"Content-Type\":\"application/json\"}、body:JSON.stringify({prompt:\"hello\"});保留 signal。POST /generate 仍返回完整 JSON,不可拿它套用 SSE 解析函数。",
+        "code": "@app.post(\"/stream\")\nasync def stream_post(request: Request, payload: PromptIn):\n    return StreamingResponse(\n        token_events(request, payload.prompt),\n        media_type=\"text/event-stream\",\n        headers={\"Cache-Control\": \"no-cache\"},\n    )",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "核对流为什么没有逐段显示",
+        "body": "先用 curl.exe -N 直连本地 API。若本地逐段到达但页面一次出现,检查是否调用了 response.text/json,或渲染是否缓存了结果。若只有经过中间服务才一次出现,再检查中间服务的响应缓冲;本课只定位差异,具体代理配置留到部署模块。"
+      }
+    ],
+    "checks": [
+      "EventSource 收到三次 token 和一次 done 后关闭,不会周期性重新发起 /stream。",
+      "fetch 版本能正常显示中文;主动 stopStream 后输出“已取消”。",
+      "扩展 POST /stream 返回 SSE;POST /generate 仍返回 application/json。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "偶发 JSON.parse 错误,或中文出现替换字符",
+        "cause": "直接对每次 read 的内容解码和解析,忽略了字符、事件可能跨块。",
+        "fix": "保持同一 TextDecoder 并设置 stream:true,再缓存到完整空行边界后解析。"
+      },
+      {
+        "symptom": "一次成功结束后又出现同样文本",
+        "cause": "有限 SSE 结束后 EventSource 自动重连,客户端没有处理 done。",
+        "fix": "监听 done 调用 close;真实恢复能力需要单独定义,不默认重复执行。"
+      },
+      {
+        "symptom": "点击停止后仍把状态改成失败",
+        "cause": "把所有 fetch 异常都当成网络故障。",
+        "fix": "单独识别 AbortError,并同步把页面状态改为已取消。"
+      }
+    ],
+    "exercise": {
+      "task": "把一条包含“你好”的 SSE 消息拆成逐个字节的 ReadableStream,验证解析函数仍得到完整中文和 done。",
+      "hint": "用 TextEncoder 生成字节,再通过 Response 包装本地 ReadableStream;这只是解析器实验,不是 API 网络测试。",
+      "answer": "构造文本 'event: token\\ndata: {\"text\":\"你好\"}\\n\\nevent: done\\ndata: {}\\n\\n';对 TextEncoder().encode(text) 的每个字节调用 controller.enqueue(new Uint8Array([byte])),最后 close。用 new Response(stream,{headers:{\"Content-Type\":\"text/event-stream\"}}) 传入 consumeSse,回调应只收到完整 token 和 done。"
+    },
+    "refs": [
+      {
+        "label": "MDN:EventSource",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/EventSource"
+      },
+      {
+        "label": "MDN:读取 ReadableStream",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams"
+      },
+      {
+        "label": "MDN:TextDecoder.decode",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decode"
+      },
+      {
+        "label": "MDN:AbortController",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/AbortController"
+      }
+    ]
+  },
+  {
+    "id": "14",
+    "group": "流式与实时通信",
+    "title": "建立第一条 WebSocket 双向连接",
+    "summary": "实现 /ws 回显,观察建立、发送、接收和关闭的完整过程。",
+    "minutes": 23,
+    "objectives": [
+      "使用 accept、receive_text 和 send_json 完成回显",
+      "在浏览器连接打开后发送消息",
+      "捕获断开并释放连接相关资源"
+    ],
+    "concepts": [
+      {
+        "title": "连接先建立,消息再发送",
+        "body": "服务端 accept 后进入收发阶段;浏览器 WebSocket 从 CONNECTING 转为 OPEN 后才能发送。send 只表示数据进入发送流程,不证明对方已经处理。"
+      },
+      {
+        "title": "一条连接,多条消息",
+        "body": "本课客户端发送文本,服务端回复 JSON。普通文本对应 type:echo;文本 ping 对应 type:pong。双方必须约定数据格式,不能把 HTTP 路由的返回值当作 WebSocket 消息发送。"
+      },
+      {
+        "title": "断开是正常生命周期",
+        "body": "用户关页、主动关闭和网络中断都可能结束连接。receive_text 在断开时会抛出 WebSocketDisconnect;在 finally 中清理该连接持有的资源。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "在 main.py 插入 /ws 路由",
+        "body": "接受连接后循环读取消息。回复必须调用 send_json,而不是 return 一个字典。这个本地练习只接收文本消息,不实现用户身份或消息保存。",
+        "code": "from fastapi import WebSocket, WebSocketDisconnect\n\n@app.websocket(\"/ws\")\nasync def websocket_echo(websocket: WebSocket):\n    await websocket.accept()\n    try:\n        while True:\n            text = await websocket.receive_text()\n            if text == \"ping\":\n                await websocket.send_json({\"type\": \"pong\"})\n            else:\n                await websocket.send_json({\"type\": \"echo\", \"text\": text})\n    except WebSocketDisconnect:\n        pass\n    finally:\n        print(\"websocket closed\")",
+        "lang": "python",
+        "filename": "main.py"
+      },
+      {
+        "title": "从浏览器建立连接",
+        "body": "打开本地 http://127.0.0.1:8000/docs,在 Console 运行。代码把连接保留到 window.echoSocket,方便随后发送和关闭。open、message、close 的日志对应三个不同阶段。",
+        "code": "{\n  const socket = new WebSocket(\"ws://127.0.0.1:8000/ws\");\n  window.echoSocket = socket;\n  socket.onopen = () => console.log(\"连接已打开\");\n  socket.onmessage = event => console.log(\"收到\", JSON.parse(event.data));\n  socket.onerror = () => console.error(\"连接发生错误\");\n  socket.onclose = event => console.log(\"已关闭\", event.code, event.reason);\n}",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "在 OPEN 状态发送两种消息",
+        "body": "等“连接已打开”出现后,执行下列代码。在 Network 面板选择 WS 请求并查看消息列表,区分客户端发出的原始文本与服务端返回的 JSON。WebSocket 请求不使用 /docs 的 HTTP Execute 按钮测试。",
+        "code": "if (echoSocket.readyState === WebSocket.OPEN) {\n  echoSocket.send(\"hello\");\n  echoSocket.send(\"ping\");\n}",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "主动关闭并确认生命周期结束",
+        "body": "调用 close 后,浏览器输出关闭事件,服务端输出清理日志。关闭后不能复用旧 socket 继续发送;需要新建 WebSocket。重新连接并再发送一次 hello,确认服务仍可用。",
+        "code": "echoSocket.close(1000, \"lesson finished\");",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      }
+    ],
+    "checks": [
+      "浏览器先收到 open,发送 hello 后收到 {type:'echo',text:'hello'}。",
+      "发送 ping 后收到 {type:'pong'};同一连接可重复收发。",
+      "主动关闭后收到 close,服务端清理日志出现,重新连接仍正常。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "调用 send 报连接尚未建立",
+        "cause": "new WebSocket 返回时握手仍在进行。",
+        "fix": "在 open 回调中发送,或先检查 readyState === WebSocket.OPEN。"
+      },
+      {
+        "symptom": "浏览器无任何回复,路由却 return 了字典",
+        "cause": "WebSocket 消息不会像 HTTP 响应一样自动序列化返回。",
+        "fix": "在连接内 await websocket.send_json(...)。"
+      },
+      {
+        "symptom": "把 WebSocket 地址粘到普通地址栏后得到 404",
+        "cause": "使用 HTTP GET 测试了 WebSocket 端点,没有进行 WebSocket 握手。",
+        "fix": "通过浏览器 WebSocket API 或支持 WS 的客户端测试,使用 ws:// 地址。"
+      }
+    ],
+    "exercise": {
+      "task": "增加一个文本命令 upper:hello,让服务端回复 {type:'upper',text:'HELLO'},其他消息保持回显。",
+      "hint": "把 startswith('upper:') 分支放在默认 echo 分支之前。",
+      "answer": "在 echo 的 else 之前加入 elif text.startswith(\"upper:\"): await websocket.send_json({\"type\":\"upper\",\"text\":text.removeprefix(\"upper:\").upper()})。浏览器收到的 JSON type 应为 upper。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:WebSocket 收发与断开",
+        "url": "https://fastapi.tiangolo.com/advanced/websockets/"
+      },
+      {
+        "label": "MDN:编写 WebSocket 客户端",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications"
+      }
+    ]
+  },
+  {
+    "id": "15",
+    "group": "流式与实时通信",
+    "title": "连接断开后:重连、心跳与取消",
+    "summary": "给 WebSocket 加上有限重试和定时器清理,验证停止后不会自行恢复。",
+    "minutes": 30,
+    "objectives": [
+      "在异常关闭后用指数退避重新连接",
+      "通过应用层 ping / pong 检测连接是否仍可用",
+      "清理重连和心跳定时器,不自动重发未确认消息"
+    ],
+    "concepts": [
+      {
+        "title": "重连创建一条新连接",
+        "body": "新连接不会自动恢复旧连接内的执行状态,也不保证断开期间消息送达。本课不缓存、不重发业务消息;需要可靠恢复时,必须另行设计消息编号、确认和服务端去重。"
+      },
+      {
+        "title": "退避限制重试频率",
+        "body": "连续失败时逐渐延长等待,并加少量随机抖动,避免同时快速请求。设置最大次数;用户主动停止后取消待执行重连。"
+      },
+      {
+        "title": "心跳与页面状态",
+        "body": "这里的 ping 是普通文本消息,pong 是业务 JSON,属于应用层心跳。浏览器 WebSocket API 不提供主动发送协议级 Ping 帧的方法。计时器会受后台页面节流影响,超时阈值需要按场景调整。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "先定义连接状态与定时器清理",
+        "body": "在本地 /docs 的 Console 按步骤运行。先关闭上一课 echoSocket,避免同时观察两条连接。下面状态只属于本次练习;刷新页面可重新开始。",
+        "code": "let wsSocket = null;\nlet retryTimer = null;\nlet heartbeatTimer = null;\nlet pongTimer = null;\nlet wsStopped = true;\nlet retryCount = 0;\n\nfunction clearHeartbeat() {\n  clearInterval(heartbeatTimer);\n  clearTimeout(pongTimer);\n  heartbeatTimer = null;\n  pongTimer = null;\n}",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "加入应用层心跳",
+        "body": "每 5 秒发送一次 ping,3 秒内没有 pong 就关闭连接,让同一条关闭处理路径决定是否重连。收到 pong 时取消对应超时。这里只允许一个等待中的心跳。",
+        "code": "function startHeartbeat(socket) {\n  clearHeartbeat();\n  heartbeatTimer = setInterval(() => {\n    if (socket.readyState !== WebSocket.OPEN) return;\n    socket.send(\"ping\");\n    pongTimer = setTimeout(() => {\n      socket.close(4000, \"heartbeat timeout\");\n    }, 3000);\n  }, 5000);\n}",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "把重连集中在 close 事件中",
+        "body": "首次失败约 0.5 秒后重连,随后约 1、2、4、8 秒,另加少量随机抖动;最多连续重试 5 次。error 只记录,避免 error 和 close 各重试一次。收到服务端有效消息后清零连续失败计数。",
+        "code": "function connectWs() {\n  if (wsStopped) return;\n  const socket = new WebSocket(\"ws://127.0.0.1:8000/ws\");\n  wsSocket = socket;\n  socket.onopen = () => {\n    if (wsStopped || wsSocket !== socket) { socket.close(); return; }\n    console.log(\"已连接\");\n    startHeartbeat(socket);\n  };\n  socket.onmessage = event => {\n    if (wsSocket !== socket) return;\n    const message = JSON.parse(event.data);\n    retryCount = 0;\n    if (message.type === \"pong\") {\n      clearTimeout(pongTimer);\n      pongTimer = null;\n    } else {\n      console.log(\"收到\", message);\n    }\n  };\n  socket.onerror = () => console.log(\"连接错误,等待 close\");\n  socket.onclose = () => {\n    if (wsSocket !== socket) return;\n    clearHeartbeat();\n    if (wsStopped) return;\n    if (retryCount >= 5) {\n      wsStopped = true;\n      console.log(\"连续重试已达上限,请手动开始\");\n      return;\n    }\n    const delay = Math.min(8000, 500 * 2 ** retryCount) + Math.random() * 200;\n    retryCount += 1;\n    console.log(`第 ${retryCount} 次重试,等待 ${Math.round(delay)}ms`);\n    retryTimer = setTimeout(connectWs, delay);\n  };\n}",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "实现开始、发送与停止",
+        "body": "开始函数防止重复创建连接;发送函数在未连接时明确拒绝;停止函数先记录主动停止,再清理所有计时器和连接。stopWs 同时可供页面关闭时调用。",
+        "code": "function startWs() {\n  if (!wsStopped) return;\n  wsStopped = false;\n  retryCount = 0;\n  connectWs();\n}\nfunction sendWs(text) {\n  if (wsSocket?.readyState !== WebSocket.OPEN) {\n    console.log(\"尚未连接,本条消息未发送\");\n    return;\n  }\n  wsSocket.send(text);\n}\nfunction stopWs() {\n  wsStopped = true;\n  clearTimeout(retryTimer);\n  clearHeartbeat();\n  wsSocket?.close(1000, \"user stopped\");\n  wsSocket = null;\n  console.log(\"已停止\");\n}\nwindow.addEventListener(\"pagehide\", stopWs);\nstartWs();",
+        "lang": "javascript",
+        "filename": "浏览器 Console"
+      },
+      {
+        "title": "执行三组失败实验",
+        "body": "正常连接后用 sendWs('hello') 验证回显。接着停止本地服务,观察逐渐延长的重试间隔,重启服务后确认新连接可收发。最后在下一次重试前调用 stopWs(),等待超过 9 秒,确认没有新建 WS 请求。断线期间尝试发送的消息不能在重连后自动出现。"
+      }
+    ],
+    "checks": [
+      "服务停止时重连间隔逐步增加,连续失败达到 5 次重试后停止。",
+      "服务恢复后能建立新连接并再次收到 echo,心跳 ping 对应 pong。",
+      "stopWs 后超过 9 秒没有新连接;未连接时调用 sendWs 不会排队重发。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "每次断线都会产生两条新连接",
+        "cause": "在 error 和 close 中分别启动重试,或开始按钮允许重复创建。",
+        "fix": "只在 close 调度重试;开始前检查当前是否已经运行。"
+      },
+      {
+        "symptom": "点停止后几秒钟又自动连上",
+        "cause": "只关闭 socket,没有取消已经安排的 setTimeout。",
+        "fix": "先设置主动停止标记,再清理重试、心跳和 pong 超时定时器。"
+      },
+      {
+        "symptom": "重连后重复执行了一条操作",
+        "cause": "客户端自动重发消息,但无法确定服务端在断开前是否已经执行。",
+        "fix": "本课不自动重发;需要重试业务操作时另行加入确认、操作编号与幂等处理。"
+      }
+    ],
+    "exercise": {
+      "task": "把连续重试上限改为 3 次,并验证用户手动 startWs() 后可以开始新一轮连接。",
+      "hint": "更改重试边界,保留达到上限时 wsStopped=true,以及开始时 retryCount=0。",
+      "answer": "把 if (retryCount >= 5) 改为 >= 3。连续失败后控制台打印到达上限;随后调用 startWs() 重置次数并重新连接,若服务已恢复即可继续回显。"
+    },
+    "refs": [
+      {
+        "label": "MDN:WebSocket 客户端生命周期",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications"
+      },
+      {
+        "label": "MDN:WebSocket.close",
+        "url": "https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close"
+      },
+      {
+        "label": "RFC 6455:异常关闭后的恢复建议",
+        "url": "https://www.rfc-editor.org/rfc/rfc6455#section-7.2.3"
+      }
+    ]
+  },
+  {
+    "id": "16",
+    "group": "联调与验收",
+    "title": "把能运行的接口变成可重复验收的服务",
+    "summary": "用 pytest 检查 CRUD、错误、认证、SSE 和 WebSocket,再完成一次真实浏览器联调。",
+    "minutes": 32,
+    "objectives": [
+      "使用 TestClient 在测试中运行 FastAPI 应用",
+      "为正常路径和 401 / 404 / 422 写出明确断言",
+      "区分接口逻辑验收与真实网络流式验证"
+    ],
+    "concepts": [
+      {
+        "title": "断言检查可观察结果",
+        "body": "测试应检查状态码、响应字段与操作后结果,而不是只确认请求没报异常。每个测试创建自己的数据,通过返回 id 继续操作,避免假设某条笔记固定为 id=1。"
+      },
+      {
+        "title": "TestClient 不等同于浏览器",
+        "body": "TestClient 基于 HTTPX,可在进程内调用应用,不必先启动 Uvicorn。用 with 管理客户端才会执行 lifespan;它不验证浏览器 CORS,也不能证明真实网络按预期逐段到达。"
+      },
+      {
+        "title": "错误与正常结束分别验收",
+        "body": "422 验证输入约束;401 验证 /private 的 API Key;404 验证资源不存在。SSE 既检查事件数据,也检查 done;WebSocket 检查来回消息并关闭会话。"
+      }
+    ],
+    "steps": [
+      {
+        "title": "建立测试客户端",
+        "body": "在 api-lab 新建 tests/test_acceptance.py;以下第 1–4 步代码依次放入同一文件。先运行 python -m pip install pytest httpx。monkeypatch 只在测试期间设置演示 Key,测试结束自动恢复;导入入口为根目录 main.py。",
+        "code": "import json\nfrom uuid import uuid4\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom main import app\n\n@pytest.fixture\ndef client(monkeypatch):\n    monkeypatch.setenv(\"DEMO_API_KEY\", \"classroom-key\")\n    with TestClient(app) as test_client:\n        yield test_client\n\ndef test_health(client):\n    response = client.get(\"/health\")\n    assert response.status_code == 200",
+        "lang": "python",
+        "filename": "tests/test_acceptance.py"
+      },
+      {
+        "title": "验证创建、查询、更新与删除",
+        "body": "从 POST 返回值提取 id,再验证默认值、列表、PUT 完整替换和 DELETE 后 404。PUT 必须包含 title、content、pinned 三个可写字段。DELETE 204 响应体应为空,不能再对它调用 json()。",
+        "code": "def test_note_lifecycle(client):\n    title = f\"acceptance-{uuid4()}\"\n    created = client.post(\"/notes\", json={\"title\": title})\n    assert created.status_code == 201\n    note = created.json()\n    note_id = note[\"id\"]\n    assert note[\"content\"] == \"\"\n    assert note[\"pinned\"] is False\n    assert client.get(f\"/notes/{note_id}\").json() == note\n\n    listed = client.get(\"/notes\", params={\"q\": title, \"limit\": 100})\n    assert listed.status_code == 200\n    assert any(item[\"id\"] == note_id for item in listed.json())\n\n    replacement = {\"title\": \"更新后\", \"content\": \"测试正文\", \"pinned\": True}\n    updated = client.put(f\"/notes/{note_id}\", json=replacement)\n    assert updated.status_code == 200\n    assert updated.json() == {\"id\": note_id, **replacement}\n    assert client.get(f\"/notes/{note_id}\").json() == updated.json()\n\n    deleted = client.delete(f\"/notes/{note_id}\")\n    assert deleted.status_code == 204\n    assert deleted.content == b\"\"\n    missing = client.get(f\"/notes/{note_id}\")\n    assert missing.status_code == 404\n    assert missing.json()[\"error\"][\"code\"] == \"http_error\"",
+        "lang": "python",
+        "filename": "tests/test_acceptance.py"
+      },
+      {
+        "title": "验证输入错误与受保护接口",
+        "body": "空标题应被输入校验拒绝。只有 /private 在本地样例中需要 X-API-Key;不要把笔记的公开练习路由与认证演示混在一起。错误体沿用前面课程的统一结构 error.code / message / details。",
+        "code": "def test_validation(client):\n    invalid = client.post(\"/notes\", json={\"title\": \"\"})\n    assert invalid.status_code == 422\n    assert invalid.json()[\"error\"][\"code\"] == \"validation_error\"\n    assert client.get(\"/slow\", params={\"delay\": -1}).status_code == 422\n\n\ndef test_api_key(client):\n    assert client.get(\"/private\").status_code == 401\n    wrong = client.get(\"/private\", headers={\"X-API-Key\": \"wrong-key\"})\n    assert wrong.status_code == 401\n    assert wrong.json()[\"error\"][\"code\"] == \"http_error\"\n    valid = client.get(\"/private\", headers={\"X-API-Key\": \"classroom-key\"})\n    assert valid.status_code == 200",
+        "lang": "python",
+        "filename": "tests/test_acceptance.py"
+      },
+      {
+        "title": "验证 SSE 内容和 WebSocket 来回消息",
+        "body": "TestClient 读取有限 SSE 的完整响应后检查协议内容;该测试不用于断言首段到达时间。WebSocket 使用上下文管理器,离开时关闭连接。 如果完成了第 12 课的 stats 扩展,测试允许保留该事件,只对 token/done 的主线顺序和内容断言;done 仍必须是最后一个事件。",
+        "code": "def test_stream_matches_complete_result(client):\n    complete = client.post(\"/generate\", json={\"prompt\": \"hello\"})\n    assert complete.status_code == 200\n    response = client.get(\"/stream\", params={\"prompt\": \"hello\"})\n    assert response.status_code == 200\n    assert response.headers[\"content-type\"].startswith(\"text/event-stream\")\n    events = []\n    for block in response.text.replace(\"\\r\\n\", \"\\n\").strip().split(\"\\n\\n\"):\n        fields = {}\n        for line in block.split(\"\\n\"):\n            name, value = line.split(\":\", 1)\n            fields[name] = value.lstrip(\" \")\n        events.append((fields[\"event\"], json.loads(fields[\"data\"])))\n    assert [name for name, _ in events if name != \"stats\"] == [\"token\", \"token\", \"token\", \"done\"]\n    text = \"\".join(data[\"text\"] for name, data in events if name == \"token\")\n    assert text == complete.json()[\"text\"]\n    assert events[-1] == (\"done\", {})\n\n\ndef test_websocket(client):\n    with client.websocket_connect(\"/ws\") as socket:\n        socket.send_text(\"hello\")\n        assert socket.receive_json() == {\"type\": \"echo\", \"text\": \"hello\"}\n        socket.send_text(\"ping\")\n        assert socket.receive_json() == {\"type\": \"pong\"}",
+        "lang": "python",
+        "filename": "tests/test_acceptance.py"
+      },
+      {
+        "title": "运行自动验收,再测真实网络",
+        "body": "在 api-lab 执行 python -m pytest -q tests/test_acceptance.py。通过后另开终端运行 python -m uvicorn main:app --reload,再运行下面脚本。SSE 的 data 行应分批出现,时间通常约为 0.4、0.8、1.2 秒,而非都等到最后。最后在浏览器完成 CORS、主动停止流、WebSocket 断开重连三个场景。",
+        "code": "from time import perf_counter\nimport httpx\n\nwith httpx.Client(base_url=\"http://127.0.0.1:8000\", timeout=5.0) as client:\n    started = perf_counter()\n    with client.stream(\"GET\", \"/stream\", params={\"prompt\": \"hello\"}) as response:\n        response.raise_for_status()\n        for line in response.iter_lines():\n            if line.startswith(\"data:\"):\n                print(f\"{perf_counter() - started:.2f}s {line}\", flush=True)",
+        "lang": "python",
+        "filename": "verify_stream_network.py"
+      }
+    ],
+    "checks": [
+      "自动测试覆盖健康检查、完整 CRUD、422 校验、401 认证、删除后 404、SSE 与 WebSocket,运行结果全部通过。",
+      "真实 HTTPX / curl 客户端在响应结束前接收到 token,浏览器停止读取后服务端执行清理。",
+      "允许来源的浏览器能够调用 API;未允许来源不能读取响应;WebSocket 主动停止后不再重连。"
+    ],
+    "pitfalls": [
+      {
+        "symptom": "单个测试能过,整套测试偶尔失败",
+        "cause": "多个测试假定相同 id 或依赖前一个测试创建的数据。",
+        "fix": "每个测试自行创建记录,用返回 id 操作,并在生命周期测试中删除。"
+      },
+      {
+        "symptom": "测试里 app.state.http 不存在",
+        "cause": "没有通过 with TestClient(app) 启动 lifespan,或代码未正确注册 lifespan。",
+        "fix": "在 fixture 的上下文中创建客户端,并核对 FastAPI(lifespan=lifespan)。"
+      },
+      {
+        "symptom": "SSE 测试通过,网页仍一次显示全部内容",
+        "cause": "进程内测试只检查完整内容,没有经过浏览器、真实网络和中间服务。",
+        "fix": "用真实 Uvicorn 配合 httpx.stream / curl -N 观察时间,再检查浏览器读取与渲染。"
+      }
+    ],
+    "exercise": {
+      "task": "增加一个测试:创建笔记后,PUT 只发送 title 应返回 422,随后 GET 仍返回原来的完整内容。",
+      "hint": "输入不合法时不能发生部分更新;先保存创建响应,再比较读取结果。",
+      "answer": "创建笔记并保存 note;使用 client.put(f'/notes/{note[\"id\"]}', json={\"title\":\"修改\"}),断言 422;再次 GET 并断言等于 note,最后 DELETE 清理。这个测试同时检查完整替换约定和校验失败不写入的行为。"
+    },
+    "refs": [
+      {
+        "label": "FastAPI:TestClient 与 pytest",
+        "url": "https://fastapi.tiangolo.com/tutorial/testing/"
+      },
+      {
+        "label": "FastAPI:测试 lifespan",
+        "url": "https://fastapi.tiangolo.com/advanced/testing-events/"
+      },
+      {
+        "label": "FastAPI:测试 WebSocket",
+        "url": "https://fastapi.tiangolo.com/advanced/testing-websockets/"
+      },
+      {
+        "label": "pytest:fixtures",
+        "url": "https://docs.pytest.org/en/stable/how-to/fixtures.html"
+      },
+      {
+        "label": "pytest:临时环境变量",
+        "url": "https://docs.pytest.org/en/stable/how-to/monkeypatch.html"
+      },
+      {
+        "label": "HTTPX:流式响应",
+        "url": "https://www.python-httpx.org/quickstart/"
+      }
+    ]
+  }
+]

+ 4 - 0
app/page.tsx

@@ -0,0 +1,4 @@
+import Course from './course';
+export default function Home() {
+  return <Course />;
+}

+ 1 - 0
app/videos.json

@@ -0,0 +1 @@
+{}

+ 25 - 0
components.json

@@ -0,0 +1,25 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "base-nova",
+  "rsc": true,
+  "tsx": true,
+  "tailwind": {
+    "config": "",
+    "css": "app/globals.css",
+    "baseColor": "neutral",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "iconLibrary": "lucide",
+  "rtl": false,
+  "aliases": {
+    "components": "@/components",
+    "utils": "@/lib/utils",
+    "ui": "@/components/ui",
+    "lib": "@/lib",
+    "hooks": "@/hooks"
+  },
+  "menuColor": "default",
+  "menuAccent": "subtle",
+  "registries": {}
+}

+ 78 - 0
components/ui/accordion.tsx

@@ -0,0 +1,78 @@
+import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
+
+import { cn } from '@/lib/utils';
+import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
+
+function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
+  return (
+    <AccordionPrimitive.Root
+      data-slot="accordion"
+      className={cn('flex w-full flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
+  return (
+    <AccordionPrimitive.Item
+      data-slot="accordion-item"
+      className={cn('not-last:border-b', className)}
+      {...props}
+    />
+  );
+}
+
+function AccordionTrigger({
+  className,
+  children,
+  ...props
+}: AccordionPrimitive.Trigger.Props) {
+  return (
+    <AccordionPrimitive.Header className="flex">
+      <AccordionPrimitive.Trigger
+        data-slot="accordion-trigger"
+        className={cn(
+          'focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground rounded-lg py-2.5 text-left text-sm font-medium hover:underline focus-visible:ring-3 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none aria-disabled:pointer-events-none aria-disabled:opacity-50',
+          className,
+        )}
+        {...props}
+      >
+        {children}
+        <ChevronDownIcon
+          data-slot="accordion-trigger-icon"
+          className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
+        />
+        <ChevronUpIcon
+          data-slot="accordion-trigger-icon"
+          className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
+        />
+      </AccordionPrimitive.Trigger>
+    </AccordionPrimitive.Header>
+  );
+}
+
+function AccordionContent({
+  className,
+  children,
+  ...props
+}: AccordionPrimitive.Panel.Props) {
+  return (
+    <AccordionPrimitive.Panel
+      data-slot="accordion-content"
+      className="data-open:animate-accordion-down data-closed:animate-accordion-up text-sm overflow-hidden"
+      {...props}
+    >
+      <div
+        className={cn(
+          'pt-0 pb-2.5 h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
+          className,
+        )}
+      >
+        {children}
+      </div>
+    </AccordionPrimitive.Panel>
+  );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

+ 187 - 0
components/ui/alert-dialog.tsx

@@ -0,0 +1,187 @@
+'use client';
+
+import * as React from 'react';
+import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+
+function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
+  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
+}
+
+function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
+  return (
+    <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
+  );
+}
+
+function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
+  return (
+    <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
+  );
+}
+
+function AlertDialogOverlay({
+  className,
+  ...props
+}: AlertDialogPrimitive.Backdrop.Props) {
+  return (
+    <AlertDialogPrimitive.Backdrop
+      data-slot="alert-dialog-overlay"
+      className={cn(
+        'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogContent({
+  className,
+  size = 'default',
+  ...props
+}: AlertDialogPrimitive.Popup.Props & {
+  size?: 'default' | 'sm';
+}) {
+  return (
+    <AlertDialogPortal>
+      <AlertDialogOverlay />
+      <AlertDialogPrimitive.Popup
+        data-slot="alert-dialog-content"
+        data-size={size}
+        className={cn(
+          'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none',
+          className,
+        )}
+        {...props}
+      />
+    </AlertDialogPortal>
+  );
+}
+
+function AlertDialogHeader({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="alert-dialog-header"
+      className={cn(
+        'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogFooter({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="alert-dialog-footer"
+      className={cn(
+        'bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogMedia({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="alert-dialog-media"
+      className={cn(
+        "bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
+  return (
+    <AlertDialogPrimitive.Title
+      data-slot="alert-dialog-title"
+      className={cn(
+        'text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2 cn-font-heading',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
+  return (
+    <AlertDialogPrimitive.Description
+      data-slot="alert-dialog-description"
+      className={cn(
+        'text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogAction({
+  className,
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  return (
+    <Button
+      data-slot="alert-dialog-action"
+      className={cn(className)}
+      {...props}
+    />
+  );
+}
+
+function AlertDialogCancel({
+  className,
+  variant = 'outline',
+  size = 'default',
+  ...props
+}: AlertDialogPrimitive.Close.Props &
+  Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
+  return (
+    <AlertDialogPrimitive.Close
+      data-slot="alert-dialog-cancel"
+      className={cn(className)}
+      render={<Button variant={variant} size={size} />}
+      {...props}
+    />
+  );
+}
+
+export {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogMedia,
+  AlertDialogOverlay,
+  AlertDialogPortal,
+  AlertDialogTitle,
+  AlertDialogTrigger,
+};

+ 76 - 0
components/ui/alert.tsx

@@ -0,0 +1,76 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const alertVariants = cva(
+  "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
+  {
+    variants: {
+      variant: {
+        default: 'bg-card text-card-foreground',
+        destructive:
+          'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+);
+
+function Alert({
+  className,
+  variant,
+  ...props
+}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
+  return (
+    <div
+      data-slot="alert"
+      role="alert"
+      className={cn(alertVariants({ variant }), className)}
+      {...props}
+    />
+  );
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="alert-title"
+      className={cn(
+        'font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertDescription({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="alert-description"
+      className={cn(
+        'text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="alert-action"
+      className={cn('absolute top-2 right-2', className)}
+      {...props}
+    />
+  );
+}
+
+export { Alert, AlertTitle, AlertDescription, AlertAction };

+ 22 - 0
components/ui/aspect-ratio.tsx

@@ -0,0 +1,22 @@
+import { cn } from '@/lib/utils';
+
+function AspectRatio({
+  ratio,
+  className,
+  ...props
+}: React.ComponentProps<'div'> & { ratio: number }) {
+  return (
+    <div
+      data-slot="aspect-ratio"
+      style={
+        {
+          '--ratio': ratio,
+        } as React.CSSProperties
+      }
+      className={cn('relative aspect-(--ratio)', className)}
+      {...props}
+    />
+  );
+}
+
+export { AspectRatio };

+ 207 - 0
components/ui/attachment.tsx

@@ -0,0 +1,207 @@
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+
+const attachmentVariants = cva(
+  'rounded-xl w-fit focus-within:ring-1 focus-within:ring-ring/50 group/attachment relative flex max-w-full min-w-0 shrink-0 flex-wrap border bg-card text-card-foreground transition-colors has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed',
+  {
+    variants: {
+      size: {
+        default:
+          'gap-2 has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2 text-sm',
+        sm: 'gap-2.5 has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5 text-xs',
+        xs: 'gap-1.5 has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1 text-xs rounded-lg',
+      },
+      orientation: {
+        horizontal: 'min-w-40 items-center',
+        vertical: 'w-24 has-data-[slot=attachment-content]:w-30 flex-col',
+      },
+    },
+  },
+);
+
+function Attachment({
+  className,
+  state = 'done',
+  size = 'default',
+  orientation = 'horizontal',
+  ...props
+}: React.ComponentProps<'div'> &
+  VariantProps<typeof attachmentVariants> & {
+    state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done';
+  }) {
+  return (
+    <div
+      data-slot="attachment"
+      data-state={state}
+      data-size={size}
+      data-orientation={orientation}
+      className={cn(attachmentVariants({ size, orientation }), className)}
+      {...props}
+    />
+  );
+}
+
+const attachmentMediaVariants = cva(
+  "bg-muted text-foreground w-10 rounded-lg group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md [&_svg:not([class*='size-'])]:size-4 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5 group-data-[orientation=vertical]/attachment:w-full group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! relative flex aspect-square shrink-0 items-center justify-center overflow-hidden group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive [&_svg]:pointer-events-none",
+  {
+    variants: {
+      variant: {
+        icon: '',
+        image:
+          'opacity-60 group-data-[state=idle]/attachment:opacity-100 group-data-[state=done]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover',
+      },
+    },
+    defaultVariants: {
+      variant: 'icon',
+    },
+  },
+);
+
+function AttachmentMedia({
+  className,
+  variant = 'icon',
+  ...props
+}: React.ComponentProps<'div'> & VariantProps<typeof attachmentMediaVariants>) {
+  return (
+    <div
+      data-slot="attachment-media"
+      data-variant={variant}
+      className={cn(attachmentMediaVariants({ variant }), className)}
+      {...props}
+    />
+  );
+}
+
+function AttachmentContent({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="attachment-content"
+      className={cn(
+        'leading-tight group-data-[orientation=vertical]/attachment:px-1 max-w-full min-w-0 flex-1',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AttachmentTitle({
+  className,
+  ...props
+}: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="attachment-title"
+      className={cn(
+        'font-medium block max-w-full min-w-0 truncate group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AttachmentDescription({
+  className,
+  ...props
+}: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="attachment-description"
+      className={cn(
+        'mt-0.5 text-xs block min-w-0 truncate text-muted-foreground group-data-[state=error]/attachment:text-destructive/80',
+        'max-w-full',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AttachmentActions({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="attachment-actions"
+      className={cn(
+        'group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 relative z-20 group-data-[orientation=vertical]/attachment:gap-1 flex shrink-0 items-center',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AttachmentAction({
+  className,
+  variant,
+  size = 'icon-xs',
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  return (
+    <Button
+      data-slot="attachment-action"
+      variant={variant ?? 'ghost'}
+      size={size}
+      className={cn(className)}
+      {...props}
+    />
+  );
+}
+
+function AttachmentTrigger({
+  className,
+  render,
+  type,
+  ...props
+}: useRender.ComponentProps<'button'>) {
+  return useRender({
+    defaultTagName: 'button',
+    props: mergeProps<'button'>(
+      {
+        type: render ? type : (type ?? 'button'),
+        className: cn('absolute inset-0 z-10 outline-none', className),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'attachment-trigger',
+    },
+  });
+}
+
+function AttachmentGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="attachment-group"
+      className={cn(
+        'gap-3 scroll-px-1 py-1 flex min-w-0 scroll-fade-x snap-x snap-mandatory scrollbar-none overflow-x-auto overscroll-x-contain *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Attachment,
+  AttachmentGroup,
+  AttachmentMedia,
+  AttachmentContent,
+  AttachmentTitle,
+  AttachmentDescription,
+  AttachmentActions,
+  AttachmentAction,
+  AttachmentTrigger,
+};

+ 109 - 0
components/ui/avatar.tsx

@@ -0,0 +1,109 @@
+'use client';
+
+import * as React from 'react';
+import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar';
+
+import { cn } from '@/lib/utils';
+
+function Avatar({
+  className,
+  size = 'default',
+  ...props
+}: AvatarPrimitive.Root.Props & {
+  size?: 'default' | 'sm' | 'lg';
+}) {
+  return (
+    <AvatarPrimitive.Root
+      data-slot="avatar"
+      data-size={size}
+      className={cn(
+        'size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
+  return (
+    <AvatarPrimitive.Image
+      data-slot="avatar-image"
+      className={cn(
+        'rounded-full aspect-square size-full object-cover',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AvatarFallback({
+  className,
+  ...props
+}: AvatarPrimitive.Fallback.Props) {
+  return (
+    <AvatarPrimitive.Fallback
+      data-slot="avatar-fallback"
+      className={cn(
+        'bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="avatar-badge"
+      className={cn(
+        'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none',
+        'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
+        'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
+        'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="avatar-group"
+      className={cn(
+        'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function AvatarGroupCount({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="avatar-group-count"
+      className={cn(
+        'bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 relative flex shrink-0 items-center justify-center ring-2 ring-background',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Avatar,
+  AvatarImage,
+  AvatarFallback,
+  AvatarGroup,
+  AvatarGroupCount,
+  AvatarBadge,
+};

+ 52 - 0
components/ui/badge.tsx

@@ -0,0 +1,52 @@
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const badgeVariants = cva(
+  'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
+  {
+    variants: {
+      variant: {
+        default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
+        secondary:
+          'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
+        destructive:
+          'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
+        outline:
+          'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
+        ghost:
+          'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
+        link: 'text-primary underline-offset-4 hover:underline',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+);
+
+function Badge({
+  className,
+  variant = 'default',
+  render,
+  ...props
+}: useRender.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) {
+  return useRender({
+    defaultTagName: 'span',
+    props: mergeProps<'span'>(
+      {
+        className: cn(badgeVariants({ variant }), className),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'badge',
+      variant,
+    },
+  });
+}
+
+export { Badge, badgeVariants };

+ 122 - 0
components/ui/breadcrumb.tsx

@@ -0,0 +1,122 @@
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+
+import { cn } from '@/lib/utils';
+import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react';
+
+function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) {
+  return (
+    <nav
+      aria-label="breadcrumb"
+      data-slot="breadcrumb"
+      className={cn(className)}
+      {...props}
+    />
+  );
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
+  return (
+    <ol
+      data-slot="breadcrumb-list"
+      className={cn(
+        'text-muted-foreground gap-1.5 text-sm flex flex-wrap items-center wrap-break-word',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
+  return (
+    <li
+      data-slot="breadcrumb-item"
+      className={cn('gap-1 inline-flex items-center', className)}
+      {...props}
+    />
+  );
+}
+
+function BreadcrumbLink({
+  className,
+  render,
+  ...props
+}: useRender.ComponentProps<'a'>) {
+  return useRender({
+    defaultTagName: 'a',
+    props: mergeProps<'a'>(
+      {
+        className: cn('hover:text-foreground transition-colors', className),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'breadcrumb-link',
+    },
+  });
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="breadcrumb-page"
+      role="link"
+      aria-disabled="true"
+      aria-current="page"
+      className={cn('text-foreground font-normal', className)}
+      {...props}
+    />
+  );
+}
+
+function BreadcrumbSeparator({
+  children,
+  className,
+  ...props
+}: React.ComponentProps<'li'>) {
+  return (
+    <li
+      data-slot="breadcrumb-separator"
+      role="presentation"
+      aria-hidden="true"
+      className={cn('[&>svg]:size-3.5', className)}
+      {...props}
+    >
+      {children ?? <ChevronRightIcon className="cn-rtl-flip" />}
+    </li>
+  );
+}
+
+function BreadcrumbEllipsis({
+  className,
+  ...props
+}: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="breadcrumb-ellipsis"
+      role="presentation"
+      aria-hidden="true"
+      className={cn(
+        'size-5 [&>svg]:size-4 flex items-center justify-center',
+        className,
+      )}
+      {...props}
+    >
+      <MoreHorizontalIcon />
+      <span className="sr-only">More</span>
+    </span>
+  );
+}
+
+export {
+  Breadcrumb,
+  BreadcrumbList,
+  BreadcrumbItem,
+  BreadcrumbLink,
+  BreadcrumbPage,
+  BreadcrumbSeparator,
+  BreadcrumbEllipsis,
+};

+ 128 - 0
components/ui/bubble.tsx

@@ -0,0 +1,128 @@
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+function BubbleGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="bubble-group"
+      className={cn('gap-2 flex min-w-0 flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+const bubbleVariants = cva(
+  'gap-1 data-[align=end]:self-end max-w-[80%] data-[variant=ghost]:max-w-full group-data-[align=end]/message:self-end group/bubble relative flex w-fit min-w-0 flex-col',
+  {
+    variants: {
+      variant: {
+        default:
+          '*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80',
+        secondary:
+          '*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]',
+        muted:
+          '*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]',
+        tinted:
+          '*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]',
+        outline:
+          '*:data-[slot=bubble-content]:bg-background *:data-[slot=bubble-content]:border-border [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30',
+        ghost:
+          '*:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50 border-none',
+        destructive:
+          '*:data-[slot=bubble-content]:bg-destructive/10 dark:*:data-[slot=bubble-content]:bg-destructive/20 *:data-[slot=bubble-content]:text-destructive [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+);
+
+function Bubble({
+  variant = 'default',
+  align = 'start',
+  className,
+  ...props
+}: React.ComponentProps<'div'> &
+  VariantProps<typeof bubbleVariants> & {
+    align?: 'start' | 'end';
+  }) {
+  return (
+    <div
+      data-slot="bubble"
+      data-variant={variant}
+      data-align={align}
+      className={cn(bubbleVariants({ variant }), className)}
+      {...props}
+    />
+  );
+}
+
+function BubbleContent({
+  className,
+  render,
+  ...props
+}: useRender.ComponentProps<'div'>) {
+  return useRender({
+    defaultTagName: 'div',
+    props: mergeProps<'div'>(
+      {
+        className: cn(
+          'rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50 group-data-[align=end]/bubble:self-end w-fit max-w-full min-w-0 overflow-hidden wrap-break-word [button]:text-left [button,a]:transition-colors',
+          className,
+        ),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'bubble-content',
+    },
+  });
+}
+
+const bubbleReactionsVariants = cva(
+  'rounded-full ring-3 ring-card bg-muted shrink-0 gap-1 px-1.5 py-0.5 has-[button]:p-0 text-sm absolute z-10 flex w-fit items-center justify-center',
+  {
+    variants: {
+      side: {
+        top: 'top-0 -translate-y-3/4',
+        bottom: 'bottom-0 translate-y-3/4',
+      },
+      align: {
+        start: 'left-3',
+        end: 'right-3',
+      },
+    },
+    defaultVariants: {
+      side: 'bottom',
+      align: 'end',
+    },
+  },
+);
+
+function BubbleReactions({
+  side = 'bottom',
+  align = 'end',
+  className,
+  ...props
+}: React.ComponentProps<'div'> & {
+  align?: 'start' | 'end';
+  side?: 'top' | 'bottom';
+}) {
+  return (
+    <div
+      data-slot="bubble-reactions"
+      data-align={align}
+      data-side={side}
+      className={cn(bubbleReactionsVariants({ side, align }), className)}
+      {...props}
+    />
+  );
+}
+
+export { BubbleGroup, Bubble, BubbleContent, BubbleReactions };

+ 87 - 0
components/ui/button-group.tsx

@@ -0,0 +1,87 @@
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { Separator } from '@/components/ui/separator';
+
+const buttonGroupVariants = cva(
+  "has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
+  {
+    variants: {
+      orientation: {
+        horizontal:
+          '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! *:data-slot:rounded-r-none [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0',
+        vertical:
+          '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! flex-col *:data-slot:rounded-b-none [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0',
+      },
+    },
+    defaultVariants: {
+      orientation: 'horizontal',
+    },
+  },
+);
+
+function ButtonGroup({
+  className,
+  orientation,
+  ...props
+}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
+  return (
+    <div
+      role="group"
+      data-slot="button-group"
+      data-orientation={orientation}
+      className={cn(buttonGroupVariants({ orientation }), className)}
+      {...props}
+    />
+  );
+}
+
+function ButtonGroupText({
+  className,
+  render,
+  ...props
+}: useRender.ComponentProps<'div'>) {
+  return useRender({
+    defaultTagName: 'div',
+    props: mergeProps<'div'>(
+      {
+        className: cn(
+          "bg-muted gap-2 rounded-lg border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
+          className,
+        ),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'button-group-text',
+    },
+  });
+}
+
+function ButtonGroupSeparator({
+  className,
+  orientation = 'vertical',
+  ...props
+}: React.ComponentProps<typeof Separator>) {
+  return (
+    <Separator
+      data-slot="button-group-separator"
+      orientation={orientation}
+      className={cn(
+        'bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  ButtonGroup,
+  ButtonGroupSeparator,
+  ButtonGroupText,
+  buttonGroupVariants,
+};

+ 58 - 0
components/ui/button.tsx

@@ -0,0 +1,58 @@
+import { Button as ButtonPrimitive } from '@base-ui/react/button';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+  "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+  {
+    variants: {
+      variant: {
+        default: 'bg-primary text-primary-foreground hover:bg-primary/80',
+        outline:
+          'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
+        secondary:
+          'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
+        ghost:
+          'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
+        destructive:
+          'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
+        link: 'text-primary underline-offset-4 hover:underline',
+      },
+      size: {
+        default:
+          'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
+        xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+        sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+        lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
+        icon: 'size-8',
+        'icon-xs':
+          "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
+        'icon-sm':
+          'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
+        'icon-lg': 'size-9',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  },
+);
+
+function Button({
+  className,
+  variant = 'default',
+  size = 'default',
+  ...props
+}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
+  return (
+    <ButtonPrimitive
+      data-slot="button"
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  );
+}
+
+export { Button, buttonVariants };

+ 231 - 0
components/ui/calendar.tsx

@@ -0,0 +1,231 @@
+'use client';
+
+import * as React from 'react';
+import {
+  DayPicker,
+  getDefaultClassNames,
+  type DayButton,
+  type Locale,
+} from 'react-day-picker';
+
+import { cn } from '@/lib/utils';
+import { Button, buttonVariants } from '@/components/ui/button';
+import {
+  ChevronLeftIcon,
+  ChevronRightIcon,
+  ChevronDownIcon,
+} from 'lucide-react';
+
+function Calendar({
+  className,
+  classNames,
+  showOutsideDays = true,
+  captionLayout = 'label',
+  buttonVariant = 'ghost',
+  locale,
+  formatters,
+  components,
+  ...props
+}: React.ComponentProps<typeof DayPicker> & {
+  buttonVariant?: React.ComponentProps<typeof Button>['variant'];
+}) {
+  const defaultClassNames = getDefaultClassNames();
+
+  return (
+    <DayPicker
+      showOutsideDays={showOutsideDays}
+      className={cn(
+        'p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
+        String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
+        String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+        className,
+      )}
+      captionLayout={captionLayout}
+      locale={locale}
+      formatters={{
+        formatMonthDropdown: (date) =>
+          date.toLocaleString(locale?.code, { month: 'short' }),
+        ...formatters,
+      }}
+      classNames={{
+        root: cn('w-fit', defaultClassNames.root),
+        months: cn(
+          'relative flex flex-col gap-4 md:flex-row',
+          defaultClassNames.months,
+        ),
+        month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
+        nav: cn(
+          'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
+          defaultClassNames.nav,
+        ),
+        button_previous: cn(
+          buttonVariants({ variant: buttonVariant }),
+          'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
+          defaultClassNames.button_previous,
+        ),
+        button_next: cn(
+          buttonVariants({ variant: buttonVariant }),
+          'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
+          defaultClassNames.button_next,
+        ),
+        month_caption: cn(
+          'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
+          defaultClassNames.month_caption,
+        ),
+        dropdowns: cn(
+          'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
+          defaultClassNames.dropdowns,
+        ),
+        dropdown_root: cn(
+          'cn-calendar-dropdown-root relative rounded-(--cell-radius)',
+          defaultClassNames.dropdown_root,
+        ),
+        dropdown: cn(
+          'absolute inset-0 bg-popover opacity-0',
+          defaultClassNames.dropdown,
+        ),
+        caption_label: cn(
+          'font-medium select-none',
+          captionLayout === 'label'
+            ? 'cn-calendar-caption text-sm'
+            : 'cn-calendar-caption-label flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
+          defaultClassNames.caption_label,
+        ),
+        month_grid: cn('w-full border-collapse', defaultClassNames.month_grid),
+        weekdays: cn('flex', defaultClassNames.weekdays),
+        weekday: cn(
+          'flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none',
+          defaultClassNames.weekday,
+        ),
+        week: cn('mt-2 flex w-full', defaultClassNames.week),
+        week_number_header: cn(
+          'w-(--cell-size) select-none',
+          defaultClassNames.week_number_header,
+        ),
+        week_number: cn(
+          'text-[0.8rem] text-muted-foreground select-none',
+          defaultClassNames.week_number,
+        ),
+        day: cn(
+          'group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
+          props.showWeekNumber
+            ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
+            : '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
+          defaultClassNames.day,
+        ),
+        range_start: cn(
+          'relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
+          defaultClassNames.range_start,
+        ),
+        range_middle: cn('rounded-none', defaultClassNames.range_middle),
+        range_end: cn(
+          'relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
+          defaultClassNames.range_end,
+        ),
+        today: cn(
+          'rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none',
+          defaultClassNames.today,
+        ),
+        outside: cn(
+          'text-muted-foreground aria-selected:text-muted-foreground',
+          defaultClassNames.outside,
+        ),
+        disabled: cn(
+          'text-muted-foreground opacity-50',
+          defaultClassNames.disabled,
+        ),
+        hidden: cn('invisible', defaultClassNames.hidden),
+        ...classNames,
+      }}
+      components={{
+        Root: ({ className, rootRef, ...props }) => {
+          return (
+            <div
+              data-slot="calendar"
+              ref={rootRef}
+              className={cn(className)}
+              {...props}
+            />
+          );
+        },
+        Chevron: ({ className, orientation, ...props }) => {
+          if (orientation === 'left') {
+            return (
+              <ChevronLeftIcon
+                className={cn('cn-rtl-flip size-4', className)}
+                {...props}
+              />
+            );
+          }
+
+          if (orientation === 'right') {
+            return (
+              <ChevronRightIcon
+                className={cn('cn-rtl-flip size-4', className)}
+                {...props}
+              />
+            );
+          }
+
+          return (
+            <ChevronDownIcon className={cn('size-4', className)} {...props} />
+          );
+        },
+        DayButton: ({ ...props }) => (
+          <CalendarDayButton locale={locale} {...props} />
+        ),
+        WeekNumber: ({ children, ...props }) => {
+          return (
+            <td {...props}>
+              <div className="flex size-(--cell-size) items-center justify-center text-center">
+                {children}
+              </div>
+            </td>
+          );
+        },
+        ...components,
+      }}
+      {...props}
+    />
+  );
+}
+
+function CalendarDayButton({
+  className,
+  day,
+  modifiers,
+  locale,
+  ...props
+}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
+  const defaultClassNames = getDefaultClassNames();
+
+  const ref = React.useRef<HTMLButtonElement>(null);
+  React.useEffect(() => {
+    if (modifiers.focused) ref.current?.focus();
+  }, [modifiers.focused]);
+
+  return (
+    <Button
+      variant="ghost"
+      size="icon"
+      data-day={day.date.toLocaleDateString(locale?.code)}
+      data-selected-single={
+        modifiers.selected &&
+        !modifiers.range_start &&
+        !modifiers.range_end &&
+        !modifiers.range_middle
+      }
+      data-range-start={modifiers.range_start}
+      data-range-end={modifiers.range_end}
+      data-range-middle={modifiers.range_middle}
+      className={cn(
+        'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
+        defaultClassNames.day,
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Calendar, CalendarDayButton };

+ 103 - 0
components/ui/card.tsx

@@ -0,0 +1,103 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+function Card({
+  className,
+  size = 'default',
+  ...props
+}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
+  return (
+    <div
+      data-slot="card"
+      data-size={size}
+      className={cn(
+        'group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card-header"
+      className={cn(
+        'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card-title"
+      className={cn(
+        'font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card-description"
+      className={cn('text-sm text-muted-foreground', className)}
+      {...props}
+    />
+  );
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card-action"
+      className={cn(
+        'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card-content"
+      className={cn('px-(--card-spacing)', className)}
+      {...props}
+    />
+  );
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="card-footer"
+      className={cn(
+        'flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Card,
+  CardHeader,
+  CardFooter,
+  CardTitle,
+  CardAction,
+  CardDescription,
+  CardContent,
+};

+ 242 - 0
components/ui/carousel.tsx

@@ -0,0 +1,242 @@
+'use client';
+
+import * as React from 'react';
+import useEmblaCarousel, {
+  type UseEmblaCarouselType,
+} from 'embla-carousel-react';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+  opts?: CarouselOptions;
+  plugins?: CarouselPlugin;
+  orientation?: 'horizontal' | 'vertical';
+  setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+  carouselRef: ReturnType<typeof useEmblaCarousel>[0];
+  api: ReturnType<typeof useEmblaCarousel>[1];
+  scrollPrev: () => void;
+  scrollNext: () => void;
+  canScrollPrev: boolean;
+  canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext<CarouselContextProps | null>(null);
+
+function useCarousel() {
+  const context = React.useContext(CarouselContext);
+
+  if (!context) {
+    throw new Error('useCarousel must be used within a <Carousel />');
+  }
+
+  return context;
+}
+
+function Carousel({
+  orientation = 'horizontal',
+  opts,
+  setApi,
+  plugins,
+  className,
+  children,
+  ...props
+}: React.ComponentProps<'div'> & CarouselProps) {
+  const [carouselRef, api] = useEmblaCarousel(
+    {
+      ...opts,
+      axis: orientation === 'horizontal' ? 'x' : 'y',
+    },
+    plugins,
+  );
+  const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+  const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+  const onSelect = React.useCallback((api: CarouselApi) => {
+    if (!api) return;
+    setCanScrollPrev(api.canScrollPrev());
+    setCanScrollNext(api.canScrollNext());
+  }, []);
+
+  const scrollPrev = React.useCallback(() => {
+    api?.scrollPrev();
+  }, [api]);
+
+  const scrollNext = React.useCallback(() => {
+    api?.scrollNext();
+  }, [api]);
+
+  const handleKeyDown = React.useCallback(
+    (event: React.KeyboardEvent<HTMLDivElement>) => {
+      if (event.key === 'ArrowLeft') {
+        event.preventDefault();
+        scrollPrev();
+      } else if (event.key === 'ArrowRight') {
+        event.preventDefault();
+        scrollNext();
+      }
+    },
+    [scrollPrev, scrollNext],
+  );
+
+  React.useEffect(() => {
+    if (!api || !setApi) return;
+    setApi(api);
+  }, [api, setApi]);
+
+  React.useEffect(() => {
+    if (!api) return;
+    onSelect(api);
+    api.on('reInit', onSelect);
+    api.on('select', onSelect);
+
+    return () => {
+      api?.off('select', onSelect);
+    };
+  }, [api, onSelect]);
+
+  return (
+    <CarouselContext.Provider
+      value={{
+        carouselRef,
+        api: api,
+        opts,
+        orientation:
+          orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
+        scrollPrev,
+        scrollNext,
+        canScrollPrev,
+        canScrollNext,
+      }}
+    >
+      <div
+        onKeyDownCapture={handleKeyDown}
+        className={cn('relative', className)}
+        role="region"
+        aria-roledescription="carousel"
+        data-slot="carousel"
+        {...props}
+      >
+        {children}
+      </div>
+    </CarouselContext.Provider>
+  );
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
+  const { carouselRef, orientation } = useCarousel();
+
+  return (
+    <div
+      ref={carouselRef}
+      className="overflow-hidden"
+      data-slot="carousel-content"
+    >
+      <div
+        className={cn(
+          'flex',
+          orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
+          className,
+        )}
+        {...props}
+      />
+    </div>
+  );
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
+  const { orientation } = useCarousel();
+
+  return (
+    <div
+      role="group"
+      aria-roledescription="slide"
+      data-slot="carousel-item"
+      className={cn(
+        'min-w-0 shrink-0 grow-0 basis-full',
+        orientation === 'horizontal' ? 'pl-4' : 'pt-4',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function CarouselPrevious({
+  className,
+  variant = 'outline',
+  size = 'icon-sm',
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+  return (
+    <Button
+      data-slot="carousel-previous"
+      variant={variant}
+      size={size}
+      className={cn(
+        'rounded-full absolute touch-manipulation',
+        orientation === 'horizontal'
+          ? 'inset-y-0 -left-12 my-auto'
+          : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
+        className,
+      )}
+      disabled={!canScrollPrev}
+      onClick={scrollPrev}
+      {...props}
+    >
+      <ChevronLeftIcon className="cn-rtl-flip" />
+      <span className="sr-only">Previous slide</span>
+    </Button>
+  );
+}
+
+function CarouselNext({
+  className,
+  variant = 'outline',
+  size = 'icon-sm',
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+  return (
+    <Button
+      data-slot="carousel-next"
+      variant={variant}
+      size={size}
+      className={cn(
+        'rounded-full absolute touch-manipulation',
+        orientation === 'horizontal'
+          ? 'inset-y-0 -right-12 my-auto'
+          : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
+        className,
+      )}
+      disabled={!canScrollNext}
+      onClick={scrollNext}
+      {...props}
+    >
+      <ChevronRightIcon className="cn-rtl-flip" />
+      <span className="sr-only">Next slide</span>
+    </Button>
+  );
+}
+
+export {
+  type CarouselApi,
+  Carousel,
+  CarouselContent,
+  CarouselItem,
+  CarouselPrevious,
+  CarouselNext,
+  useCarousel,
+};

+ 373 - 0
components/ui/chart.tsx

@@ -0,0 +1,373 @@
+'use client';
+
+import * as React from 'react';
+import * as RechartsPrimitive from 'recharts';
+import type { TooltipValueType } from 'recharts';
+
+import { cn } from '@/lib/utils';
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: '', dark: '.dark' } as const;
+
+const INITIAL_DIMENSION = { width: 320, height: 200 } as const;
+type TooltipNameType = number | string;
+
+export type ChartConfig = Record<
+  string,
+  {
+    label?: React.ReactNode;
+    icon?: React.ComponentType;
+  } & (
+    | { color?: string; theme?: never }
+    | { color?: never; theme: Record<keyof typeof THEMES, string> }
+  )
+>;
+
+type ChartContextProps = {
+  config: ChartConfig;
+};
+
+const ChartContext = React.createContext<ChartContextProps | null>(null);
+
+function useChart() {
+  const context = React.useContext(ChartContext);
+
+  if (!context) {
+    throw new Error('useChart must be used within a <ChartContainer />');
+  }
+
+  return context;
+}
+
+function ChartContainer({
+  id,
+  className,
+  children,
+  config,
+  initialDimension = INITIAL_DIMENSION,
+  ...props
+}: React.ComponentProps<'div'> & {
+  config: ChartConfig;
+  children: React.ComponentProps<
+    typeof RechartsPrimitive.ResponsiveContainer
+  >['children'];
+  initialDimension?: {
+    width: number;
+    height: number;
+  };
+}) {
+  const uniqueId = React.useId();
+  const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`;
+
+  return (
+    <ChartContext.Provider value={{ config }}>
+      <div
+        data-slot="chart"
+        data-chart={chartId}
+        className={cn(
+          "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
+          className,
+        )}
+        {...props}
+      >
+        <ChartStyle id={chartId} config={config} />
+        <RechartsPrimitive.ResponsiveContainer
+          initialDimension={initialDimension}
+        >
+          {children}
+        </RechartsPrimitive.ResponsiveContainer>
+      </div>
+    </ChartContext.Provider>
+  );
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+  const colorConfig = Object.entries(config).filter(
+    ([, config]) => config.theme ?? config.color,
+  );
+
+  if (!colorConfig.length) {
+    return null;
+  }
+
+  return (
+    <style
+      dangerouslySetInnerHTML={{
+        __html: Object.entries(THEMES)
+          .map(
+            ([theme, prefix]) => `
+${prefix} [data-chart=${id}] {
+${colorConfig
+  .map(([key, itemConfig]) => {
+    const color =
+      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
+      itemConfig.color;
+    return color ? `  --color-${key}: ${color};` : null;
+  })
+  .join('\n')}
+}
+`,
+          )
+          .join('\n'),
+      }}
+    />
+  );
+};
+
+const ChartTooltip = RechartsPrimitive.Tooltip;
+
+function ChartTooltipContent({
+  active,
+  payload,
+  className,
+  indicator = 'dot',
+  hideLabel = false,
+  hideIndicator = false,
+  label,
+  labelFormatter,
+  labelClassName,
+  formatter,
+  color,
+  nameKey,
+  labelKey,
+}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
+  React.ComponentProps<'div'> & {
+    hideLabel?: boolean;
+    hideIndicator?: boolean;
+    indicator?: 'line' | 'dot' | 'dashed';
+    nameKey?: string;
+    labelKey?: string;
+  } & Omit<
+    RechartsPrimitive.DefaultTooltipContentProps<
+      TooltipValueType,
+      TooltipNameType
+    >,
+    'accessibilityLayer'
+  >) {
+  const { config } = useChart();
+
+  const tooltipLabel = React.useMemo(() => {
+    if (hideLabel || !payload?.length) {
+      return null;
+    }
+
+    const [item] = payload;
+    const key = `${labelKey ?? item?.dataKey ?? item?.name ?? 'value'}`;
+    const itemConfig = getPayloadConfigFromPayload(config, item, key);
+    const value =
+      !labelKey && typeof label === 'string'
+        ? (config[label]?.label ?? label)
+        : itemConfig?.label;
+
+    if (labelFormatter) {
+      return (
+        <div className={cn('font-medium', labelClassName)}>
+          {labelFormatter(value, payload)}
+        </div>
+      );
+    }
+
+    if (!value) {
+      return null;
+    }
+
+    return <div className={cn('font-medium', labelClassName)}>{value}</div>;
+  }, [
+    label,
+    labelFormatter,
+    payload,
+    hideLabel,
+    labelClassName,
+    config,
+    labelKey,
+  ]);
+
+  if (!active || !payload?.length) {
+    return null;
+  }
+
+  const nestLabel = payload.length === 1 && indicator !== 'dot';
+
+  return (
+    <div
+      className={cn(
+        'border-border/50 bg-background gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl grid min-w-32 items-start',
+        className,
+      )}
+    >
+      {!nestLabel ? tooltipLabel : null}
+      <div className="grid gap-1.5">
+        {payload
+          .filter((item) => item.type !== 'none')
+          .map((item, index) => {
+            const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`;
+            const itemConfig = getPayloadConfigFromPayload(config, item, key);
+            const indicatorColor = color ?? item.payload?.fill ?? item.color;
+
+            return (
+              <div
+                key={index}
+                className={cn(
+                  'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
+                  indicator === 'dot' && 'items-center',
+                )}
+              >
+                {formatter && item?.value !== undefined && item.name ? (
+                  formatter(item.value, item.name, item, index, item.payload)
+                ) : (
+                  <>
+                    {itemConfig?.icon ? (
+                      <itemConfig.icon />
+                    ) : (
+                      !hideIndicator && (
+                        <div
+                          className={cn(
+                            'shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)',
+                            {
+                              'h-2.5 w-2.5': indicator === 'dot',
+                              'w-1': indicator === 'line',
+                              'w-0 border-[1.5px] border-dashed bg-transparent':
+                                indicator === 'dashed',
+                              'my-0.5': nestLabel && indicator === 'dashed',
+                            },
+                          )}
+                          style={
+                            {
+                              '--color-bg': indicatorColor,
+                              '--color-border': indicatorColor,
+                            } as React.CSSProperties
+                          }
+                        />
+                      )
+                    )}
+                    <div
+                      className={cn(
+                        'flex flex-1 justify-between leading-none',
+                        nestLabel ? 'items-end' : 'items-center',
+                      )}
+                    >
+                      <div className="grid gap-1.5">
+                        {nestLabel ? tooltipLabel : null}
+                        <span className="text-muted-foreground">
+                          {itemConfig?.label ?? item.name}
+                        </span>
+                      </div>
+                      {item.value != null && (
+                        <span className="font-mono font-medium text-foreground tabular-nums">
+                          {typeof item.value === 'number'
+                            ? item.value.toLocaleString()
+                            : String(item.value)}
+                        </span>
+                      )}
+                    </div>
+                  </>
+                )}
+              </div>
+            );
+          })}
+      </div>
+    </div>
+  );
+}
+
+const ChartLegend = RechartsPrimitive.Legend;
+
+function ChartLegendContent({
+  className,
+  hideIcon = false,
+  payload,
+  verticalAlign = 'bottom',
+  nameKey,
+}: React.ComponentProps<'div'> & {
+  hideIcon?: boolean;
+  nameKey?: string;
+} & RechartsPrimitive.DefaultLegendContentProps) {
+  const { config } = useChart();
+
+  if (!payload?.length) {
+    return null;
+  }
+
+  return (
+    <div
+      className={cn(
+        'flex items-center justify-center gap-4',
+        verticalAlign === 'top' ? 'pb-3' : 'pt-3',
+        className,
+      )}
+    >
+      {payload
+        .filter((item) => item.type !== 'none')
+        .map((item, index) => {
+          const key = `${nameKey ?? item.dataKey ?? 'value'}`;
+          const itemConfig = getPayloadConfigFromPayload(config, item, key);
+
+          return (
+            <div
+              key={index}
+              className={cn(
+                'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground',
+              )}
+            >
+              {itemConfig?.icon && !hideIcon ? (
+                <itemConfig.icon />
+              ) : (
+                <div
+                  className="h-2 w-2 shrink-0 rounded-[2px]"
+                  style={{
+                    backgroundColor: item.color,
+                  }}
+                />
+              )}
+              {itemConfig?.label}
+            </div>
+          );
+        })}
+    </div>
+  );
+}
+
+function getPayloadConfigFromPayload(
+  config: ChartConfig,
+  payload: unknown,
+  key: string,
+) {
+  if (typeof payload !== 'object' || payload === null) {
+    return undefined;
+  }
+
+  const payloadPayload =
+    'payload' in payload &&
+    typeof payload.payload === 'object' &&
+    payload.payload !== null
+      ? payload.payload
+      : undefined;
+
+  let configLabelKey: string = key;
+
+  if (
+    key in payload &&
+    typeof payload[key as keyof typeof payload] === 'string'
+  ) {
+    configLabelKey = payload[key as keyof typeof payload] as string;
+  } else if (
+    payloadPayload &&
+    key in payloadPayload &&
+    typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
+  ) {
+    configLabelKey = payloadPayload[
+      key as keyof typeof payloadPayload
+    ] as string;
+  }
+
+  return configLabelKey in config ? config[configLabelKey] : config[key];
+}
+
+export {
+  ChartContainer,
+  ChartTooltip,
+  ChartTooltipContent,
+  ChartLegend,
+  ChartLegendContent,
+  ChartStyle,
+};

+ 28 - 0
components/ui/checkbox.tsx

@@ -0,0 +1,28 @@
+'use client';
+
+import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox';
+
+import { cn } from '@/lib/utils';
+import { CheckIcon } from 'lucide-react';
+
+function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
+  return (
+    <CheckboxPrimitive.Root
+      data-slot="checkbox"
+      className={cn(
+        'peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary',
+        className,
+      )}
+      {...props}
+    >
+      <CheckboxPrimitive.Indicator
+        data-slot="checkbox-indicator"
+        className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
+      >
+        <CheckIcon />
+      </CheckboxPrimitive.Indicator>
+    </CheckboxPrimitive.Root>
+  );
+}
+
+export { Checkbox };

+ 21 - 0
components/ui/collapsible.tsx

@@ -0,0 +1,21 @@
+'use client';
+
+import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
+
+function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
+  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
+}
+
+function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
+  return (
+    <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
+  );
+}
+
+function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
+  return (
+    <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
+  );
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };

+ 300 - 0
components/ui/combobox.tsx

@@ -0,0 +1,300 @@
+'use client';
+
+import * as React from 'react';
+import { Combobox as ComboboxPrimitive } from '@base-ui/react';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import {
+  InputGroup,
+  InputGroupAddon,
+  InputGroupButton,
+  InputGroupInput,
+} from '@/components/ui/input-group';
+import { ChevronDownIcon, XIcon, CheckIcon } from 'lucide-react';
+
+const Combobox = ComboboxPrimitive.Root;
+
+function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
+  return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
+}
+
+function ComboboxTrigger({
+  className,
+  children,
+  ...props
+}: ComboboxPrimitive.Trigger.Props) {
+  return (
+    <ComboboxPrimitive.Trigger
+      data-slot="combobox-trigger"
+      className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
+      {...props}
+    >
+      {children}
+      <ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" />
+    </ComboboxPrimitive.Trigger>
+  );
+}
+
+function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
+  return (
+    <ComboboxPrimitive.Clear
+      data-slot="combobox-clear"
+      render={<InputGroupButton variant="ghost" size="icon-xs" />}
+      className={cn(className)}
+      {...props}
+    >
+      <XIcon className="pointer-events-none" />
+    </ComboboxPrimitive.Clear>
+  );
+}
+
+function ComboboxInput({
+  className,
+  children,
+  disabled = false,
+  showTrigger = true,
+  showClear = false,
+  ...props
+}: ComboboxPrimitive.Input.Props & {
+  showTrigger?: boolean;
+  showClear?: boolean;
+}) {
+  return (
+    <InputGroup className={cn('w-auto', className)}>
+      <ComboboxPrimitive.Input
+        render={<InputGroupInput disabled={disabled} />}
+        {...props}
+      />
+      <InputGroupAddon align="inline-end">
+        {showTrigger && (
+          <InputGroupButton
+            size="icon-xs"
+            variant="ghost"
+            render={<ComboboxTrigger />}
+            data-slot="input-group-button"
+            className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
+            disabled={disabled}
+          />
+        )}
+        {showClear && <ComboboxClear disabled={disabled} />}
+      </InputGroupAddon>
+      {children}
+    </InputGroup>
+  );
+}
+
+function ComboboxContent({
+  className,
+  side = 'bottom',
+  sideOffset = 6,
+  align = 'start',
+  alignOffset = 0,
+  anchor,
+  ...props
+}: ComboboxPrimitive.Popup.Props &
+  Pick<
+    ComboboxPrimitive.Positioner.Props,
+    'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor'
+  >) {
+  return (
+    <ComboboxPrimitive.Portal>
+      <ComboboxPrimitive.Positioner
+        side={side}
+        sideOffset={sideOffset}
+        align={align}
+        alignOffset={alignOffset}
+        anchor={anchor}
+        className="isolate z-50"
+      >
+        <ComboboxPrimitive.Popup
+          data-slot="combobox-content"
+          data-chips={!!anchor}
+          className={cn(
+            'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 overflow-hidden rounded-lg shadow-md ring-1 duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 cn-menu-target cn-menu-translucent group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)',
+            className,
+          )}
+          {...props}
+        />
+      </ComboboxPrimitive.Positioner>
+    </ComboboxPrimitive.Portal>
+  );
+}
+
+function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
+  return (
+    <ComboboxPrimitive.List
+      data-slot="combobox-list"
+      className={cn(
+        'no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 p-1 data-empty:p-0 overflow-y-auto overscroll-contain',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ComboboxItem({
+  className,
+  children,
+  ...props
+}: ComboboxPrimitive.Item.Props) {
+  return (
+    <ComboboxPrimitive.Item
+      data-slot="combobox-item"
+      className={cn(
+        "data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      <ComboboxPrimitive.ItemIndicator
+        render={
+          <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
+        }
+      >
+        <CheckIcon className="pointer-events-none" />
+      </ComboboxPrimitive.ItemIndicator>
+    </ComboboxPrimitive.Item>
+  );
+}
+
+function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
+  return (
+    <ComboboxPrimitive.Group
+      data-slot="combobox-group"
+      className={cn(className)}
+      {...props}
+    />
+  );
+}
+
+function ComboboxLabel({
+  className,
+  ...props
+}: ComboboxPrimitive.GroupLabel.Props) {
+  return (
+    <ComboboxPrimitive.GroupLabel
+      data-slot="combobox-label"
+      className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
+      {...props}
+    />
+  );
+}
+
+function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
+  return (
+    <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
+  );
+}
+
+function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
+  return (
+    <ComboboxPrimitive.Empty
+      data-slot="combobox-empty"
+      className={cn(
+        'text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ComboboxSeparator({
+  className,
+  ...props
+}: ComboboxPrimitive.Separator.Props) {
+  return (
+    <ComboboxPrimitive.Separator
+      data-slot="combobox-separator"
+      className={cn('bg-border -mx-1 my-1 h-px', className)}
+      {...props}
+    />
+  );
+}
+
+function ComboboxChips({
+  className,
+  ...props
+}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
+  ComboboxPrimitive.Chips.Props) {
+  return (
+    <ComboboxPrimitive.Chips
+      data-slot="combobox-chips"
+      className={cn(
+        'dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-8 flex-wrap items-center gap-1 rounded-lg border bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ComboboxChip({
+  className,
+  children,
+  showRemove = true,
+  ...props
+}: ComboboxPrimitive.Chip.Props & {
+  showRemove?: boolean;
+}) {
+  return (
+    <ComboboxPrimitive.Chip
+      data-slot="combobox-chip"
+      className={cn(
+        'bg-muted text-foreground flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-data-[slot=combobox-chip-remove]:pr-0 has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50',
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      {showRemove && (
+        <ComboboxPrimitive.ChipRemove
+          render={<Button variant="ghost" size="icon-xs" />}
+          className="-ml-1 opacity-50 hover:opacity-100"
+          data-slot="combobox-chip-remove"
+        >
+          <XIcon className="pointer-events-none" />
+        </ComboboxPrimitive.ChipRemove>
+      )}
+    </ComboboxPrimitive.Chip>
+  );
+}
+
+function ComboboxChipsInput({
+  className,
+  ...props
+}: ComboboxPrimitive.Input.Props) {
+  return (
+    <ComboboxPrimitive.Input
+      data-slot="combobox-chip-input"
+      className={cn('min-w-16 flex-1 outline-none', className)}
+      {...props}
+    />
+  );
+}
+
+function useComboboxAnchor() {
+  return React.useRef<HTMLDivElement | null>(null);
+}
+
+export {
+  Combobox,
+  ComboboxInput,
+  ComboboxContent,
+  ComboboxList,
+  ComboboxItem,
+  ComboboxGroup,
+  ComboboxLabel,
+  ComboboxCollection,
+  ComboboxEmpty,
+  ComboboxSeparator,
+  ComboboxChips,
+  ComboboxChip,
+  ComboboxChipsInput,
+  ComboboxTrigger,
+  ComboboxValue,
+  useComboboxAnchor,
+};

+ 193 - 0
components/ui/command.tsx

@@ -0,0 +1,193 @@
+'use client';
+
+import * as React from 'react';
+import { Command as CommandPrimitive } from 'cmdk';
+
+import { cn } from '@/lib/utils';
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog';
+import { InputGroup, InputGroupAddon } from '@/components/ui/input-group';
+import { SearchIcon, CheckIcon } from 'lucide-react';
+
+function Command({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive>) {
+  return (
+    <CommandPrimitive
+      data-slot="command"
+      className={cn(
+        'bg-popover text-popover-foreground rounded-xl! p-1 flex size-full flex-col overflow-hidden',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function CommandDialog({
+  title = 'Command Palette',
+  description = 'Search for a command to run...',
+  children,
+  className,
+  showCloseButton = false,
+  ...props
+}: Omit<React.ComponentProps<typeof Dialog>, 'children'> & {
+  title?: string;
+  description?: string;
+  className?: string;
+  showCloseButton?: boolean;
+  children: React.ReactNode;
+}) {
+  return (
+    <Dialog {...props}>
+      <DialogHeader className="sr-only">
+        <DialogTitle>{title}</DialogTitle>
+        <DialogDescription>{description}</DialogDescription>
+      </DialogHeader>
+      <DialogContent
+        className={cn(
+          'rounded-xl! top-1/3 translate-y-0 overflow-hidden p-0',
+          className,
+        )}
+        showCloseButton={showCloseButton}
+      >
+        {children}
+      </DialogContent>
+    </Dialog>
+  );
+}
+
+function CommandInput({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Input>) {
+  return (
+    <div data-slot="command-input-wrapper" className="p-1 pb-0">
+      <InputGroup className="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!">
+        <CommandPrimitive.Input
+          data-slot="command-input"
+          className={cn(
+            'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
+            className,
+          )}
+          {...props}
+        />
+        <InputGroupAddon>
+          <SearchIcon className="size-4 shrink-0 opacity-50" />
+        </InputGroupAddon>
+      </InputGroup>
+    </div>
+  );
+}
+
+function CommandList({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.List>) {
+  return (
+    <CommandPrimitive.List
+      data-slot="command-list"
+      className={cn(
+        'no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function CommandEmpty({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
+  return (
+    <CommandPrimitive.Empty
+      data-slot="command-empty"
+      className={cn('py-6 text-center text-sm', className)}
+      {...props}
+    />
+  );
+}
+
+function CommandGroup({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Group>) {
+  return (
+    <CommandPrimitive.Group
+      data-slot="command-group"
+      className={cn(
+        'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function CommandSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
+  return (
+    <CommandPrimitive.Separator
+      data-slot="command-separator"
+      className={cn('bg-border -mx-1 h-px', className)}
+      {...props}
+    />
+  );
+}
+
+function CommandItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof CommandPrimitive.Item>) {
+  return (
+    <CommandPrimitive.Item
+      data-slot="command-item"
+      className={cn(
+        "data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! [&_svg:not([class*='size-'])]:size-4 group/command-item data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      <CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
+    </CommandPrimitive.Item>
+  );
+}
+
+function CommandShortcut({
+  className,
+  ...props
+}: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="command-shortcut"
+      className={cn(
+        'text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Command,
+  CommandDialog,
+  CommandInput,
+  CommandList,
+  CommandEmpty,
+  CommandGroup,
+  CommandItem,
+  CommandShortcut,
+  CommandSeparator,
+};

+ 272 - 0
components/ui/context-menu.tsx

@@ -0,0 +1,272 @@
+'use client';
+
+import * as React from 'react';
+import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react/context-menu';
+
+import { cn } from '@/lib/utils';
+import { ChevronRightIcon, CheckIcon } from 'lucide-react';
+
+function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
+  return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
+}
+
+function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
+  return (
+    <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
+  );
+}
+
+function ContextMenuTrigger({
+  className,
+  ...props
+}: ContextMenuPrimitive.Trigger.Props) {
+  return (
+    <ContextMenuPrimitive.Trigger
+      data-slot="context-menu-trigger"
+      className={cn('select-none', className)}
+      {...props}
+    />
+  );
+}
+
+function ContextMenuContent({
+  className,
+  align = 'start',
+  alignOffset = 4,
+  side = 'right',
+  sideOffset = 0,
+  ...props
+}: ContextMenuPrimitive.Popup.Props &
+  Pick<
+    ContextMenuPrimitive.Positioner.Props,
+    'align' | 'alignOffset' | 'side' | 'sideOffset'
+  >) {
+  return (
+    <ContextMenuPrimitive.Portal>
+      <ContextMenuPrimitive.Positioner
+        className="isolate z-50 outline-none"
+        align={align}
+        alignOffset={alignOffset}
+        side={side}
+        sideOffset={sideOffset}
+      >
+        <ContextMenuPrimitive.Popup
+          data-slot="context-menu-content"
+          className={cn(
+            'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 cn-menu-target cn-menu-translucent z-50 max-h-(--available-height) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none',
+            className,
+          )}
+          {...props}
+        />
+      </ContextMenuPrimitive.Positioner>
+    </ContextMenuPrimitive.Portal>
+  );
+}
+
+function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
+  return (
+    <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
+  );
+}
+
+function ContextMenuLabel({
+  className,
+  inset,
+  ...props
+}: ContextMenuPrimitive.GroupLabel.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <ContextMenuPrimitive.GroupLabel
+      data-slot="context-menu-label"
+      data-inset={inset}
+      className={cn(
+        'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ContextMenuItem({
+  className,
+  inset,
+  variant = 'default',
+  ...props
+}: ContextMenuPrimitive.Item.Props & {
+  inset?: boolean;
+  variant?: 'default' | 'destructive';
+}) {
+  return (
+    <ContextMenuPrimitive.Item
+      data-slot="context-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
+  return (
+    <ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
+  );
+}
+
+function ContextMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: ContextMenuPrimitive.SubmenuTrigger.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <ContextMenuPrimitive.SubmenuTrigger
+      data-slot="context-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0",
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="cn-rtl-flip ml-auto" />
+    </ContextMenuPrimitive.SubmenuTrigger>
+  );
+}
+
+function ContextMenuSubContent({
+  ...props
+}: React.ComponentProps<typeof ContextMenuContent>) {
+  return (
+    <ContextMenuContent
+      data-slot="context-menu-sub-content"
+      className="shadow-lg cn-menu-target cn-menu-translucent"
+      side="right"
+      {...props}
+    />
+  );
+}
+
+function ContextMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  inset,
+  ...props
+}: ContextMenuPrimitive.CheckboxItem.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <ContextMenuPrimitive.CheckboxItem
+      data-slot="context-menu-checkbox-item"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+        className,
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="absolute right-2 pointer-events-none">
+        <ContextMenuPrimitive.CheckboxItemIndicator>
+          <CheckIcon />
+        </ContextMenuPrimitive.CheckboxItemIndicator>
+      </span>
+      {children}
+    </ContextMenuPrimitive.CheckboxItem>
+  );
+}
+
+function ContextMenuRadioGroup({
+  ...props
+}: ContextMenuPrimitive.RadioGroup.Props) {
+  return (
+    <ContextMenuPrimitive.RadioGroup
+      data-slot="context-menu-radio-group"
+      {...props}
+    />
+  );
+}
+
+function ContextMenuRadioItem({
+  className,
+  children,
+  inset,
+  ...props
+}: ContextMenuPrimitive.RadioItem.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <ContextMenuPrimitive.RadioItem
+      data-slot="context-menu-radio-item"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+        className,
+      )}
+      {...props}
+    >
+      <span className="absolute right-2 pointer-events-none">
+        <ContextMenuPrimitive.RadioItemIndicator>
+          <CheckIcon />
+        </ContextMenuPrimitive.RadioItemIndicator>
+      </span>
+      {children}
+    </ContextMenuPrimitive.RadioItem>
+  );
+}
+
+function ContextMenuSeparator({
+  className,
+  ...props
+}: ContextMenuPrimitive.Separator.Props) {
+  return (
+    <ContextMenuPrimitive.Separator
+      data-slot="context-menu-separator"
+      className={cn('bg-border -mx-1 my-1 h-px', className)}
+      {...props}
+    />
+  );
+}
+
+function ContextMenuShortcut({
+  className,
+  ...props
+}: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="context-menu-shortcut"
+      className={cn(
+        'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  ContextMenu,
+  ContextMenuTrigger,
+  ContextMenuContent,
+  ContextMenuItem,
+  ContextMenuCheckboxItem,
+  ContextMenuRadioItem,
+  ContextMenuLabel,
+  ContextMenuSeparator,
+  ContextMenuShortcut,
+  ContextMenuGroup,
+  ContextMenuPortal,
+  ContextMenuSub,
+  ContextMenuSubContent,
+  ContextMenuSubTrigger,
+  ContextMenuRadioGroup,
+};

+ 159 - 0
components/ui/dialog.tsx

@@ -0,0 +1,159 @@
+'use client';
+
+import * as React from 'react';
+import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { XIcon } from 'lucide-react';
+
+function Dialog({ ...props }: DialogPrimitive.Root.Props) {
+  return <DialogPrimitive.Root data-slot="dialog" {...props} />;
+}
+
+function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
+  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
+}
+
+function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
+  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
+}
+
+function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
+  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
+}
+
+function DialogOverlay({
+  className,
+  ...props
+}: DialogPrimitive.Backdrop.Props) {
+  return (
+    <DialogPrimitive.Backdrop
+      data-slot="dialog-overlay"
+      className={cn(
+        'fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function DialogContent({
+  className,
+  children,
+  showCloseButton = true,
+  ...props
+}: DialogPrimitive.Popup.Props & {
+  showCloseButton?: boolean;
+}) {
+  return (
+    <DialogPortal>
+      <DialogOverlay />
+      <DialogPrimitive.Popup
+        data-slot="dialog-content"
+        className={cn(
+          'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
+          className,
+        )}
+        {...props}
+      >
+        {children}
+        {showCloseButton && (
+          <DialogPrimitive.Close
+            data-slot="dialog-close"
+            render={
+              <Button
+                variant="ghost"
+                className="absolute top-2 right-2"
+                size="icon-sm"
+              />
+            }
+          >
+            <XIcon />
+            <span className="sr-only">Close</span>
+          </DialogPrimitive.Close>
+        )}
+      </DialogPrimitive.Popup>
+    </DialogPortal>
+  );
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="dialog-header"
+      className={cn('flex flex-col gap-2', className)}
+      {...props}
+    />
+  );
+}
+
+function DialogFooter({
+  className,
+  showCloseButton = false,
+  children,
+  ...props
+}: React.ComponentProps<'div'> & {
+  showCloseButton?: boolean;
+}) {
+  return (
+    <div
+      data-slot="dialog-footer"
+      className={cn(
+        '-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end',
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      {showCloseButton && (
+        <DialogPrimitive.Close render={<Button variant="outline" />}>
+          Close
+        </DialogPrimitive.Close>
+      )}
+    </div>
+  );
+}
+
+function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
+  return (
+    <DialogPrimitive.Title
+      data-slot="dialog-title"
+      className={cn(
+        'font-heading text-base leading-none font-medium',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function DialogDescription({
+  className,
+  ...props
+}: DialogPrimitive.Description.Props) {
+  return (
+    <DialogPrimitive.Description
+      data-slot="dialog-description"
+      className={cn(
+        'text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogOverlay,
+  DialogPortal,
+  DialogTitle,
+  DialogTrigger,
+};

+ 6 - 0
components/ui/direction.tsx

@@ -0,0 +1,6 @@
+'use client';
+
+export {
+  DirectionProvider,
+  useDirection,
+} from '@base-ui/react/direction-provider';

+ 228 - 0
components/ui/drawer.tsx

@@ -0,0 +1,228 @@
+'use client';
+
+import * as React from 'react';
+import { Drawer as DrawerPrimitive } from '@base-ui/react/drawer';
+
+import { cn } from '@/lib/utils';
+
+type DrawerContextProps = {
+  hasSnapPoints: boolean;
+  modal: DrawerPrimitive.Root.Props['modal'];
+  showSwipeHandle: boolean;
+  swipeDirection: NonNullable<DrawerPrimitive.Root.Props['swipeDirection']>;
+};
+
+const DrawerContext = React.createContext<DrawerContextProps | null>(null);
+
+function useDrawer() {
+  const context = React.useContext(DrawerContext);
+
+  if (!context) {
+    throw new Error('useDrawer must be used within a Drawer.');
+  }
+
+  return context;
+}
+
+function Drawer({
+  modal = true,
+  showSwipeHandle = false,
+  snapPoints,
+  swipeDirection = 'down',
+  ...props
+}: DrawerPrimitive.Root.Props & {
+  showSwipeHandle?: boolean;
+}) {
+  const hasSnapPoints = snapPoints != null && snapPoints.length > 0;
+  const contextValue = React.useMemo(
+    () => ({ hasSnapPoints, modal, showSwipeHandle, swipeDirection }),
+    [hasSnapPoints, modal, showSwipeHandle, swipeDirection],
+  );
+
+  return (
+    <DrawerContext.Provider value={contextValue}>
+      <DrawerPrimitive.Root
+        data-slot="drawer"
+        modal={modal}
+        snapPoints={snapPoints}
+        swipeDirection={swipeDirection}
+        {...props}
+      />
+    </DrawerContext.Provider>
+  );
+}
+
+function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) {
+  return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
+}
+
+function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) {
+  return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
+}
+
+function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {
+  return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
+}
+
+function DrawerOverlay({
+  className,
+  ...props
+}: DrawerPrimitive.Backdrop.Props) {
+  return (
+    <DrawerPrimitive.Backdrop
+      data-slot="drawer-overlay"
+      className={cn(
+        'bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50 min-h-dvh opacity-[max(var(--drawer-overlay-min-opacity,0),calc(1-var(--drawer-swipe-progress)))] transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] select-none data-ending-style:pointer-events-none data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-snap-points:[--drawer-overlay-min-opacity:0.5] data-starting-style:opacity-0 data-swiping:duration-0 supports-[-webkit-touch-callout:none]:absolute',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function DrawerSwipeHandle({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="drawer-swipe-handle"
+      aria-hidden="true"
+      className={cn(
+        'after:block after:shrink-0 after:rounded-full after:bg-muted group-data-[swipe-axis=y]/drawer-popup:h-3 group-data-[swipe-axis=y]/drawer-popup:w-full group-data-[swipe-axis=y]/drawer-popup:justify-center group-data-[swipe-axis=y]/drawer-popup:after:h-1 group-data-[swipe-axis=y]/drawer-popup:after:w-24 group-data-[swipe-direction=down]/drawer-popup:items-end group-data-[swipe-direction=up]/drawer-popup:items-start group-data-[swipe-axis=x]/drawer-popup:h-full group-data-[swipe-axis=x]/drawer-popup:w-3 group-data-[swipe-axis=x]/drawer-popup:items-center group-data-[swipe-axis=x]/drawer-popup:after:h-24 group-data-[swipe-axis=x]/drawer-popup:after:w-1 group-data-[swipe-direction=left]/drawer-popup:justify-start group-data-[swipe-direction=right]/drawer-popup:justify-end relative z-10 flex shrink-0 cursor-grab transition-opacity duration-200 group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-[swipe-direction=left]/drawer-popup:order-last group-data-[swipe-direction=up]/drawer-popup:order-last active:cursor-grabbing',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function DrawerContent({
+  className,
+  children,
+  ...props
+}: DrawerPrimitive.Popup.Props) {
+  const { hasSnapPoints, modal, showSwipeHandle, swipeDirection } = useDrawer();
+  const swipeAxis =
+    swipeDirection === 'down' || swipeDirection === 'up' ? 'y' : 'x';
+
+  return (
+    <DrawerPortal data-slot="drawer-portal">
+      {modal === true && (
+        <DrawerOverlay data-snap-points={hasSnapPoints ? '' : undefined} />
+      )}
+      <DrawerPrimitive.Viewport
+        data-slot="drawer-viewport"
+        data-modal={modal}
+        className="pointer-events-none fixed inset-0 z-50 select-none data-[modal=true]:pointer-events-auto"
+      >
+        <DrawerPrimitive.Popup
+          data-slot="drawer-popup"
+          data-swipe-axis={swipeAxis}
+          data-snap-points={hasSnapPoints ? '' : undefined}
+          className={cn(
+            // Base.
+            'bg-popover text-popover-foreground text-sm data-[swipe-direction=down]:rounded-t-xl data-[swipe-direction=down]:border-t data-[swipe-direction=left]:rounded-r-xl data-[swipe-direction=left]:border-r data-[swipe-direction=right]:rounded-l-xl data-[swipe-direction=right]:border-l data-[swipe-direction=up]:rounded-b-xl data-[swipe-direction=up]:border-b group/drawer-popup pointer-events-auto fixed z-50 m-(--drawer-inset,0px) flex h-(--drawer-content-height) max-h-(--drawer-content-max-height,none) min-h-0 w-(--drawer-content-width,auto) transform-[translate3d(var(--translate-x,0px),var(--translate-y,0px),0)_scale(var(--stack-scale))] flex-col transition-[transform,height,opacity,filter] duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform outline-none select-none [interpolate-size:allow-keywords]',
+            // Nested.
+            'data-nested-drawer-open:overflow-hidden data-nested-drawer-open:brightness-95',
+            // Bleed.
+            'after:pointer-events-none after:absolute after:bg-(--drawer-bleed-background,var(--color-popover)) data-[swipe-axis=x]:after:inset-y-0 data-[swipe-axis=x]:after:w-(--bleed) data-[swipe-axis=y]:after:inset-x-0 data-[swipe-axis=y]:after:h-(--bleed) data-[swipe-direction=down]:after:top-full data-[swipe-direction=left]:after:right-full data-[swipe-direction=right]:after:left-full data-[swipe-direction=up]:after:bottom-full',
+            // Sizing.
+            '[--drawer-content-height:var(--drawer-height,auto)] data-[swipe-axis=x]:[--drawer-content-width:75%] data-[swipe-axis=y]:[--drawer-content-max-height:calc(100dvh-6rem)] data-[swipe-axis=y]:data-snap-points:[--drawer-content-height:100dvh] data-[swipe-axis=x]:sm:[--drawer-content-width:24rem]',
+            // Stack.
+            '[--bleed:3rem] [--peek:1rem] [--stack-height:var(--drawer-frontmost-height,var(--drawer-height,0px))] [--stack-peek-offset:max(0px,calc((var(--nested-drawers)-var(--stack-progress))*var(--peek)))] [--stack-progress:clamp(0,var(--drawer-swipe-progress),1)] [--stack-scale-base:max(0,calc(1-(var(--nested-drawers)*var(--stack-step))))] [--stack-scale:clamp(0,calc(var(--stack-scale-base)+(var(--stack-step)*var(--stack-progress))),1)] [--stack-shrink:calc(1-var(--stack-scale))] [--stack-step:0.05]',
+            // Transitions.
+            'data-ending-style:transform-(--closed-transform) data-ending-style:opacity-[0.9999] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-nested-drawer-swiping:duration-0 data-ending-style:data-nested-drawer-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-starting-style:transform-(--closed-transform) data-swiping:duration-0 data-ending-style:data-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)]',
+            // Axis: y.
+            'data-[swipe-axis=y]:inset-x-0 data-[swipe-axis=y]:data-nested-drawer-open:h-(--stack-height)',
+            // Axis: x.
+            'data-[swipe-axis=x]:inset-y-0 data-[swipe-axis=x]:flex-row',
+            // Direction: down.
+            'data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))]',
+            // Direction: up.
+            'data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:origin-top data-[swipe-direction=up]:[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] data-[swipe-direction=up]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)+var(--stack-peek-offset)+(var(--stack-shrink)*var(--stack-height)))]',
+            // Direction: left.
+            'data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:origin-left data-[swipe-direction=left]:[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] data-[swipe-direction=left]:[--translate-x:calc(var(--drawer-swipe-movement-x)+var(--stack-peek-offset)+(var(--stack-shrink)*100%))]',
+            // Direction: right.
+            'data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:origin-right data-[swipe-direction=right]:[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] data-[swipe-direction=right]:[--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset)-(var(--stack-shrink)*100%))]',
+            className,
+          )}
+          {...props}
+        >
+          {showSwipeHandle && <DrawerSwipeHandle />}
+          <DrawerPrimitive.Content
+            data-slot="drawer-content"
+            className={cn(
+              'flex min-h-0 flex-1 flex-col overflow-hidden overscroll-contain rounded-[inherit] transition-opacity duration-300 ease-[cubic-bezier(0.45,1.005,0,1.005)] select-text group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-swiping/drawer-popup:select-none',
+            )}
+          >
+            {children}
+          </DrawerPrimitive.Content>
+        </DrawerPrimitive.Popup>
+      </DrawerPrimitive.Viewport>
+    </DrawerPortal>
+  );
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="drawer-header"
+      className={cn(
+        'gap-0.5 p-4 pb-0 md:gap-0.5 md:text-left flex shrink-0 flex-col group-data-[swipe-axis=y]/drawer-popup:text-center',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="drawer-footer"
+      className={cn('gap-2 p-4 pt-0 mt-auto flex shrink-0 flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) {
+  return (
+    <DrawerPrimitive.Title
+      data-slot="drawer-title"
+      className={cn(
+        'text-foreground text-base font-medium cn-font-heading',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function DrawerDescription({
+  className,
+  ...props
+}: DrawerPrimitive.Description.Props) {
+  return (
+    <DrawerPrimitive.Description
+      data-slot="drawer-description"
+      className={cn('text-muted-foreground text-sm text-balance', className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Drawer,
+  DrawerPortal,
+  DrawerOverlay,
+  DrawerSwipeHandle,
+  DrawerTrigger,
+  DrawerClose,
+  DrawerContent,
+  DrawerHeader,
+  DrawerFooter,
+  DrawerTitle,
+  DrawerDescription,
+};

+ 272 - 0
components/ui/dropdown-menu.tsx

@@ -0,0 +1,272 @@
+'use client';
+
+import * as React from 'react';
+import { Menu as MenuPrimitive } from '@base-ui/react/menu';
+
+import { cn } from '@/lib/utils';
+import { ChevronRightIcon, CheckIcon } from 'lucide-react';
+
+function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
+  return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
+}
+
+function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
+  return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
+}
+
+function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
+  return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
+}
+
+function DropdownMenuContent({
+  align = 'start',
+  alignOffset = 0,
+  side = 'bottom',
+  sideOffset = 4,
+  className,
+  ...props
+}: MenuPrimitive.Popup.Props &
+  Pick<
+    MenuPrimitive.Positioner.Props,
+    'align' | 'alignOffset' | 'side' | 'sideOffset'
+  >) {
+  return (
+    <MenuPrimitive.Portal>
+      <MenuPrimitive.Positioner
+        className="isolate z-50 outline-none"
+        align={align}
+        alignOffset={alignOffset}
+        side={side}
+        sideOffset={sideOffset}
+      >
+        <MenuPrimitive.Popup
+          data-slot="dropdown-menu-content"
+          className={cn(
+            'z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95',
+            className,
+          )}
+          {...props}
+        />
+      </MenuPrimitive.Positioner>
+    </MenuPrimitive.Portal>
+  );
+}
+
+function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
+  return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
+}
+
+function DropdownMenuLabel({
+  className,
+  inset,
+  ...props
+}: MenuPrimitive.GroupLabel.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <MenuPrimitive.GroupLabel
+      data-slot="dropdown-menu-label"
+      data-inset={inset}
+      className={cn(
+        'px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuItem({
+  className,
+  inset,
+  variant = 'default',
+  ...props
+}: MenuPrimitive.Item.Props & {
+  inset?: boolean;
+  variant?: 'default' | 'destructive';
+}) {
+  return (
+    <MenuPrimitive.Item
+      data-slot="dropdown-menu-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
+  return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
+}
+
+function DropdownMenuSubTrigger({
+  className,
+  inset,
+  children,
+  ...props
+}: MenuPrimitive.SubmenuTrigger.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <MenuPrimitive.SubmenuTrigger
+      data-slot="dropdown-menu-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      <ChevronRightIcon className="ml-auto" />
+    </MenuPrimitive.SubmenuTrigger>
+  );
+}
+
+function DropdownMenuSubContent({
+  align = 'start',
+  alignOffset = -3,
+  side = 'right',
+  sideOffset = 0,
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuContent>) {
+  return (
+    <DropdownMenuContent
+      data-slot="dropdown-menu-sub-content"
+      className={cn(
+        'w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
+        className,
+      )}
+      align={align}
+      alignOffset={alignOffset}
+      side={side}
+      sideOffset={sideOffset}
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuCheckboxItem({
+  className,
+  children,
+  checked,
+  inset,
+  ...props
+}: MenuPrimitive.CheckboxItem.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <MenuPrimitive.CheckboxItem
+      data-slot="dropdown-menu-checkbox-item"
+      data-inset={inset}
+      className={cn(
+        "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span
+        className="pointer-events-none absolute right-2 flex items-center justify-center"
+        data-slot="dropdown-menu-checkbox-item-indicator"
+      >
+        <MenuPrimitive.CheckboxItemIndicator>
+          <CheckIcon />
+        </MenuPrimitive.CheckboxItemIndicator>
+      </span>
+      {children}
+    </MenuPrimitive.CheckboxItem>
+  );
+}
+
+function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
+  return (
+    <MenuPrimitive.RadioGroup
+      data-slot="dropdown-menu-radio-group"
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuRadioItem({
+  className,
+  children,
+  inset,
+  ...props
+}: MenuPrimitive.RadioItem.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <MenuPrimitive.RadioItem
+      data-slot="dropdown-menu-radio-item"
+      data-inset={inset}
+      className={cn(
+        "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    >
+      <span
+        className="pointer-events-none absolute right-2 flex items-center justify-center"
+        data-slot="dropdown-menu-radio-item-indicator"
+      >
+        <MenuPrimitive.RadioItemIndicator>
+          <CheckIcon />
+        </MenuPrimitive.RadioItemIndicator>
+      </span>
+      {children}
+    </MenuPrimitive.RadioItem>
+  );
+}
+
+function DropdownMenuSeparator({
+  className,
+  ...props
+}: MenuPrimitive.Separator.Props) {
+  return (
+    <MenuPrimitive.Separator
+      data-slot="dropdown-menu-separator"
+      className={cn('-mx-1 my-1 h-px bg-border', className)}
+      {...props}
+    />
+  );
+}
+
+function DropdownMenuShortcut({
+  className,
+  ...props
+}: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="dropdown-menu-shortcut"
+      className={cn(
+        'ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  DropdownMenu,
+  DropdownMenuPortal,
+  DropdownMenuTrigger,
+  DropdownMenuContent,
+  DropdownMenuGroup,
+  DropdownMenuLabel,
+  DropdownMenuItem,
+  DropdownMenuCheckboxItem,
+  DropdownMenuRadioGroup,
+  DropdownMenuRadioItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubTrigger,
+  DropdownMenuSubContent,
+};

+ 104 - 0
components/ui/empty.tsx

@@ -0,0 +1,104 @@
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+function Empty({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="empty"
+      className={cn(
+        'gap-4 rounded-xl border-dashed p-6 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="empty-header"
+      className={cn('gap-2 flex max-w-sm flex-col items-center', className)}
+      {...props}
+    />
+  );
+}
+
+const emptyMediaVariants = cva(
+  'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
+  {
+    variants: {
+      variant: {
+        default: 'bg-transparent',
+        icon: "bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-4",
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+);
+
+function EmptyMedia({
+  className,
+  variant = 'default',
+  ...props
+}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) {
+  return (
+    <div
+      data-slot="empty-icon"
+      data-variant={variant}
+      className={cn(emptyMediaVariants({ variant, className }))}
+      {...props}
+    />
+  );
+}
+
+function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="empty-title"
+      className={cn(
+        'text-sm font-medium tracking-tight cn-font-heading',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
+  return (
+    <div
+      data-slot="empty-description"
+      className={cn(
+        'text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="empty-content"
+      className={cn(
+        'gap-2.5 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Empty,
+  EmptyHeader,
+  EmptyTitle,
+  EmptyDescription,
+  EmptyContent,
+  EmptyMedia,
+};

+ 238 - 0
components/ui/field.tsx

@@ -0,0 +1,238 @@
+'use client';
+
+import { useMemo } from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { Label } from '@/components/ui/label';
+import { Separator } from '@/components/ui/separator';
+
+function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
+  return (
+    <fieldset
+      data-slot="field-set"
+      className={cn(
+        'gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldLegend({
+  className,
+  variant = 'legend',
+  ...props
+}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
+  return (
+    <legend
+      data-slot="field-legend"
+      data-variant={variant}
+      className={cn(
+        'mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="field-group"
+      className={cn(
+        'gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+const fieldVariants = cva(
+  'data-[invalid=true]:text-destructive gap-2 group/field flex w-full',
+  {
+    variants: {
+      orientation: {
+        vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
+        horizontal:
+          'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
+        responsive:
+          'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
+      },
+    },
+    defaultVariants: {
+      orientation: 'vertical',
+    },
+  },
+);
+
+function Field({
+  className,
+  orientation = 'vertical',
+  ...props
+}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
+  return (
+    <div
+      role="group"
+      data-slot="field"
+      data-orientation={orientation}
+      className={cn(fieldVariants({ orientation }), className)}
+      {...props}
+    />
+  );
+}
+
+function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="field-content"
+      className={cn(
+        'gap-0.5 group/field-content flex flex-1 flex-col leading-snug',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof Label>) {
+  return (
+    <Label
+      data-slot="field-label"
+      className={cn(
+        'has-data-checked:bg-primary/5 has-data-checked:border-primary/30 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted/50 has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:ring-ring/50 has-[>[data-slot=field]]:has-[:focus-visible]:ring-3 *:data-[slot=field]:p-2.5 group/field-label peer/field-label flex w-fit',
+        'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="field-label"
+      className={cn(
+        'gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50 flex w-fit items-center',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
+  return (
+    <p
+      data-slot="field-description"
+      className={cn(
+        'text-muted-foreground text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-data-horizontal/field:text-balance',
+        'last:mt-0 nth-last-2:-mt-1',
+        '[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function FieldSeparator({
+  children,
+  className,
+  ...props
+}: React.ComponentProps<'div'> & {
+  children?: React.ReactNode;
+}) {
+  return (
+    <div
+      data-slot="field-separator"
+      data-content={!!children}
+      className={cn(
+        '-my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 relative',
+        className,
+      )}
+      {...props}
+    >
+      <Separator className="absolute inset-0 top-1/2" />
+      {children && (
+        <span
+          className="text-muted-foreground px-2 relative mx-auto block w-fit bg-background"
+          data-slot="field-separator-content"
+        >
+          {children}
+        </span>
+      )}
+    </div>
+  );
+}
+
+function FieldError({
+  className,
+  children,
+  errors,
+  ...props
+}: React.ComponentProps<'div'> & {
+  errors?: Array<{ message?: string } | undefined>;
+}) {
+  const content = useMemo(() => {
+    if (children) {
+      return children;
+    }
+
+    if (!errors?.length) {
+      return null;
+    }
+
+    const uniqueErrors = [
+      ...new Map(errors.map((error) => [error?.message, error])).values(),
+    ];
+
+    if (uniqueErrors?.length == 1) {
+      return uniqueErrors[0]?.message;
+    }
+
+    return (
+      <ul className="ml-4 flex list-disc flex-col gap-1">
+        {uniqueErrors.map(
+          (error, index) =>
+            error?.message && <li key={index}>{error.message}</li>,
+        )}
+      </ul>
+    );
+  }, [children, errors]);
+
+  if (!content) {
+    return null;
+  }
+
+  return (
+    <div
+      role="alert"
+      data-slot="field-error"
+      className={cn('text-destructive text-sm font-normal', className)}
+      {...props}
+    >
+      {content}
+    </div>
+  );
+}
+
+export {
+  Field,
+  FieldLabel,
+  FieldDescription,
+  FieldError,
+  FieldGroup,
+  FieldLegend,
+  FieldSeparator,
+  FieldSet,
+  FieldContent,
+  FieldTitle,
+};

+ 51 - 0
components/ui/hover-card.tsx

@@ -0,0 +1,51 @@
+'use client';
+
+import { PreviewCard as PreviewCardPrimitive } from '@base-ui/react/preview-card';
+
+import { cn } from '@/lib/utils';
+
+function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
+  return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />;
+}
+
+function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
+  return (
+    <PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
+  );
+}
+
+function HoverCardContent({
+  className,
+  side = 'bottom',
+  sideOffset = 4,
+  align = 'center',
+  alignOffset = 4,
+  ...props
+}: PreviewCardPrimitive.Popup.Props &
+  Pick<
+    PreviewCardPrimitive.Positioner.Props,
+    'align' | 'alignOffset' | 'side' | 'sideOffset'
+  >) {
+  return (
+    <PreviewCardPrimitive.Portal data-slot="hover-card-portal">
+      <PreviewCardPrimitive.Positioner
+        align={align}
+        alignOffset={alignOffset}
+        side={side}
+        sideOffset={sideOffset}
+        className="isolate z-50"
+      >
+        <PreviewCardPrimitive.Popup
+          data-slot="hover-card-content"
+          className={cn(
+            'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground w-64 rounded-lg p-2.5 text-sm shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 origin-(--transform-origin) outline-hidden',
+            className,
+          )}
+          {...props}
+        />
+      </PreviewCardPrimitive.Positioner>
+    </PreviewCardPrimitive.Portal>
+  );
+}
+
+export { HoverCard, HoverCardTrigger, HoverCardContent };

+ 158 - 0
components/ui/input-group.tsx

@@ -0,0 +1,158 @@
+'use client';
+
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+
+function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="input-group"
+      role="group"
+      className={cn(
+        'border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 h-8 rounded-lg border transition-colors in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+const inputGroupAddonVariants = cva(
+  "text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none",
+  {
+    variants: {
+      align: {
+        'inline-start':
+          'pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first',
+        'inline-end':
+          'pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last',
+        'block-start':
+          'px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start',
+        'block-end':
+          'px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start',
+      },
+    },
+    defaultVariants: {
+      align: 'inline-start',
+    },
+  },
+);
+
+function InputGroupAddon({
+  className,
+  align = 'inline-start',
+  ...props
+}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
+  return (
+    <div
+      role="group"
+      data-slot="input-group-addon"
+      data-align={align}
+      className={cn(inputGroupAddonVariants({ align }), className)}
+      onClick={(e) => {
+        if ((e.target as HTMLElement).closest('button')) {
+          return;
+        }
+        e.currentTarget.parentElement?.querySelector('input')?.focus();
+      }}
+      {...props}
+    />
+  );
+}
+
+const inputGroupButtonVariants = cva(
+  'gap-2 text-sm flex items-center shadow-none',
+  {
+    variants: {
+      size: {
+        xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
+        sm: '',
+        'icon-xs':
+          'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0',
+        'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
+      },
+    },
+    defaultVariants: {
+      size: 'xs',
+    },
+  },
+);
+
+function InputGroupButton({
+  className,
+  type = 'button',
+  variant = 'ghost',
+  size = 'xs',
+  ...props
+}: Omit<React.ComponentProps<typeof Button>, 'size' | 'type'> &
+  VariantProps<typeof inputGroupButtonVariants> & {
+    type?: 'button' | 'submit' | 'reset';
+  }) {
+  return (
+    <Button
+      type={type}
+      data-size={size}
+      variant={variant}
+      className={cn(inputGroupButtonVariants({ size }), className)}
+      {...props}
+    />
+  );
+}
+
+function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
+  return (
+    <span
+      className={cn(
+        "text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function InputGroupInput({
+  className,
+  ...props
+}: React.ComponentProps<'input'>) {
+  return (
+    <Input
+      data-slot="input-group-control"
+      className={cn(
+        'rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent flex-1',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function InputGroupTextarea({
+  className,
+  ...props
+}: React.ComponentProps<'textarea'>) {
+  return (
+    <Textarea
+      data-slot="input-group-control"
+      className={cn(
+        'rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent flex-1 resize-none',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  InputGroup,
+  InputGroupAddon,
+  InputGroupButton,
+  InputGroupText,
+  InputGroupInput,
+  InputGroupTextarea,
+};

+ 86 - 0
components/ui/input-otp.tsx

@@ -0,0 +1,86 @@
+'use client';
+
+import * as React from 'react';
+import { OTPInput, OTPInputContext } from 'input-otp';
+
+import { cn } from '@/lib/utils';
+import { MinusIcon } from 'lucide-react';
+
+function InputOTP({
+  className,
+  containerClassName,
+  ...props
+}: React.ComponentProps<typeof OTPInput> & {
+  containerClassName?: string;
+}) {
+  return (
+    <OTPInput
+      data-slot="input-otp"
+      containerClassName={cn(
+        'cn-input-otp flex items-center has-disabled:opacity-50',
+        containerClassName,
+      )}
+      spellCheck={false}
+      className={cn('disabled:cursor-not-allowed', className)}
+      {...props}
+    />
+  );
+}
+
+function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="input-otp-group"
+      className={cn(
+        'has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive rounded-lg has-aria-invalid:ring-3 flex items-center',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function InputOTPSlot({
+  index,
+  className,
+  ...props
+}: React.ComponentProps<'div'> & {
+  index: number;
+}) {
+  const inputOTPContext = React.useContext(OTPInputContext);
+  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
+
+  return (
+    <div
+      data-slot="input-otp-slot"
+      data-active={isActive}
+      className={cn(
+        'dark:bg-input/30 border-input data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive size-8 border-y border-r text-sm transition-all outline-none first:rounded-l-lg first:border-l last:rounded-r-lg data-[active=true]:ring-3 relative flex items-center justify-center data-[active=true]:z-10',
+        className,
+      )}
+      {...props}
+    >
+      {char}
+      {hasFakeCaret && (
+        <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
+          <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
+        </div>
+      )}
+    </div>
+  );
+}
+
+function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="input-otp-separator"
+      className="[&_svg:not([class*='size-'])]:size-4 flex items-center"
+      role="separator"
+      {...props}
+    >
+      <MinusIcon />
+    </div>
+  );
+}
+
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

+ 20 - 0
components/ui/input.tsx

@@ -0,0 +1,20 @@
+import * as React from 'react';
+import { Input as InputPrimitive } from '@base-ui/react/input';
+
+import { cn } from '@/lib/utils';
+
+function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
+  return (
+    <InputPrimitive
+      type={type}
+      data-slot="input"
+      className={cn(
+        'h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Input };

+ 201 - 0
components/ui/item.tsx

@@ -0,0 +1,201 @@
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { Separator } from '@/components/ui/separator';
+
+function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      role="list"
+      data-slot="item-group"
+      className={cn(
+        'gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof Separator>) {
+  return (
+    <Separator
+      data-slot="item-separator"
+      orientation="horizontal"
+      className={cn('my-2', className)}
+      {...props}
+    />
+  );
+}
+
+const itemVariants = cva(
+  '[a]:hover:bg-muted rounded-lg border text-sm group/item flex w-full flex-wrap items-center transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors',
+  {
+    variants: {
+      variant: {
+        default: 'border-transparent',
+        outline: 'border-border',
+        muted: 'bg-muted/50 border-transparent',
+      },
+      size: {
+        default: 'gap-2.5 px-3 py-2.5',
+        sm: 'gap-2.5 px-3 py-2.5',
+        xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  },
+);
+
+function Item({
+  className,
+  variant = 'default',
+  size = 'default',
+  render,
+  ...props
+}: useRender.ComponentProps<'div'> & VariantProps<typeof itemVariants>) {
+  return useRender({
+    defaultTagName: 'div',
+    props: mergeProps<'div'>(
+      {
+        className: cn(itemVariants({ variant, size, className })),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'item',
+      variant,
+      size,
+    },
+  });
+}
+
+const itemMediaVariants = cva(
+  'gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none',
+  {
+    variants: {
+      variant: {
+        default: 'bg-transparent',
+        icon: "[&_svg:not([class*='size-'])]:size-4",
+        image:
+          'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+);
+
+function ItemMedia({
+  className,
+  variant = 'default',
+  ...props
+}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
+  return (
+    <div
+      data-slot="item-media"
+      data-variant={variant}
+      className={cn(itemMediaVariants({ variant, className }))}
+      {...props}
+    />
+  );
+}
+
+function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="item-content"
+      className={cn(
+        'gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="item-title"
+      className={cn(
+        'gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
+  return (
+    <p
+      data-slot="item-description"
+      className={cn(
+        'text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="item-actions"
+      className={cn('gap-2 flex items-center', className)}
+      {...props}
+    />
+  );
+}
+
+function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="item-header"
+      className={cn(
+        'gap-2 flex basis-full items-center justify-between',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="item-footer"
+      className={cn(
+        'gap-2 flex basis-full items-center justify-between',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Item,
+  ItemMedia,
+  ItemContent,
+  ItemActions,
+  ItemGroup,
+  ItemSeparator,
+  ItemTitle,
+  ItemDescription,
+  ItemHeader,
+  ItemFooter,
+};

+ 26 - 0
components/ui/kbd.tsx

@@ -0,0 +1,26 @@
+import { cn } from '@/lib/utils';
+
+function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
+  return (
+    <kbd
+      data-slot="kbd"
+      className={cn(
+        "bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-sm px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3 pointer-events-none inline-flex items-center justify-center select-none",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <kbd
+      data-slot="kbd-group"
+      className={cn('gap-1 inline-flex items-center', className)}
+      {...props}
+    />
+  );
+}
+
+export { Kbd, KbdGroup };

+ 20 - 0
components/ui/label.tsx

@@ -0,0 +1,20 @@
+'use client';
+
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+function Label({ className, ...props }: React.ComponentProps<'label'>) {
+  return (
+    <label
+      data-slot="label"
+      className={cn(
+        'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Label };

+ 71 - 0
components/ui/marker.tsx

@@ -0,0 +1,71 @@
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const markerVariants = cva(
+  "gap-2 text-sm text-muted-foreground [a]:hover:text-foreground [a]:underline-offset-3 [a]:underline [&_svg:not([class*='size-'])]:size-4 min-h-4 text-left group/marker relative flex w-full items-center",
+  {
+    variants: {
+      variant: {
+        default: '',
+        separator:
+          'before:h-px before:min-w-0 before:flex-1 before:bg-border after:h-px after:min-w-0 after:flex-1 after:bg-border before:mr-1 after:ml-1',
+        border: 'border-b border-border pb-2',
+      },
+    },
+  },
+);
+
+function Marker({
+  className,
+  variant = 'default',
+  render,
+  ...props
+}: useRender.ComponentProps<'div'> & VariantProps<typeof markerVariants>) {
+  return useRender({
+    defaultTagName: 'div',
+    props: mergeProps<'div'>(
+      {
+        className: cn(markerVariants({ variant, className })),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'marker',
+      variant,
+    },
+  });
+}
+
+function MarkerIcon({ className, ...props }: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="marker-icon"
+      aria-hidden="true"
+      className={cn(
+        "size-4 [&_svg:not([class*='size-'])]:size-4 shrink-0",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MarkerContent({ className, ...props }: React.ComponentProps<'span'>) {
+  return (
+    <span
+      data-slot="marker-content"
+      className={cn(
+        'group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3 min-w-0 wrap-break-word',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Marker, MarkerIcon, MarkerContent, markerVariants };

+ 284 - 0
components/ui/menubar.tsx

@@ -0,0 +1,284 @@
+'use client';
+
+import * as React from 'react';
+import { Menu as MenuPrimitive } from '@base-ui/react/menu';
+import { Menubar as MenubarPrimitive } from '@base-ui/react/menubar';
+
+import { cn } from '@/lib/utils';
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuGroup,
+  DropdownMenuItem,
+  DropdownMenuLabel,
+  DropdownMenuPortal,
+  DropdownMenuRadioGroup,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubContent,
+  DropdownMenuSubTrigger,
+  DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { CheckIcon } from 'lucide-react';
+
+function Menubar({ className, ...props }: MenubarPrimitive.Props) {
+  return (
+    <MenubarPrimitive
+      data-slot="menubar"
+      className={cn(
+        'h-8 gap-0.5 rounded-lg border p-[3px] flex items-center',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarMenu({ ...props }: React.ComponentProps<typeof DropdownMenu>) {
+  return <DropdownMenu data-slot="menubar-menu" {...props} />;
+}
+
+function MenubarGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuGroup>) {
+  return <DropdownMenuGroup data-slot="menubar-group" {...props} />;
+}
+
+function MenubarPortal({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuPortal>) {
+  return <DropdownMenuPortal data-slot="menubar-portal" {...props} />;
+}
+
+function MenubarTrigger({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuTrigger>) {
+  return (
+    <DropdownMenuTrigger
+      data-slot="menubar-trigger"
+      className={cn(
+        'hover:bg-muted aria-expanded:bg-muted rounded-sm px-1.5 py-[2px] text-sm font-medium flex items-center outline-hidden select-none',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarContent({
+  className,
+  align = 'start',
+  alignOffset = -4,
+  sideOffset = 8,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuContent>) {
+  return (
+    <DropdownMenuContent
+      data-slot="menubar-content"
+      align={align}
+      alignOffset={alignOffset}
+      sideOffset={sideOffset}
+      className={cn(
+        'bg-popover text-popover-foreground data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 cn-menu-target cn-menu-translucent',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarItem({
+  className,
+  inset,
+  variant = 'default',
+  ...props
+}: React.ComponentProps<typeof DropdownMenuItem>) {
+  return (
+    <DropdownMenuItem
+      data-slot="menubar-item"
+      data-inset={inset}
+      data-variant={variant}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-disabled:opacity-50 data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/menubar-item",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarCheckboxItem({
+  className,
+  children,
+  checked,
+  inset,
+  ...props
+}: MenuPrimitive.CheckboxItem.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <MenuPrimitive.CheckboxItem
+      data-slot="menubar-checkbox-item"
+      data-inset={inset}
+      className={cn(
+        'focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm data-inset:pl-7 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
+        className,
+      )}
+      checked={checked}
+      {...props}
+    >
+      <span className="left-1.5 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center">
+        <MenuPrimitive.CheckboxItemIndicator>
+          <CheckIcon />
+        </MenuPrimitive.CheckboxItemIndicator>
+      </span>
+      {children}
+    </MenuPrimitive.CheckboxItem>
+  );
+}
+
+function MenubarRadioGroup({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuRadioGroup>) {
+  return <DropdownMenuRadioGroup data-slot="menubar-radio-group" {...props} />;
+}
+
+function MenubarRadioItem({
+  className,
+  children,
+  inset,
+  ...props
+}: MenuPrimitive.RadioItem.Props & {
+  inset?: boolean;
+}) {
+  return (
+    <MenuPrimitive.RadioItem
+      data-slot="menubar-radio-item"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm data-disabled:opacity-50 data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0",
+        className,
+      )}
+      {...props}
+    >
+      <span className="left-1.5 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center">
+        <MenuPrimitive.RadioItemIndicator>
+          <CheckIcon />
+        </MenuPrimitive.RadioItemIndicator>
+      </span>
+      {children}
+    </MenuPrimitive.RadioItem>
+  );
+}
+
+function MenubarLabel({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuLabel> & {
+  inset?: boolean;
+}) {
+  return (
+    <DropdownMenuLabel
+      data-slot="menubar-label"
+      data-inset={inset}
+      className={cn(
+        'px-1.5 py-1 text-sm font-medium data-inset:pl-7',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuSeparator>) {
+  return (
+    <DropdownMenuSeparator
+      data-slot="menubar-separator"
+      className={cn('bg-border -mx-1 my-1 h-px', className)}
+      {...props}
+    />
+  );
+}
+
+function MenubarShortcut({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuShortcut>) {
+  return (
+    <DropdownMenuShortcut
+      data-slot="menubar-shortcut"
+      className={cn(
+        'text-muted-foreground group-focus/menubar-item:text-accent-foreground text-xs tracking-widest ml-auto',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarSub({
+  ...props
+}: React.ComponentProps<typeof DropdownMenuSub>) {
+  return <DropdownMenuSub data-slot="menubar-sub" {...props} />;
+}
+
+function MenubarSubTrigger({
+  className,
+  inset,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuSubTrigger> & {
+  inset?: boolean;
+}) {
+  return (
+    <DropdownMenuSubTrigger
+      data-slot="menubar-sub-trigger"
+      data-inset={inset}
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MenubarSubContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof DropdownMenuSubContent>) {
+  return (
+    <DropdownMenuSubContent
+      data-slot="menubar-sub-content"
+      className={cn(
+        'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-32 rounded-lg p-1 shadow-lg ring-1 duration-100 cn-menu-target cn-menu-translucent',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  Menubar,
+  MenubarPortal,
+  MenubarMenu,
+  MenubarTrigger,
+  MenubarContent,
+  MenubarGroup,
+  MenubarSeparator,
+  MenubarLabel,
+  MenubarItem,
+  MenubarShortcut,
+  MenubarCheckboxItem,
+  MenubarRadioGroup,
+  MenubarRadioItem,
+  MenubarSub,
+  MenubarSubTrigger,
+  MenubarSubContent,
+};

+ 130 - 0
components/ui/message-scroller.tsx

@@ -0,0 +1,130 @@
+'use client';
+
+import * as React from 'react';
+import {
+  MessageScroller as MessageScrollerPrimitive,
+  useMessageScroller,
+  useMessageScrollerScrollable,
+  useMessageScrollerVisibility,
+} from '@shadcn/react/message-scroller';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { ArrowDownIcon } from 'lucide-react';
+
+function MessageScrollerProvider(
+  props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>,
+) {
+  return <MessageScrollerPrimitive.Provider {...props} />;
+}
+
+function MessageScroller({
+  className,
+  ...props
+}: React.ComponentProps<typeof MessageScrollerPrimitive.Root>) {
+  return (
+    <MessageScrollerPrimitive.Root
+      data-slot="message-scroller"
+      className={cn(
+        'group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MessageScrollerViewport({
+  className,
+  ...props
+}: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>) {
+  return (
+    <MessageScrollerPrimitive.Viewport
+      data-slot="message-scroller-viewport"
+      className={cn(
+        'size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MessageScrollerContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof MessageScrollerPrimitive.Content>) {
+  return (
+    <MessageScrollerPrimitive.Content
+      data-slot="message-scroller-content"
+      className={cn('gap-6 flex h-max min-h-full flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function MessageScrollerItem({
+  className,
+  scrollAnchor = false,
+  ...props
+}: React.ComponentProps<typeof MessageScrollerPrimitive.Item>) {
+  return (
+    <MessageScrollerPrimitive.Item
+      data-slot="message-scroller-item"
+      scrollAnchor={scrollAnchor}
+      className={cn(
+        'min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MessageScrollerButton({
+  direction = 'end',
+  className,
+  children,
+  render,
+  variant = 'secondary',
+  size = 'icon-sm',
+  ...props
+}: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &
+  Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
+  return (
+    <MessageScrollerPrimitive.Button
+      data-slot="message-scroller-button"
+      data-direction={direction}
+      data-variant={variant}
+      data-size={size}
+      direction={direction}
+      className={cn(
+        'absolute inset-s-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180',
+        className,
+      )}
+      render={render ?? <Button variant={variant} size={size} />}
+      {...props}
+    >
+      {children ?? (
+        <>
+          <ArrowDownIcon />
+          <span className="sr-only">
+            {direction === 'end' ? 'Scroll to end' : 'Scroll to start'}
+          </span>
+        </>
+      )}
+    </MessageScrollerPrimitive.Button>
+  );
+}
+
+export {
+  MessageScrollerProvider,
+  MessageScroller,
+  MessageScrollerViewport,
+  MessageScrollerContent,
+  MessageScrollerItem,
+  MessageScrollerButton,
+  useMessageScroller,
+  useMessageScrollerScrollable,
+  useMessageScrollerVisibility,
+};

+ 92 - 0
components/ui/message.tsx

@@ -0,0 +1,92 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+function MessageGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="message-group"
+      className={cn('gap-2 flex min-w-0 flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function Message({
+  className,
+  align = 'start',
+  ...props
+}: React.ComponentProps<'div'> & { align?: 'start' | 'end' }) {
+  return (
+    <div
+      data-slot="message"
+      data-align={align}
+      className={cn(
+        'text-sm gap-2 group/message relative flex w-full min-w-0 data-[align=end]:flex-row-reverse',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MessageAvatar({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="message-avatar"
+      className={cn(
+        'min-w-8 group-has-data-[slot=message-footer]/message:-translate-y-8 flex w-fit shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MessageContent({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="message-content"
+      className={cn(
+        'gap-2.5 group-data-[align=end]/message:*:data-slot:self-end flex w-full min-w-0 flex-col wrap-break-word',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MessageHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="message-header"
+      className={cn(
+        'text-xs font-medium text-muted-foreground px-3 group-has-data-[variant=ghost]/message:px-0 flex max-w-full min-w-0 items-center',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function MessageFooter({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="message-footer"
+      className={cn(
+        'text-xs font-medium text-muted-foreground px-3 group-has-data-[variant=ghost]/message:px-0 flex max-w-full min-w-0 items-center group-data-[align=end]/message:justify-end',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export {
+  MessageGroup,
+  Message,
+  MessageAvatar,
+  MessageContent,
+  MessageFooter,
+  MessageHeader,
+};

+ 65 - 0
components/ui/native-select.tsx

@@ -0,0 +1,65 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+import { ChevronDownIcon } from 'lucide-react';
+
+type NativeSelectProps = Omit<React.ComponentProps<'select'>, 'size'> & {
+  size?: 'sm' | 'default';
+};
+
+function NativeSelect({
+  className,
+  size = 'default',
+  ...props
+}: NativeSelectProps) {
+  return (
+    <div
+      className={cn(
+        'group/native-select relative w-fit has-[select:disabled]:opacity-50',
+        className,
+      )}
+      data-slot="native-select-wrapper"
+      data-size={size}
+    >
+      <select
+        data-slot="native-select"
+        data-size={size}
+        className="border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 w-full min-w-0 appearance-none rounded-lg border bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 outline-none disabled:pointer-events-none disabled:cursor-not-allowed"
+        {...props}
+      />
+      <ChevronDownIcon
+        className="text-muted-foreground top-1/2 right-2.5 size-4 -translate-y-1/2 pointer-events-none absolute select-none"
+        aria-hidden="true"
+        data-slot="native-select-icon"
+      />
+    </div>
+  );
+}
+
+function NativeSelectOption({
+  className,
+  ...props
+}: React.ComponentProps<'option'>) {
+  return (
+    <option
+      data-slot="native-select-option"
+      className={cn('bg-[Canvas] text-[CanvasText]', className)}
+      {...props}
+    />
+  );
+}
+
+function NativeSelectOptGroup({
+  className,
+  ...props
+}: React.ComponentProps<'optgroup'>) {
+  return (
+    <optgroup
+      data-slot="native-select-optgroup"
+      className={cn('bg-[Canvas] text-[CanvasText]', className)}
+      {...props}
+    />
+  );
+}
+
+export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };

+ 171 - 0
components/ui/navigation-menu.tsx

@@ -0,0 +1,171 @@
+import { NavigationMenu as NavigationMenuPrimitive } from '@base-ui/react/navigation-menu';
+import { cva } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { ChevronDownIcon } from 'lucide-react';
+
+function NavigationMenu({
+  align = 'start',
+  className,
+  children,
+  ...props
+}: NavigationMenuPrimitive.Root.Props &
+  Pick<NavigationMenuPrimitive.Positioner.Props, 'align'>) {
+  return (
+    <NavigationMenuPrimitive.Root
+      data-slot="navigation-menu"
+      className={cn(
+        'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      <NavigationMenuPositioner align={align} />
+    </NavigationMenuPrimitive.Root>
+  );
+}
+
+function NavigationMenuList({
+  className,
+  ...props
+}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.List>) {
+  return (
+    <NavigationMenuPrimitive.List
+      data-slot="navigation-menu-list"
+      className={cn(
+        'gap-0 group flex flex-1 list-none items-center justify-center',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function NavigationMenuItem({
+  className,
+  ...props
+}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.Item>) {
+  return (
+    <NavigationMenuPrimitive.Item
+      data-slot="navigation-menu-item"
+      className={cn('relative', className)}
+      {...props}
+    />
+  );
+}
+
+const navigationMenuTriggerStyle = cva(
+  'hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all focus-visible:ring-3 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none',
+);
+
+function NavigationMenuTrigger({
+  className,
+  children,
+  ...props
+}: NavigationMenuPrimitive.Trigger.Props) {
+  return (
+    <NavigationMenuPrimitive.Trigger
+      data-slot="navigation-menu-trigger"
+      className={cn(navigationMenuTriggerStyle(), 'group', className)}
+      {...props}
+    >
+      {children}{' '}
+      <ChevronDownIcon
+        className="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180"
+        aria-hidden="true"
+      />
+    </NavigationMenuPrimitive.Trigger>
+  );
+}
+
+function NavigationMenuContent({
+  className,
+  ...props
+}: NavigationMenuPrimitive.Content.Props) {
+  return (
+    <NavigationMenuPrimitive.Content
+      data-slot="navigation-menu-content"
+      className={cn(
+        'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:ring-foreground/10 p-1 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto transition-[opacity,transform,translate] duration-[0.35s] data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function NavigationMenuPositioner({
+  className,
+  side = 'bottom',
+  sideOffset = 8,
+  align = 'start',
+  alignOffset = 0,
+  ...props
+}: NavigationMenuPrimitive.Positioner.Props) {
+  return (
+    <NavigationMenuPrimitive.Portal>
+      <NavigationMenuPrimitive.Positioner
+        side={side}
+        sideOffset={sideOffset}
+        align={align}
+        alignOffset={alignOffset}
+        className={cn(
+          'ease-[cubic-bezier(0.22,1,0.36,1)] data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0 isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-[0.35s] data-instant:transition-none',
+          className,
+        )}
+        {...props}
+      >
+        <NavigationMenuPrimitive.Popup className="bg-popover text-popover-foreground ring-foreground/10 rounded-lg shadow ring-1 outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 data-[ending-style]:easing-[ease] xs:w-(--popup-width) relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) transition-[opacity,transform,width,height,scale,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)]">
+          <NavigationMenuPrimitive.Viewport className="relative size-full overflow-hidden" />
+        </NavigationMenuPrimitive.Popup>
+      </NavigationMenuPrimitive.Positioner>
+    </NavigationMenuPrimitive.Portal>
+  );
+}
+
+function NavigationMenuLink({
+  className,
+  ...props
+}: NavigationMenuPrimitive.Link.Props) {
+  return (
+    <NavigationMenuPrimitive.Link
+      data-slot="navigation-menu-link"
+      className={cn(
+        "data-active:focus:bg-muted data-active:hover:bg-muted data-active:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none focus-visible:ring-3 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function NavigationMenuIndicator({
+  className,
+  ...props
+}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.Icon>) {
+  return (
+    <NavigationMenuPrimitive.Icon
+      data-slot="navigation-menu-indicator"
+      className={cn(
+        'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden',
+        className,
+      )}
+      {...props}
+    >
+      <div className="bg-border rounded-tl-sm shadow-md relative top-[60%] h-2 w-2 rotate-45" />
+    </NavigationMenuPrimitive.Icon>
+  );
+}
+
+export {
+  NavigationMenu,
+  NavigationMenuContent,
+  NavigationMenuIndicator,
+  NavigationMenuItem,
+  NavigationMenuLink,
+  NavigationMenuList,
+  NavigationMenuTrigger,
+  navigationMenuTriggerStyle,
+  NavigationMenuPositioner,
+};

+ 133 - 0
components/ui/pagination.tsx

@@ -0,0 +1,133 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import {
+  ChevronLeftIcon,
+  ChevronRightIcon,
+  MoreHorizontalIcon,
+} from 'lucide-react';
+
+function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
+  return (
+    <nav
+      role="navigation"
+      aria-label="pagination"
+      data-slot="pagination"
+      className={cn('mx-auto flex w-full justify-center', className)}
+      {...props}
+    />
+  );
+}
+
+function PaginationContent({
+  className,
+  ...props
+}: React.ComponentProps<'ul'>) {
+  return (
+    <ul
+      data-slot="pagination-content"
+      className={cn('gap-0.5 flex items-center', className)}
+      {...props}
+    />
+  );
+}
+
+function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
+  return <li data-slot="pagination-item" {...props} />;
+}
+
+type PaginationLinkProps = {
+  isActive?: boolean;
+} & Pick<React.ComponentProps<typeof Button>, 'size'> &
+  React.ComponentProps<'a'>;
+
+function PaginationLink({
+  className,
+  isActive,
+  size = 'icon',
+  ...props
+}: PaginationLinkProps) {
+  return (
+    <Button
+      variant={isActive ? 'outline' : 'ghost'}
+      size={size}
+      className={cn(className)}
+      nativeButton={false}
+      render={
+        <a
+          aria-current={isActive ? 'page' : undefined}
+          data-slot="pagination-link"
+          data-active={isActive}
+          {...props}
+        />
+      }
+    />
+  );
+}
+
+function PaginationPrevious({
+  className,
+  text = 'Previous',
+  ...props
+}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
+  return (
+    <PaginationLink
+      aria-label="Go to previous page"
+      size="default"
+      className={cn('pl-1.5!', className)}
+      {...props}
+    >
+      <ChevronLeftIcon data-icon="inline-start" className="cn-rtl-flip" />
+      <span className="hidden sm:block">{text}</span>
+    </PaginationLink>
+  );
+}
+
+function PaginationNext({
+  className,
+  text = 'Next',
+  ...props
+}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
+  return (
+    <PaginationLink
+      aria-label="Go to next page"
+      size="default"
+      className={cn('pr-1.5!', className)}
+      {...props}
+    >
+      <span className="hidden sm:block">{text}</span>
+      <ChevronRightIcon data-icon="inline-end" className="cn-rtl-flip" />
+    </PaginationLink>
+  );
+}
+
+function PaginationEllipsis({
+  className,
+  ...props
+}: React.ComponentProps<'span'>) {
+  return (
+    <span
+      aria-hidden
+      data-slot="pagination-ellipsis"
+      className={cn(
+        "size-8 [&_svg:not([class*='size-'])]:size-4 flex items-center justify-center",
+        className,
+      )}
+      {...props}
+    >
+      <MoreHorizontalIcon />
+      <span className="sr-only">More pages</span>
+    </span>
+  );
+}
+
+export {
+  Pagination,
+  PaginationContent,
+  PaginationEllipsis,
+  PaginationItem,
+  PaginationLink,
+  PaginationNext,
+  PaginationPrevious,
+};

+ 90 - 0
components/ui/popover.tsx

@@ -0,0 +1,90 @@
+'use client';
+
+import * as React from 'react';
+import { Popover as PopoverPrimitive } from '@base-ui/react/popover';
+
+import { cn } from '@/lib/utils';
+
+function Popover({ ...props }: PopoverPrimitive.Root.Props) {
+  return <PopoverPrimitive.Root data-slot="popover" {...props} />;
+}
+
+function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
+  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
+}
+
+function PopoverContent({
+  className,
+  align = 'center',
+  alignOffset = 0,
+  side = 'bottom',
+  sideOffset = 4,
+  ...props
+}: PopoverPrimitive.Popup.Props &
+  Pick<
+    PopoverPrimitive.Positioner.Props,
+    'align' | 'alignOffset' | 'side' | 'sideOffset'
+  >) {
+  return (
+    <PopoverPrimitive.Portal>
+      <PopoverPrimitive.Positioner
+        align={align}
+        alignOffset={alignOffset}
+        side={side}
+        sideOffset={sideOffset}
+        className="isolate z-50"
+      >
+        <PopoverPrimitive.Popup
+          data-slot="popover-content"
+          className={cn(
+            'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-72 origin-(--transform-origin) outline-hidden',
+            className,
+          )}
+          {...props}
+        />
+      </PopoverPrimitive.Positioner>
+    </PopoverPrimitive.Portal>
+  );
+}
+
+function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="popover-header"
+      className={cn('flex flex-col gap-0.5 text-sm', className)}
+      {...props}
+    />
+  );
+}
+
+function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
+  return (
+    <PopoverPrimitive.Title
+      data-slot="popover-title"
+      className={cn('font-medium', className)}
+      {...props}
+    />
+  );
+}
+
+function PopoverDescription({
+  className,
+  ...props
+}: PopoverPrimitive.Description.Props) {
+  return (
+    <PopoverPrimitive.Description
+      data-slot="popover-description"
+      className={cn('text-muted-foreground', className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Popover,
+  PopoverContent,
+  PopoverDescription,
+  PopoverHeader,
+  PopoverTitle,
+  PopoverTrigger,
+};

+ 83 - 0
components/ui/progress.tsx

@@ -0,0 +1,83 @@
+'use client';
+
+import { Progress as ProgressPrimitive } from '@base-ui/react/progress';
+
+import { cn } from '@/lib/utils';
+
+function Progress({
+  className,
+  children,
+  value,
+  ...props
+}: ProgressPrimitive.Root.Props) {
+  return (
+    <ProgressPrimitive.Root
+      value={value}
+      data-slot="progress"
+      className={cn('flex flex-wrap gap-3', className)}
+      {...props}
+    >
+      {children}
+      <ProgressTrack>
+        <ProgressIndicator />
+      </ProgressTrack>
+    </ProgressPrimitive.Root>
+  );
+}
+
+function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
+  return (
+    <ProgressPrimitive.Track
+      className={cn(
+        'bg-muted h-1 rounded-full relative flex w-full items-center overflow-x-hidden',
+        className,
+      )}
+      data-slot="progress-track"
+      {...props}
+    />
+  );
+}
+
+function ProgressIndicator({
+  className,
+  ...props
+}: ProgressPrimitive.Indicator.Props) {
+  return (
+    <ProgressPrimitive.Indicator
+      data-slot="progress-indicator"
+      className={cn('bg-primary h-full transition-all', className)}
+      {...props}
+    />
+  );
+}
+
+function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
+  return (
+    <ProgressPrimitive.Label
+      className={cn('text-sm font-medium', className)}
+      data-slot="progress-label"
+      {...props}
+    />
+  );
+}
+
+function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
+  return (
+    <ProgressPrimitive.Value
+      className={cn(
+        'text-muted-foreground ml-auto text-sm tabular-nums',
+        className,
+      )}
+      data-slot="progress-value"
+      {...props}
+    />
+  );
+}
+
+export {
+  Progress,
+  ProgressTrack,
+  ProgressIndicator,
+  ProgressLabel,
+  ProgressValue,
+};

+ 38 - 0
components/ui/radio-group.tsx

@@ -0,0 +1,38 @@
+'use client';
+
+import { Radio as RadioPrimitive } from '@base-ui/react/radio';
+import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group';
+
+import { cn } from '@/lib/utils';
+
+function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
+  return (
+    <RadioGroupPrimitive
+      data-slot="radio-group"
+      className={cn('grid gap-2 w-full', className)}
+      {...props}
+    />
+  );
+}
+
+function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
+  return (
+    <RadioPrimitive.Root
+      data-slot="radio-group-item"
+      className={cn(
+        'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:border-destructive/50 flex size-4 rounded-full focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-[:focus-visible]/field-label:data-checked:border-primary group/radio-group-item peer relative aspect-square shrink-0 border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50',
+        className,
+      )}
+      {...props}
+    >
+      <RadioPrimitive.Indicator
+        data-slot="radio-group-indicator"
+        className="flex size-4 items-center justify-center"
+      >
+        <span className="bg-primary-foreground absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full" />
+      </RadioPrimitive.Indicator>
+    </RadioPrimitive.Root>
+  );
+}
+
+export { RadioGroup, RadioGroupItem };

+ 50 - 0
components/ui/resizable.tsx

@@ -0,0 +1,50 @@
+'use client';
+
+import * as ResizablePrimitive from 'react-resizable-panels';
+
+import { cn } from '@/lib/utils';
+
+function ResizablePanelGroup({
+  className,
+  ...props
+}: ResizablePrimitive.GroupProps) {
+  return (
+    <ResizablePrimitive.Group
+      data-slot="resizable-panel-group"
+      className={cn(
+        'flex h-full w-full aria-[orientation=vertical]:flex-col',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
+  return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
+}
+
+function ResizableHandle({
+  withHandle,
+  className,
+  ...props
+}: ResizablePrimitive.SeparatorProps & {
+  withHandle?: boolean;
+}) {
+  return (
+    <ResizablePrimitive.Separator
+      data-slot="resizable-handle"
+      className={cn(
+        'relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90',
+        className,
+      )}
+      {...props}
+    >
+      {withHandle && (
+        <div className="bg-border h-6 w-1 rounded-lg z-10 flex shrink-0" />
+      )}
+    </ResizablePrimitive.Separator>
+  );
+}
+
+export { ResizableHandle, ResizablePanel, ResizablePanelGroup };

+ 55 - 0
components/ui/scroll-area.tsx

@@ -0,0 +1,55 @@
+'use client';
+
+import * as React from 'react';
+import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area';
+
+import { cn } from '@/lib/utils';
+
+function ScrollArea({
+  className,
+  children,
+  ...props
+}: ScrollAreaPrimitive.Root.Props) {
+  return (
+    <ScrollAreaPrimitive.Root
+      data-slot="scroll-area"
+      className={cn('relative', className)}
+      {...props}
+    >
+      <ScrollAreaPrimitive.Viewport
+        data-slot="scroll-area-viewport"
+        className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
+      >
+        {children}
+      </ScrollAreaPrimitive.Viewport>
+      <ScrollBar />
+      <ScrollAreaPrimitive.Corner />
+    </ScrollAreaPrimitive.Root>
+  );
+}
+
+function ScrollBar({
+  className,
+  orientation = 'vertical',
+  ...props
+}: ScrollAreaPrimitive.Scrollbar.Props) {
+  return (
+    <ScrollAreaPrimitive.Scrollbar
+      data-slot="scroll-area-scrollbar"
+      data-orientation={orientation}
+      orientation={orientation}
+      className={cn(
+        'data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none',
+        className,
+      )}
+      {...props}
+    >
+      <ScrollAreaPrimitive.Thumb
+        data-slot="scroll-area-thumb"
+        className="rounded-full relative flex-1 bg-border"
+      />
+    </ScrollAreaPrimitive.Scrollbar>
+  );
+}
+
+export { ScrollArea, ScrollBar };

+ 202 - 0
components/ui/select.tsx

@@ -0,0 +1,202 @@
+'use client';
+
+import * as React from 'react';
+import { Select as SelectPrimitive } from '@base-ui/react/select';
+
+import { cn } from '@/lib/utils';
+import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react';
+
+const Select = SelectPrimitive.Root;
+
+function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
+  return (
+    <SelectPrimitive.Group
+      data-slot="select-group"
+      className={cn('scroll-my-1 p-1', className)}
+      {...props}
+    />
+  );
+}
+
+function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
+  return (
+    <SelectPrimitive.Value
+      data-slot="select-value"
+      className={cn('flex flex-1 text-left', className)}
+      {...props}
+    />
+  );
+}
+
+function SelectTrigger({
+  className,
+  size = 'default',
+  children,
+  ...props
+}: SelectPrimitive.Trigger.Props & {
+  size?: 'sm' | 'default';
+}) {
+  return (
+    <SelectPrimitive.Trigger
+      data-slot="select-trigger"
+      data-size={size}
+      className={cn(
+        "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    >
+      {children}
+      <SelectPrimitive.Icon
+        render={
+          <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
+        }
+      />
+    </SelectPrimitive.Trigger>
+  );
+}
+
+function SelectContent({
+  className,
+  children,
+  side = 'bottom',
+  sideOffset = 4,
+  align = 'center',
+  alignOffset = 0,
+  alignItemWithTrigger = true,
+  ...props
+}: SelectPrimitive.Popup.Props &
+  Pick<
+    SelectPrimitive.Positioner.Props,
+    'align' | 'alignOffset' | 'side' | 'sideOffset' | 'alignItemWithTrigger'
+  >) {
+  return (
+    <SelectPrimitive.Portal>
+      <SelectPrimitive.Positioner
+        side={side}
+        sideOffset={sideOffset}
+        align={align}
+        alignOffset={alignOffset}
+        alignItemWithTrigger={alignItemWithTrigger}
+        className="isolate z-50"
+      >
+        <SelectPrimitive.Popup
+          data-slot="select-content"
+          data-align-trigger={alignItemWithTrigger}
+          className={cn(
+            'relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
+            className,
+          )}
+          {...props}
+        >
+          <SelectScrollUpButton />
+          <SelectPrimitive.List>{children}</SelectPrimitive.List>
+          <SelectScrollDownButton />
+        </SelectPrimitive.Popup>
+      </SelectPrimitive.Positioner>
+    </SelectPrimitive.Portal>
+  );
+}
+
+function SelectLabel({
+  className,
+  ...props
+}: SelectPrimitive.GroupLabel.Props) {
+  return (
+    <SelectPrimitive.GroupLabel
+      data-slot="select-label"
+      className={cn('px-1.5 py-1 text-xs text-muted-foreground', className)}
+      {...props}
+    />
+  );
+}
+
+function SelectItem({
+  className,
+  children,
+  ...props
+}: SelectPrimitive.Item.Props) {
+  return (
+    <SelectPrimitive.Item
+      data-slot="select-item"
+      className={cn(
+        "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+        className,
+      )}
+      {...props}
+    >
+      <SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
+        {children}
+      </SelectPrimitive.ItemText>
+      <SelectPrimitive.ItemIndicator
+        render={
+          <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
+        }
+      >
+        <CheckIcon className="pointer-events-none" />
+      </SelectPrimitive.ItemIndicator>
+    </SelectPrimitive.Item>
+  );
+}
+
+function SelectSeparator({
+  className,
+  ...props
+}: SelectPrimitive.Separator.Props) {
+  return (
+    <SelectPrimitive.Separator
+      data-slot="select-separator"
+      className={cn('pointer-events-none -mx-1 my-1 h-px bg-border', className)}
+      {...props}
+    />
+  );
+}
+
+function SelectScrollUpButton({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
+  return (
+    <SelectPrimitive.ScrollUpArrow
+      data-slot="select-scroll-up-button"
+      className={cn(
+        "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    >
+      <ChevronUpIcon />
+    </SelectPrimitive.ScrollUpArrow>
+  );
+}
+
+function SelectScrollDownButton({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
+  return (
+    <SelectPrimitive.ScrollDownArrow
+      data-slot="select-scroll-down-button"
+      className={cn(
+        "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
+        className,
+      )}
+      {...props}
+    >
+      <ChevronDownIcon />
+    </SelectPrimitive.ScrollDownArrow>
+  );
+}
+
+export {
+  Select,
+  SelectContent,
+  SelectGroup,
+  SelectItem,
+  SelectLabel,
+  SelectScrollDownButton,
+  SelectScrollUpButton,
+  SelectSeparator,
+  SelectTrigger,
+  SelectValue,
+};

+ 25 - 0
components/ui/separator.tsx

@@ -0,0 +1,25 @@
+'use client';
+
+import { Separator as SeparatorPrimitive } from '@base-ui/react/separator';
+
+import { cn } from '@/lib/utils';
+
+function Separator({
+  className,
+  orientation = 'horizontal',
+  ...props
+}: SeparatorPrimitive.Props) {
+  return (
+    <SeparatorPrimitive
+      data-slot="separator"
+      orientation={orientation}
+      className={cn(
+        'shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Separator };

+ 137 - 0
components/ui/sheet.tsx

@@ -0,0 +1,137 @@
+'use client';
+
+import * as React from 'react';
+import { Dialog as SheetPrimitive } from '@base-ui/react/dialog';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { XIcon } from 'lucide-react';
+
+function Sheet({ ...props }: SheetPrimitive.Root.Props) {
+  return <SheetPrimitive.Root data-slot="sheet" {...props} />;
+}
+
+function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
+  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
+}
+
+function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
+  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
+}
+
+function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
+  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
+}
+
+function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
+  return (
+    <SheetPrimitive.Backdrop
+      data-slot="sheet-overlay"
+      className={cn(
+        'bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SheetContent({
+  className,
+  children,
+  side = 'right',
+  showCloseButton = true,
+  ...props
+}: SheetPrimitive.Popup.Props & {
+  side?: 'top' | 'right' | 'bottom' | 'left';
+  showCloseButton?: boolean;
+}) {
+  return (
+    <SheetPortal>
+      <SheetOverlay />
+      <SheetPrimitive.Popup
+        data-slot="sheet-content"
+        data-side={side}
+        className={cn(
+          'bg-popover text-popover-foreground fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem]',
+          className,
+        )}
+        {...props}
+      >
+        {children}
+        {showCloseButton && (
+          <SheetPrimitive.Close
+            data-slot="sheet-close"
+            render={
+              <Button
+                variant="ghost"
+                className="absolute top-3 right-3"
+                size="icon-sm"
+              />
+            }
+          >
+            <XIcon />
+            <span className="sr-only">Close</span>
+          </SheetPrimitive.Close>
+        )}
+      </SheetPrimitive.Popup>
+    </SheetPortal>
+  );
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="sheet-header"
+      className={cn('gap-0.5 p-4 flex flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="sheet-footer"
+      className={cn('gap-2 p-4 mt-auto flex flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
+  return (
+    <SheetPrimitive.Title
+      data-slot="sheet-title"
+      className={cn(
+        'text-foreground text-base font-medium cn-font-heading',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SheetDescription({
+  className,
+  ...props
+}: SheetPrimitive.Description.Props) {
+  return (
+    <SheetPrimitive.Description
+      data-slot="sheet-description"
+      className={cn('text-muted-foreground text-sm', className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Sheet,
+  SheetTrigger,
+  SheetClose,
+  SheetContent,
+  SheetHeader,
+  SheetFooter,
+  SheetTitle,
+  SheetDescription,
+};

+ 723 - 0
components/ui/sidebar.tsx

@@ -0,0 +1,723 @@
+'use client';
+
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { useIsMobile } from '@/hooks/use-mobile';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Separator } from '@/components/ui/separator';
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetHeader,
+  SheetTitle,
+} from '@/components/ui/sheet';
+import { Skeleton } from '@/components/ui/skeleton';
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { PanelLeftIcon } from 'lucide-react';
+
+const SIDEBAR_COOKIE_NAME = 'sidebar_state';
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = '16rem';
+const SIDEBAR_WIDTH_MOBILE = '18rem';
+const SIDEBAR_WIDTH_ICON = '3rem';
+const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
+
+type SidebarContextProps = {
+  state: 'expanded' | 'collapsed';
+  open: boolean;
+  setOpen: (open: boolean) => void;
+  openMobile: boolean;
+  setOpenMobile: (open: boolean) => void;
+  isMobile: boolean;
+  toggleSidebar: () => void;
+};
+
+const SidebarContext = React.createContext<SidebarContextProps | null>(null);
+
+function useSidebar() {
+  const context = React.useContext(SidebarContext);
+  if (!context) {
+    throw new Error('useSidebar must be used within a SidebarProvider.');
+  }
+
+  return context;
+}
+
+function SidebarProvider({
+  defaultOpen = true,
+  open: openProp,
+  onOpenChange: setOpenProp,
+  className,
+  style,
+  children,
+  ...props
+}: React.ComponentProps<'div'> & {
+  defaultOpen?: boolean;
+  open?: boolean;
+  onOpenChange?: (open: boolean) => void;
+}) {
+  const isMobile = useIsMobile();
+  const [openMobile, setOpenMobile] = React.useState(false);
+
+  // This is the internal state of the sidebar.
+  // We use openProp and setOpenProp for control from outside the component.
+  const [_open, _setOpen] = React.useState(defaultOpen);
+  const open = openProp ?? _open;
+  const setOpen = React.useCallback(
+    (value: boolean | ((value: boolean) => boolean)) => {
+      const openState = typeof value === 'function' ? value(open) : value;
+      if (setOpenProp) {
+        setOpenProp(openState);
+      } else {
+        _setOpen(openState);
+      }
+
+      // This sets the cookie to keep the sidebar state.
+      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+    },
+    [setOpenProp, open],
+  );
+
+  // Helper to toggle the sidebar.
+  const toggleSidebar = React.useCallback(() => {
+    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
+  }, [isMobile, setOpen, setOpenMobile]);
+
+  // Adds a keyboard shortcut to toggle the sidebar.
+  React.useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (
+        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+        (event.metaKey || event.ctrlKey)
+      ) {
+        event.preventDefault();
+        toggleSidebar();
+      }
+    };
+
+    window.addEventListener('keydown', handleKeyDown);
+    return () => window.removeEventListener('keydown', handleKeyDown);
+  }, [toggleSidebar]);
+
+  // We add a state so that we can do data-state="expanded" or "collapsed".
+  // This makes it easier to style the sidebar with Tailwind classes.
+  const state = open ? 'expanded' : 'collapsed';
+
+  const contextValue = React.useMemo<SidebarContextProps>(
+    () => ({
+      state,
+      open,
+      setOpen,
+      isMobile,
+      openMobile,
+      setOpenMobile,
+      toggleSidebar,
+    }),
+    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+  );
+
+  return (
+    <SidebarContext.Provider value={contextValue}>
+      <div
+        data-slot="sidebar-wrapper"
+        style={
+          {
+            '--sidebar-width': SIDEBAR_WIDTH,
+            '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
+            ...style,
+          } as React.CSSProperties
+        }
+        className={cn(
+          'group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar',
+          className,
+        )}
+        {...props}
+      >
+        {children}
+      </div>
+    </SidebarContext.Provider>
+  );
+}
+
+function Sidebar({
+  side = 'left',
+  variant = 'sidebar',
+  collapsible = 'offcanvas',
+  className,
+  children,
+  dir,
+  ...props
+}: React.ComponentProps<'div'> & {
+  side?: 'left' | 'right';
+  variant?: 'sidebar' | 'floating' | 'inset';
+  collapsible?: 'offcanvas' | 'icon' | 'none';
+}) {
+  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+
+  if (collapsible === 'none') {
+    return (
+      <div
+        data-slot="sidebar"
+        className={cn(
+          'flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground',
+          className,
+        )}
+        {...props}
+      >
+        {children}
+      </div>
+    );
+  }
+
+  if (isMobile) {
+    return (
+      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
+        <SheetContent
+          dir={dir}
+          data-sidebar="sidebar"
+          data-slot="sidebar"
+          data-mobile="true"
+          className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
+          style={
+            {
+              '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
+            } as React.CSSProperties
+          }
+          side={side}
+        >
+          <SheetHeader className="sr-only">
+            <SheetTitle>Sidebar</SheetTitle>
+            <SheetDescription>Displays the mobile sidebar.</SheetDescription>
+          </SheetHeader>
+          <div className="flex h-full w-full flex-col">{children}</div>
+        </SheetContent>
+      </Sheet>
+    );
+  }
+
+  return (
+    <div
+      className="group peer hidden text-sidebar-foreground md:block"
+      data-state={state}
+      data-collapsible={state === 'collapsed' ? collapsible : ''}
+      data-variant={variant}
+      data-side={side}
+      data-slot="sidebar"
+    >
+      {/* This is what handles the sidebar gap on desktop */}
+      <div
+        data-slot="sidebar-gap"
+        className={cn(
+          'transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent',
+          'group-data-[collapsible=offcanvas]:w-0',
+          'group-data-[side=right]:rotate-180',
+          variant === 'floating' || variant === 'inset'
+            ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
+            : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
+        )}
+      />
+      <div
+        data-slot="sidebar-container"
+        data-side={side}
+        className={cn(
+          'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex',
+          // Adjust the padding for floating and inset variants.
+          variant === 'floating' || variant === 'inset'
+            ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
+            : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
+          className,
+        )}
+        {...props}
+      >
+        <div
+          data-sidebar="sidebar"
+          data-slot="sidebar-inner"
+          className="bg-sidebar group-data-[variant=floating]:ring-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 flex size-full flex-col"
+        >
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function SidebarTrigger({
+  className,
+  onClick,
+  ...props
+}: React.ComponentProps<typeof Button>) {
+  const { toggleSidebar } = useSidebar();
+
+  return (
+    <Button
+      data-sidebar="trigger"
+      data-slot="sidebar-trigger"
+      variant="ghost"
+      size="icon-sm"
+      className={cn(className)}
+      onClick={(event) => {
+        onClick?.(event);
+        toggleSidebar();
+      }}
+      {...props}
+    >
+      <PanelLeftIcon className="cn-rtl-flip" />
+      <span className="sr-only">Toggle Sidebar</span>
+    </Button>
+  );
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
+  const { toggleSidebar } = useSidebar();
+
+  return (
+    <button
+      data-sidebar="rail"
+      data-slot="sidebar-rail"
+      aria-label="Toggle Sidebar"
+      tabIndex={-1}
+      onClick={toggleSidebar}
+      title="Toggle Sidebar"
+      className={cn(
+        'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',
+        'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
+        '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
+        'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar',
+        '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
+        '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
+  return (
+    <main
+      data-slot="sidebar-inset"
+      className={cn(
+        'bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 relative flex w-full flex-1 flex-col',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarInput({
+  className,
+  ...props
+}: React.ComponentProps<typeof Input>) {
+  return (
+    <Input
+      data-slot="sidebar-input"
+      data-sidebar="input"
+      className={cn('bg-background h-8 w-full shadow-none', className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="sidebar-header"
+      data-sidebar="header"
+      className={cn('gap-2 p-2 flex flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="sidebar-footer"
+      data-sidebar="footer"
+      className={cn('gap-2 p-2 flex flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof Separator>) {
+  return (
+    <Separator
+      data-slot="sidebar-separator"
+      data-sidebar="separator"
+      className={cn('bg-sidebar-border mx-2 w-auto', className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="sidebar-content"
+      data-sidebar="content"
+      className={cn(
+        'no-scrollbar gap-0 flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="sidebar-group"
+      data-sidebar="group"
+      className={cn('p-2 relative flex w-full min-w-0 flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarGroupLabel({
+  className,
+  render,
+  ...props
+}: useRender.ComponentProps<'div'> & React.ComponentProps<'div'>) {
+  return useRender({
+    defaultTagName: 'div',
+    props: mergeProps<'div'>(
+      {
+        className: cn(
+          'text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs font-medium transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0',
+          className,
+        ),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'sidebar-group-label',
+      sidebar: 'group-label',
+    },
+  });
+}
+
+function SidebarGroupAction({
+  className,
+  render,
+  ...props
+}: useRender.ComponentProps<'button'> & React.ComponentProps<'button'>) {
+  return useRender({
+    defaultTagName: 'button',
+    props: mergeProps<'button'>(
+      {
+        className: cn(
+          'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0',
+          className,
+        ),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'sidebar-group-action',
+      sidebar: 'group-action',
+    },
+  });
+}
+
+function SidebarGroupContent({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="sidebar-group-content"
+      data-sidebar="group-content"
+      className={cn('text-sm w-full', className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
+  return (
+    <ul
+      data-slot="sidebar-menu"
+      data-sidebar="menu"
+      className={cn('gap-0 flex w-full min-w-0 flex-col', className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
+  return (
+    <li
+      data-slot="sidebar-menu-item"
+      data-sidebar="menu-item"
+      className={cn('group/menu-item relative', className)}
+      {...props}
+    />
+  );
+}
+
+const sidebarMenuButtonVariants = cva(
+  'ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button group/menu-button flex w-full items-center overflow-hidden outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate',
+  {
+    variants: {
+      variant: {
+        default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
+        outline:
+          'bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)]',
+      },
+      size: {
+        default: 'h-8 text-sm',
+        sm: 'h-7 text-xs',
+        lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  },
+);
+
+function SidebarMenuButton({
+  render,
+  isActive = false,
+  variant = 'default',
+  size = 'default',
+  tooltip,
+  className,
+  ...props
+}: useRender.ComponentProps<'button'> &
+  React.ComponentProps<'button'> & {
+    isActive?: boolean;
+    tooltip?: string | React.ComponentProps<typeof TooltipContent>;
+  } & VariantProps<typeof sidebarMenuButtonVariants>) {
+  const { isMobile, state } = useSidebar();
+  const comp = useRender({
+    defaultTagName: 'button',
+    props: mergeProps<'button'>(
+      {
+        className: cn(sidebarMenuButtonVariants({ variant, size }), className),
+      },
+      props,
+    ),
+    render: !tooltip ? render : <TooltipTrigger render={render} />,
+    state: {
+      slot: 'sidebar-menu-button',
+      sidebar: 'menu-button',
+      size,
+      active: isActive,
+    },
+  });
+
+  if (!tooltip) {
+    return comp;
+  }
+
+  if (typeof tooltip === 'string') {
+    tooltip = {
+      children: tooltip,
+    };
+  }
+
+  return (
+    <Tooltip>
+      {comp}
+      <TooltipContent
+        side="right"
+        align="center"
+        hidden={state !== 'collapsed' || isMobile}
+        {...tooltip}
+      />
+    </Tooltip>
+  );
+}
+
+function SidebarMenuAction({
+  className,
+  render,
+  showOnHover = false,
+  ...props
+}: useRender.ComponentProps<'button'> &
+  React.ComponentProps<'button'> & {
+    showOnHover?: boolean;
+  }) {
+  return useRender({
+    defaultTagName: 'button',
+    props: mergeProps<'button'>(
+      {
+        className: cn(
+          'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-md p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0',
+          showOnHover &&
+            'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0',
+          className,
+        ),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'sidebar-menu-action',
+      sidebar: 'menu-action',
+    },
+  });
+}
+
+function SidebarMenuBadge({
+  className,
+  ...props
+}: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="sidebar-menu-badge"
+      data-sidebar="menu-badge"
+      className={cn(
+        'text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 h-5 min-w-5 rounded-md px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuSkeleton({
+  className,
+  showIcon = false,
+  ...props
+}: React.ComponentProps<'div'> & {
+  showIcon?: boolean;
+}) {
+  // Random width between 50 to 90%.
+  const [width] = React.useState(() => {
+    return `${Math.floor(Math.random() * 40) + 50}%`;
+  });
+
+  return (
+    <div
+      data-slot="sidebar-menu-skeleton"
+      data-sidebar="menu-skeleton"
+      className={cn('h-8 gap-2 rounded-md px-2 flex items-center', className)}
+      {...props}
+    >
+      {showIcon && (
+        <Skeleton
+          className="size-4 rounded-md"
+          data-sidebar="menu-skeleton-icon"
+        />
+      )}
+      <Skeleton
+        className="h-4 max-w-(--skeleton-width) flex-1"
+        data-sidebar="menu-skeleton-text"
+        style={
+          {
+            '--skeleton-width': width,
+          } as React.CSSProperties
+        }
+      />
+    </div>
+  );
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
+  return (
+    <ul
+      data-slot="sidebar-menu-sub"
+      data-sidebar="menu-sub"
+      className={cn(
+        'border-sidebar-border mx-3.5 translate-x-px gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden flex min-w-0 flex-col',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuSubItem({
+  className,
+  ...props
+}: React.ComponentProps<'li'>) {
+  return (
+    <li
+      data-slot="sidebar-menu-sub-item"
+      data-sidebar="menu-sub-item"
+      className={cn('group/menu-sub-item relative', className)}
+      {...props}
+    />
+  );
+}
+
+function SidebarMenuSubButton({
+  render,
+  size = 'md',
+  isActive = false,
+  className,
+  ...props
+}: useRender.ComponentProps<'a'> &
+  React.ComponentProps<'a'> & {
+    size?: 'sm' | 'md';
+    isActive?: boolean;
+  }) {
+  return useRender({
+    defaultTagName: 'a',
+    props: mergeProps<'a'>(
+      {
+        className: cn(
+          'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0',
+          className,
+        ),
+      },
+      props,
+    ),
+    render,
+    state: {
+      slot: 'sidebar-menu-sub-button',
+      sidebar: 'menu-sub-button',
+      size,
+      active: isActive,
+    },
+  });
+}
+
+export {
+  Sidebar,
+  SidebarContent,
+  SidebarFooter,
+  SidebarGroup,
+  SidebarGroupAction,
+  SidebarGroupContent,
+  SidebarGroupLabel,
+  SidebarHeader,
+  SidebarInput,
+  SidebarInset,
+  SidebarMenu,
+  SidebarMenuAction,
+  SidebarMenuBadge,
+  SidebarMenuButton,
+  SidebarMenuItem,
+  SidebarMenuSkeleton,
+  SidebarMenuSub,
+  SidebarMenuSubButton,
+  SidebarMenuSubItem,
+  SidebarProvider,
+  SidebarRail,
+  SidebarSeparator,
+  SidebarTrigger,
+  useSidebar,
+};

+ 13 - 0
components/ui/skeleton.tsx

@@ -0,0 +1,13 @@
+import { cn } from '@/lib/utils';
+
+function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="skeleton"
+      className={cn('animate-pulse rounded-md bg-muted', className)}
+      {...props}
+    />
+  );
+}
+
+export { Skeleton };

+ 52 - 0
components/ui/slider.tsx

@@ -0,0 +1,52 @@
+import { Slider as SliderPrimitive } from '@base-ui/react/slider';
+
+import { cn } from '@/lib/utils';
+
+function Slider({
+  className,
+  defaultValue,
+  value,
+  min = 0,
+  max = 100,
+  ...props
+}: SliderPrimitive.Root.Props) {
+  const _values = Array.isArray(value)
+    ? value
+    : Array.isArray(defaultValue)
+      ? defaultValue
+      : [min, max];
+
+  return (
+    <SliderPrimitive.Root
+      className={cn('data-horizontal:w-full data-vertical:h-full', className)}
+      data-slot="slider"
+      defaultValue={defaultValue}
+      value={value}
+      min={min}
+      max={max}
+      thumbAlignment="edge"
+      {...props}
+    >
+      <SliderPrimitive.Control className="data-vertical:min-h-40 relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:w-auto data-vertical:flex-col">
+        <SliderPrimitive.Track
+          data-slot="slider-track"
+          className="bg-muted rounded-full data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1 relative grow overflow-hidden select-none"
+        >
+          <SliderPrimitive.Indicator
+            data-slot="slider-range"
+            className="bg-primary select-none data-horizontal:h-full data-vertical:w-full"
+          />
+        </SliderPrimitive.Track>
+        {Array.from({ length: _values.length }, (_, index) => (
+          <SliderPrimitive.Thumb
+            data-slot="slider-thumb"
+            key={index}
+            className="border-ring ring-ring/50 relative size-3 rounded-full border bg-white transition-[color,box-shadow] after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 block shrink-0 select-none disabled:pointer-events-none disabled:opacity-50"
+          />
+        ))}
+      </SliderPrimitive.Control>
+    </SliderPrimitive.Root>
+  );
+}
+
+export { Slider };

+ 16 - 0
components/ui/spinner.tsx

@@ -0,0 +1,16 @@
+import { cn } from '@/lib/utils';
+import { Loader2Icon } from 'lucide-react';
+
+function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <Loader2Icon
+      data-slot="spinner"
+      role="status"
+      aria-label="Loading"
+      className={cn('size-4 animate-spin', className)}
+      {...props}
+    />
+  );
+}
+
+export { Spinner };

+ 32 - 0
components/ui/switch.tsx

@@ -0,0 +1,32 @@
+'use client';
+
+import { Switch as SwitchPrimitive } from '@base-ui/react/switch';
+
+import { cn } from '@/lib/utils';
+
+function Switch({
+  className,
+  size = 'default',
+  ...props
+}: SwitchPrimitive.Root.Props & {
+  size?: 'sm' | 'default';
+}) {
+  return (
+    <SwitchPrimitive.Root
+      data-slot="switch"
+      data-size={size}
+      className={cn(
+        'peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50',
+        className,
+      )}
+      {...props}
+    >
+      <SwitchPrimitive.Thumb
+        data-slot="switch-thumb"
+        className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
+      />
+    </SwitchPrimitive.Root>
+  );
+}
+
+export { Switch };

+ 116 - 0
components/ui/table.tsx

@@ -0,0 +1,116 @@
+'use client';
+
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+function Table({ className, ...props }: React.ComponentProps<'table'>) {
+  return (
+    <div
+      data-slot="table-container"
+      className="relative w-full overflow-x-auto"
+    >
+      <table
+        data-slot="table"
+        className={cn('w-full caption-bottom text-sm', className)}
+        {...props}
+      />
+    </div>
+  );
+}
+
+function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
+  return (
+    <thead
+      data-slot="table-header"
+      className={cn('[&_tr]:border-b', className)}
+      {...props}
+    />
+  );
+}
+
+function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
+  return (
+    <tbody
+      data-slot="table-body"
+      className={cn('[&_tr:last-child]:border-0', className)}
+      {...props}
+    />
+  );
+}
+
+function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
+  return (
+    <tfoot
+      data-slot="table-footer"
+      className={cn(
+        'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
+  return (
+    <tr
+      data-slot="table-row"
+      className={cn(
+        'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors has-aria-expanded:bg-muted/50',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
+  return (
+    <th
+      data-slot="table-head"
+      className={cn(
+        'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
+  return (
+    <td
+      data-slot="table-cell"
+      className={cn(
+        'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function TableCaption({
+  className,
+  ...props
+}: React.ComponentProps<'caption'>) {
+  return (
+    <caption
+      data-slot="table-caption"
+      className={cn('text-muted-foreground mt-4 text-sm', className)}
+      {...props}
+    />
+  );
+}
+
+export {
+  Table,
+  TableHeader,
+  TableBody,
+  TableFooter,
+  TableHead,
+  TableRow,
+  TableCell,
+  TableCaption,
+};

+ 82 - 0
components/ui/tabs.tsx

@@ -0,0 +1,82 @@
+'use client';
+
+import { Tabs as TabsPrimitive } from '@base-ui/react/tabs';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+function Tabs({
+  className,
+  orientation = 'horizontal',
+  ...props
+}: TabsPrimitive.Root.Props) {
+  return (
+    <TabsPrimitive.Root
+      data-slot="tabs"
+      data-orientation={orientation}
+      className={cn(
+        'group/tabs flex gap-2 data-horizontal:flex-col',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+const tabsListVariants = cva(
+  'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none',
+  {
+    variants: {
+      variant: {
+        default: 'bg-muted',
+        line: 'gap-1 bg-transparent',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+    },
+  },
+);
+
+function TabsList({
+  className,
+  variant = 'default',
+  ...props
+}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
+  return (
+    <TabsPrimitive.List
+      data-slot="tabs-list"
+      data-variant={variant}
+      className={cn(tabsListVariants({ variant }), className)}
+      {...props}
+    />
+  );
+}
+
+function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
+  return (
+    <TabsPrimitive.Tab
+      data-slot="tabs-trigger"
+      className={cn(
+        "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
+        'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
+        'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
+  return (
+    <TabsPrimitive.Panel
+      data-slot="tabs-content"
+      className={cn('flex-1 text-sm outline-none', className)}
+      {...props}
+    />
+  );
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };

+ 18 - 0
components/ui/textarea.tsx

@@ -0,0 +1,18 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
+  return (
+    <textarea
+      data-slot="textarea"
+      className={cn(
+        'flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+export { Textarea };

+ 229 - 0
components/ui/toast.tsx

@@ -0,0 +1,229 @@
+'use client';
+
+import * as React from 'react';
+import { Toast as ToastPrimitive } from '@base-ui/react/toast';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import {
+  XIcon,
+  CircleCheckIcon,
+  InfoIcon,
+  TriangleAlertIcon,
+  OctagonXIcon,
+  Loader2Icon,
+} from 'lucide-react';
+
+const toast = ToastPrimitive.createToastManager();
+
+function ToastProvider({ ...props }: ToastPrimitive.Provider.Props) {
+  return <ToastPrimitive.Provider {...props} />;
+}
+
+function ToastPortal({ ...props }: ToastPrimitive.Portal.Props) {
+  return <ToastPrimitive.Portal data-slot="toast-portal" {...props} />;
+}
+
+function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
+  return (
+    <ToastPrimitive.Viewport
+      data-slot="toast-viewport"
+      className={cn(
+        'pointer-events-none fixed inset-x-4 bottom-4 z-50 mx-auto w-auto max-w-sm outline-none sm:right-4 sm:left-auto sm:mx-0 sm:w-full',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function Toast({ className, ...props }: ToastPrimitive.Root.Props) {
+  return (
+    <ToastPrimitive.Root
+      data-slot="toast"
+      className={cn(
+        'rounded-2xl group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
+        '[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]',
+        'h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]',
+        "after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
+        'data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]',
+        'data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]',
+        '[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]',
+        'data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
+        'data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
+        'data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
+        'data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
+        'data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
+        'data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
+        'data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
+        'data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ToastContent({ className, ...props }: ToastPrimitive.Content.Props) {
+  return (
+    <ToastPrimitive.Content
+      data-slot="toast-content"
+      className={cn(
+        'flex h-full items-center gap-3 overflow-hidden p-4 transition-opacity duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] data-behind:opacity-0 data-expanded:opacity-100',
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ToastTitle({ className, ...props }: ToastPrimitive.Title.Props) {
+  return (
+    <ToastPrimitive.Title
+      data-slot="toast-title"
+      className={cn('text-sm font-medium', className)}
+      {...props}
+    />
+  );
+}
+
+function ToastDescription({
+  className,
+  ...props
+}: ToastPrimitive.Description.Props) {
+  return (
+    <ToastPrimitive.Description
+      data-slot="toast-description"
+      className={cn('text-sm text-muted-foreground', className)}
+      {...props}
+    />
+  );
+}
+
+function ToastAction({
+  className,
+  render = <Button variant="outline" size="sm" />,
+  ...props
+}: ToastPrimitive.Action.Props) {
+  return (
+    <ToastPrimitive.Action
+      data-slot="toast-action"
+      render={render}
+      className={cn('shrink-0', className)}
+      {...props}
+    />
+  );
+}
+
+function ToastClose({
+  className,
+  children,
+  render = <Button variant="ghost" size="icon-sm" />,
+  ...props
+}: ToastPrimitive.Close.Props) {
+  return (
+    <ToastPrimitive.Close
+      data-slot="toast-close"
+      aria-label="Close toast"
+      render={render}
+      className={cn(
+        "relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground",
+        className,
+      )}
+      {...props}
+    >
+      {children ?? <XIcon aria-hidden="true" />}
+    </ToastPrimitive.Close>
+  );
+}
+
+function ToastIcon({ type }: { type: string | undefined }) {
+  let icon: React.ReactNode = null;
+
+  if (type === 'success') {
+    icon = <CircleCheckIcon aria-hidden="true" />;
+  }
+
+  if (type === 'info') {
+    icon = <InfoIcon aria-hidden="true" />;
+  }
+
+  if (type === 'warning') {
+    icon = <TriangleAlertIcon aria-hidden="true" />;
+  }
+
+  if (type === 'error') {
+    icon = <OctagonXIcon className="text-destructive" aria-hidden="true" />;
+  }
+
+  if (type === 'loading') {
+    icon = <Loader2Icon className="animate-spin" aria-hidden="true" />;
+  }
+
+  if (!icon) {
+    return null;
+  }
+
+  return (
+    <span
+      data-slot="toast-icon"
+      className="shrink-0 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
+    >
+      {icon}
+    </span>
+  );
+}
+
+function ToastList() {
+  const { toasts } = ToastPrimitive.useToastManager();
+
+  return toasts.map((toastItem) => (
+    <Toast key={toastItem.id} toast={toastItem}>
+      <ToastContent>
+        <ToastIcon type={toastItem.type} />
+        <div className="flex min-w-0 flex-1 flex-col gap-1">
+          <ToastTitle />
+          <ToastDescription />
+        </div>
+        <ToastAction />
+        <ToastClose />
+      </ToastContent>
+    </Toast>
+  ));
+}
+
+function Toaster({
+  children,
+  toastManager = toast,
+  ...props
+}: ToastPrimitive.Provider.Props) {
+  return (
+    <ToastProvider toastManager={toastManager} {...props}>
+      {children}
+      <ToastPortal>
+        <ToastViewport>
+          <ToastList />
+        </ToastViewport>
+      </ToastPortal>
+    </ToastProvider>
+  );
+}
+
+const createToastManager = ToastPrimitive.createToastManager;
+const useToastManager = ToastPrimitive.useToastManager;
+
+export {
+  Toaster,
+  Toast,
+  ToastAction,
+  ToastClose,
+  ToastContent,
+  ToastDescription,
+  ToastPortal,
+  ToastProvider,
+  ToastTitle,
+  ToastViewport,
+  createToastManager,
+  toast,
+  useToastManager,
+};

+ 89 - 0
components/ui/toggle-group.tsx

@@ -0,0 +1,89 @@
+'use client';
+
+import * as React from 'react';
+import { Toggle as TogglePrimitive } from '@base-ui/react/toggle';
+import { ToggleGroup as ToggleGroupPrimitive } from '@base-ui/react/toggle-group';
+import { type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { toggleVariants } from '@/components/ui/toggle';
+
+const ToggleGroupContext = React.createContext<
+  VariantProps<typeof toggleVariants> & {
+    spacing?: number;
+    orientation?: 'horizontal' | 'vertical';
+  }
+>({
+  size: 'default',
+  variant: 'default',
+  spacing: 2,
+  orientation: 'horizontal',
+});
+
+function ToggleGroup({
+  className,
+  variant,
+  size,
+  spacing = 2,
+  orientation = 'horizontal',
+  children,
+  ...props
+}: ToggleGroupPrimitive.Props &
+  VariantProps<typeof toggleVariants> & {
+    spacing?: number;
+    orientation?: 'horizontal' | 'vertical';
+  }) {
+  return (
+    <ToggleGroupPrimitive
+      data-slot="toggle-group"
+      data-variant={variant}
+      data-size={size}
+      data-spacing={spacing}
+      data-orientation={orientation}
+      style={{ '--gap': spacing } as React.CSSProperties}
+      className={cn(
+        'rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-vertical:flex-col data-vertical:items-stretch',
+        className,
+      )}
+      {...props}
+    >
+      <ToggleGroupContext.Provider
+        value={{ variant, size, spacing, orientation }}
+      >
+        {children}
+      </ToggleGroupContext.Provider>
+    </ToggleGroupPrimitive>
+  );
+}
+
+function ToggleGroupItem({
+  className,
+  children,
+  variant = 'default',
+  size = 'default',
+  ...props
+}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
+  const context = React.useContext(ToggleGroupContext);
+
+  return (
+    <TogglePrimitive
+      data-slot="toggle-group-item"
+      data-variant={context.variant || variant}
+      data-size={context.size || size}
+      data-spacing={context.spacing}
+      className={cn(
+        'group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg shrink-0 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t',
+        toggleVariants({
+          variant: context.variant || variant,
+          size: context.size || size,
+        }),
+        className,
+      )}
+      {...props}
+    >
+      {children}
+    </TogglePrimitive>
+  );
+}
+
+export { ToggleGroup, ToggleGroupItem };

+ 45 - 0
components/ui/toggle.tsx

@@ -0,0 +1,45 @@
+'use client';
+
+import { Toggle as TogglePrimitive } from '@base-ui/react/toggle';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const toggleVariants = cva(
+  "hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-lg text-sm font-medium transition-all [&_svg:not([class*='size-'])]:size-4 group/toggle inline-flex items-center justify-center whitespace-nowrap outline-none hover:bg-muted focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+  {
+    variants: {
+      variant: {
+        default: 'bg-transparent',
+        outline: 'border-input hover:bg-muted border bg-transparent',
+      },
+      size: {
+        default:
+          'h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
+        sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+        lg: 'h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  },
+);
+
+function Toggle({
+  className,
+  variant = 'default',
+  size = 'default',
+  ...props
+}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
+  return (
+    <TogglePrimitive
+      data-slot="toggle"
+      className={cn(toggleVariants({ variant, size, className }))}
+      {...props}
+    />
+  );
+}
+
+export { Toggle, toggleVariants };

+ 66 - 0
components/ui/tooltip.tsx

@@ -0,0 +1,66 @@
+'use client';
+
+import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip';
+
+import { cn } from '@/lib/utils';
+
+function TooltipProvider({
+  delay = 0,
+  ...props
+}: TooltipPrimitive.Provider.Props) {
+  return (
+    <TooltipPrimitive.Provider
+      data-slot="tooltip-provider"
+      delay={delay}
+      {...props}
+    />
+  );
+}
+
+function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
+  return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
+}
+
+function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
+  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
+}
+
+function TooltipContent({
+  className,
+  side = 'top',
+  sideOffset = 4,
+  align = 'center',
+  alignOffset = 0,
+  children,
+  ...props
+}: TooltipPrimitive.Popup.Props &
+  Pick<
+    TooltipPrimitive.Positioner.Props,
+    'align' | 'alignOffset' | 'side' | 'sideOffset'
+  >) {
+  return (
+    <TooltipPrimitive.Portal>
+      <TooltipPrimitive.Positioner
+        align={align}
+        alignOffset={alignOffset}
+        side={side}
+        sideOffset={sideOffset}
+        className="isolate z-50"
+      >
+        <TooltipPrimitive.Popup
+          data-slot="tooltip-content"
+          className={cn(
+            'z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
+            className,
+          )}
+          {...props}
+        >
+          {children}
+          <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
+        </TooltipPrimitive.Popup>
+      </TooltipPrimitive.Positioner>
+    </TooltipPrimitive.Portal>
+  );
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

+ 5 - 0
examples/.gitignore

@@ -0,0 +1,5 @@
+.venv/
+__pycache__/
+.pytest_cache/
+*.pyc
+.env

+ 198 - 0
examples/README.md

@@ -0,0 +1,198 @@
+# API Lab · FastAPI 笔记服务
+
+这份代码配合「API 服务开发」模块。用同一个小服务练习请求参数、REST CRUD、错误响应、依赖、异步等待、SSE 与 WebSocket。代码直接运行,不需要模型账户或模型密钥。
+
+这是各课核心功能的参考实现。课后扩展由学生按课件添加,例如第 06 课的 offset 分页、第 13 课的 POST /stream,以及 /private/status。`PromptIn`、`encode_sse()` 与 `token_events()` 已提供,第 13 课可直接复用。代码不预置全部练习答案。
+
+推荐 Python 3.11 或更新版本;本项目在 Windows、Python 3.12.6 下实际验证。依赖版本固定在 `requirements.txt`。数据库、Redis、任务队列、Docker 与服务器部署留到后续模块。
+
+## 1. 启动服务
+
+先解压并进入 `api-lab` 文件夹。后续命令都在这个文件夹执行。
+
+Windows PowerShell:
+
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+python -m pip install -r requirements.txt
+$env:DEMO_API_KEY = 'classroom-key'
+python -m uvicorn main:app --reload
+```
+
+如果 PowerShell 不允许执行激活脚本,不需要修改系统策略,改为直接调用虚拟环境中的 Python:
+
+```powershell
+.\.venv\Scripts\python.exe -m pip install -r requirements.txt
+$env:DEMO_API_KEY = 'classroom-key'
+.\.venv\Scripts\python.exe -m uvicorn main:app --reload
+```
+
+macOS / Linux:
+
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install -r requirements.txt
+export DEMO_API_KEY='classroom-key'
+python -m uvicorn main:app --reload
+```
+
+终端保留运行,打开:
+
+- `http://127.0.0.1:8000/health`:应返回 `{"status":"ok"}`。
+- `http://127.0.0.1:8000/docs`:展开端点,点击 **Try it out → Execute**,观察 Request URL、Response code 和 Response body。
+- `http://127.0.0.1:8000/openapi.json`:查看接口描述。
+
+`main:app` 表示导入 `main.py` 中的 `app` 对象。`--reload` 在代码改动后重启进程,因此笔记会清空。按 `Ctrl+C` 停止服务。
+
+## 2. 跑一遍 CRUD
+
+在 `/docs` 按顺序执行:
+
+1. `POST /notes` 提交 `{"title":"第一篇笔记"}`,得到 **201**,记录返回的 `id` 与响应头 `Location: /notes/{id}`。`content` 默认空字符串,`pinned` 默认 `false`。
+2. `GET /notes` 得到 JSON 数组,默认最多 10 条;填写 `q` 过滤标题或正文,填写 `pinned=true` 或 `pinned=false` 筛选置顶状态,填写 `limit` 限制返回条数。先筛选再限制条数,不传 `pinned` 时保留两种状态。
+3. `GET /notes/{note_id}` 用刚才的 ID 读取笔记。
+4. `PUT /notes/{note_id}` 提交 `{"title":"修改后的笔记","content":"完整替换","pinned":true}`。三个可修改字段都必须出现,少一个得到 **422**。
+5. `DELETE /notes/{note_id}` 得到 **204** 和空响应体。此时不要调用前端的 `response.json()`。
+6. 再读同一个 ID,得到 **404**;标题提交空字符串、纯空白或超过 120 个字符,得到 **422**。
+
+标题、正文会去掉首尾空白。`pinned` 是布尔类型,示例使用 JSON 的 `true` / `false`;Pydantic 默认还允许部分类型转换,不等同于严格模式。未声明的请求体字段会返回 422。
+
+## 3. 浏览器联调
+
+打开第二个终端,同样进入 `api-lab`,激活虚拟环境后执行:
+
+```bash
+python -m http.server 5500
+```
+
+打开 `http://localhost:5500/web-client.html`。不要双击 HTML 使用 `file://`。页面有三个可以直接操作的实验:
+
+- 创建笔记:`fetch()` 发送 POST JSON,输出状态码与响应体。
+- SSE:通过 `EventSource` 收到三个 `token` 事件和一个 `done` 事件;完整文本逐段显示。
+- WebSocket:连接后发送文本并收到 echo,发送文本 `ping` 收到 `pong`,然后断开并重连。
+
+打开浏览器开发者工具的 **Network** 面板。POST JSON 的跨域请求会触发 OPTIONS 预检;SSE 在连接内连续收到事件;WebSocket 在 Messages / Frames 中显示双向消息。不同浏览器的面板名称略有差异。
+
+服务仅允许 `http://localhost:5500` 与 `http://127.0.0.1:5500` 这两个浏览器来源。域名、协议或端口不同就是不同来源。CORS 是浏览器的跨域读取规则,不能替代身份验证;WebSocket 也不由这段 CORS 中间件提供鉴权。
+
+要复现第 10 课的来源不匹配,先临时把 `allow_origins` 收窄为只有 `http://127.0.0.1:5500`,再从 `http://localhost:5500` 打开前端;实验后恢复两个来源。
+
+## 4. SSE 的实际内容
+
+先启动 API。在另一终端执行以下命令,观察每个事件约隔 0.4 秒到达:
+
+Windows PowerShell:
+
+```powershell
+curl.exe -N 'http://127.0.0.1:8000/stream?prompt=hello'
+```
+
+macOS / Linux:
+
+```bash
+curl -N 'http://127.0.0.1:8000/stream?prompt=hello'
+```
+
+协议内容如下。每个事件末尾的空行负责划分事件边界。
+
+```text
+event: token
+data: {"text": "收到:"}
+
+event: token
+data: {"text": "hello"}
+
+event: token
+data: {"text": "。处理完成。"}
+
+event: done
+data: {}
+
+```
+
+`token` 和 `done` 是本例约定的事件名;每个 `token` 的内容是演示片段,不代表模型分词器中的 token。`data` 用 `json.dumps()` 编码,所以文本里的引号和换行不会破坏 SSE 帧。
+
+对照 `POST /generate`:它也模拟三个 0.4 秒处理步骤,但等约 1.2 秒后才一次返回完整 JSON。SSE 每完成一个步骤就发送一次。流正常完成或中途关闭时,`token_events()` 的 `finally` 会在服务端终端打印 `stream closed`。
+
+`EventSource` 默认使用 GET,不能直接提交 JSON 请求体或任意自定义请求头。本例使用查询参数;需要 POST 请求体时可按课程扩展为 `fetch()` 与 `ReadableStream`。页面在收到 `done`、发生错误、手动停止、再次开始或离开页面时关闭旧连接,避免默认重连再次生成内容。
+
+服务检查客户端是否断开。`Cache-Control: no-cache` 减少缓存干扰;`X-Accel-Buffering: no` 用于支持它的代理。真实线上流式到达仍取决于代理与客户端配置,不能只靠这个响应头保证。
+
+## 5. 依赖与异步等待
+
+`GET /private` 演示 `Depends()` 与 `X-API-Key` 请求头:
+
+- 服务进程未设置 `DEMO_API_KEY`:返回 **503**,其余端点仍可使用。
+- 设置后,漏传或传错 `X-API-Key`:返回 **401**。
+- 设置为 `classroom-key` 后传入同样的请求头:返回 **200**。
+
+`classroom-key` 只是课堂示例值。只有 `/private` 受这个演示依赖保护,`/notes`、`/stream` 和 `/ws` 没有身份验证。
+
+`GET /slow?delay=1` 使用 `await asyncio.sleep()` 模拟可等待的 I/O。合法等待时间是 0~5 秒。服务启动后运行:
+
+```bash
+python concurrency_client.py
+```
+
+脚本同时发出四个 1 秒请求,打印每个响应与总耗时。在本机空闲状态下,总耗时通常接近 1 秒加网络与调度开销。`async def` 不会自动把同步文件读写、同步网络调用或 CPU 密集计算变为不阻塞的工作。
+
+`GET /upstream-demo?delay=1` 通过共享的 HTTPX AsyncClient 调用本机 `/slow`,成功返回 `{"upstream":{"delay":1,"status":"done"}}`。客户端在 lifespan 中创建,并在应用关闭时释放。连接超时为 1 秒,其余默认超时为 3 秒;`delay=4` 会触发读取超时,接口返回 **504**。上游非成功状态或连接失败返回 **502**。
+
+这个出站实验固定调用 `http://127.0.0.1:8000`,不访问公网,也不接受用户输入任意 URL。请按启动步骤使用 8000 端口;若修改服务端口,要同时修改 lifespan 中的 `base_url`。
+
+## 6. 端点契约
+
+| 方法与路径 | 输入 | 成功响应 | 练习目标 |
+| --- | --- | --- | --- |
+| GET `/health` | 无 | 200 `{"status":"ok"}` | 路由与启动 |
+| GET `/notes` | 可选 `q`、`pinned`;`limit` 默认 10,范围 1~100 | 200 笔记数组 | 查询参数 |
+| POST `/notes` | `title`;可选 `content`、`pinned` | 201 笔记对象;`Location` 指向新资源 | 请求体与默认值 |
+| GET `/notes/{note_id}` | 正整数 ID | 200 笔记对象 | 路径参数、404 |
+| PUT `/notes/{note_id}` | `title`、`content`、`pinned` 都必填 | 200 笔记对象 | 完整替换 |
+| DELETE `/notes/{note_id}` | 正整数 ID | 204 空响应体 | 删除语义 |
+| GET `/private` | 请求头 `X-API-Key` | 200 `{"message":"API key accepted"}` | 依赖与环境变量 |
+| GET `/slow` | `delay` 默认 1,范围 0~5 | 200 `{"delay":1,"status":"done"}` | 异步等待 |
+| GET `/upstream-demo` | `delay` 默认 1,范围 0~5 | 200 `{"upstream":{"delay":1,"status":"done"}}` | HTTPX 生命周期、502/504 |
+| POST `/generate` | `{"prompt":"hello"}`,1~200 字符 | 200 `{"text":"收到:hello。处理完成。"}` | 一次性响应 |
+| GET `/stream` | `prompt` 默认 hello,1~200 字符 | 200 `text/event-stream` | 有限 SSE 流 |
+| WS `/ws` | 文本;文本 `ping` 是特殊约定 | echo 或 pong JSON 消息 | 同一连接上的双向通信 |
+
+HTTP 异常与请求校验错误统一为:
+
+```json
+{"error":{"code":"http_error","message":"Note not found","details":[]}}
+```
+
+校验失败时,`code` 为 `validation_error`,`details` 包含 `loc`、`msg`、`type`。这个教学处理器覆盖 HTTPException 与 RequestValidationError;未处理的程序异常、CORS 预检拒绝和 WebSocket 消息不属于该 JSON 契约。生产环境的日志、鉴权与错误追踪在后续模块补充。
+
+## 7. 验收
+
+```bash
+python -m pytest -q
+python -m pip check
+```
+
+测试覆盖 CRUD 全流程与 Location、PUT 必填字段、404/422、标题限制、q/pinned 组合过滤与默认 limit、环境变量与请求头、SSE 帧边界、中文与换行内容、WebSocket echo/ping/重连、CORS 预检和健康端点。HTTPX 上游成功、超时、HTTP 错误、连接失败使用 MockTransport 验证,不访问公网;同时检查 lifespan 关闭客户端。`TestClient` 的 SSE 测试检查协议格式与完整内容,不用于判断真实网络的首段到达时间。
+
+实际验证结果记录在 `VERIFICATION.md`。示例的测试会重置内存笔记,适合独立测试进程,不要将测试代码作为线上操作入口。
+
+## 8. 文件与使用边界
+
+- `main.py`:核心功能参考实现,保留在一个文件中便于逐节查找。课程可进一步练习拆分 models、routers 和 dependencies。
+- `web-client.html`:不需要构建工具的本地浏览器客户端。
+- `concurrency_client.py`:四个并发请求的实测脚本。
+- `test_main.py`:可执行验收。
+- `requirements.txt`:已验证的固定依赖。
+
+所有笔记保存在当前 Python 进程的内存中,重启清空、多个 worker 彼此不共享。请按示例运行单个开发进程。这里没有用户隔离、持久化、线上密钥管理、限流或任务队列;课程后续再接入数据库及部署能力。
+
+## 官方参考
+
+- FastAPI 请求体与模型:https://fastapi.tiangolo.com/tutorial/body/
+- FastAPI 错误处理:https://fastapi.tiangolo.com/tutorial/handling-errors/
+- FastAPI 自定义与流式响应:https://fastapi.tiangolo.com/advanced/custom-response/
+- FastAPI WebSocket:https://fastapi.tiangolo.com/advanced/websockets/
+- FastAPI CORS:https://fastapi.tiangolo.com/tutorial/cors/
+- FastAPI 测试:https://fastapi.tiangolo.com/tutorial/testing/

+ 51 - 0
examples/VERIFICATION.md

@@ -0,0 +1,51 @@
+# 验证记录
+
+验证日期:2026-09-10。系统:Windows。Python:3.12.6。使用独立虚拟环境安装 `requirements.txt`,未修改全局 Python 环境。
+
+## 自动化验收
+
+```text
+python -m pytest -q
+30 passed, 1 warning in 16.04s
+
+python -m pip check
+No broken requirements found.
+```
+
+唯一警告来自 Starlette TestClient 对 AnyIO `BlockingPortal` 旧别名的引用,是依赖的弃用提示;本次没有测试失败。代码没有屏蔽该提示。
+
+覆盖项:健康检查和 OpenAPI、CRUD 生命周期与 Location 响应头、q/pinned 组合筛选与默认 10 条限制、标题与查询参数验证、PUT 完整替换、统一 404/422 结构、请求头与环境变量、401 认证挑战响应头、SSE 帧及特殊字符、WebSocket echo/ping/重连、CORS 预检。
+
+HTTPX 使用 MockTransport 验证上游成功、读取超时映射 504、非成功 HTTP 状态映射 502、连接失败映射 502;另测 lifespan 创建并关闭共享客户端。这些上游测试不访问网络。
+
+## 真实网络验证
+
+另外启动真实 Uvicorn 子进程,通过 localhost TCP 连接调用服务。验证结束后已关闭该子进程。
+
+| 核验 | 实测结果 |
+| --- | --- |
+| HTTP 健康检查、文档、创建 / 读取 / 删除、Location、pinned 筛选 | 通过 |
+| `/private` 正确密钥与漏传密钥 | 200 / 401 |
+| `/generate` 完整结果到达 | 请求开始后 1.218 秒 |
+| 三个 SSE token 到达时间 | 请求开始后 0.411 / 0.813 / 1.199 秒 |
+| 完整文本与 done 事件 | `收到:hello。处理完成。`,通过 |
+| 客户端中途关流后再次健康检查 | 200 |
+| 真实 WebSocket echo、ping、关闭连接 | 通过 |
+| 同时请求四个 `/slow?delay=1` | 全部 200,总耗时 1.054 秒 |
+
+上面的耗时是这次本机实测值,不是性能保证。SSE 到达时间来自真实 HTTP 流,未使用 TestClient 测量。
+
+## 已验证的主要版本
+
+| 依赖 | 版本 |
+| --- | --- |
+| FastAPI | 0.141.1 |
+| Pydantic | 2.13.5 |
+| Starlette | 0.52.1 |
+| Uvicorn | 0.52.4 |
+| websockets | 15.0.1 |
+| HTTPX | 0.28.1 |
+| pytest | 9.1.1 |
+| AnyIO | 4.15.1 |
+
+所有直接与间接依赖的精确版本见 `requirements.txt`。

+ 20 - 0
examples/concurrency_client.py

@@ -0,0 +1,20 @@
+"""先启动 uvicorn,再运行 python concurrency_client.py。"""
+
+import asyncio
+from time import perf_counter
+
+import httpx
+
+
+async def main():
+    async with httpx.AsyncClient(base_url="http://127.0.0.1:8000", timeout=10) as client:
+        started = perf_counter()
+        responses = await asyncio.gather(*[client.get("/slow?delay=1") for _ in range(4)])
+        for index, response in enumerate(responses, start=1):
+            response.raise_for_status()
+            print(f"请求 {index}: {response.json()}")
+        print(f"总耗时: {perf_counter() - started:.2f} 秒")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 271 - 0
examples/main.py

@@ -0,0 +1,271 @@
+"""API 服务开发课程:同一个内存笔记服务,练习 HTTP、SSE 与 WebSocket。"""
+
+import asyncio
+import json
+import os
+import secrets
+from contextlib import asynccontextmanager
+from typing import Annotated
+
+import httpx
+from fastapi import Depends, FastAPI, Header, HTTPException, Path, Query, Request
+from fastapi import Response, WebSocket, WebSocketDisconnect
+from fastapi.exceptions import RequestValidationError
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse, StreamingResponse
+from pydantic import BaseModel, ConfigDict, Field
+from starlette.exceptions import HTTPException as StarletteHTTPException
+
+
+class ErrorDetail(BaseModel):
+    loc: list[str | int]
+    msg: str
+    type: str
+
+
+class ErrorContent(BaseModel):
+    code: str
+    message: str
+    details: list[ErrorDetail]
+
+
+class ErrorResponse(BaseModel):
+    error: ErrorContent
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    timeout = httpx.Timeout(3.0, connect=1.0)
+    async with httpx.AsyncClient(
+        base_url="http://127.0.0.1:8000", timeout=timeout,
+    ) as client:
+        app.state.http = client
+        yield
+
+
+app = FastAPI(
+    title="API Lab · 笔记服务",
+    version="1.0.0",
+    lifespan=lifespan,
+    responses={422: {"model": ErrorResponse, "description": "请求参数校验失败"}},
+)
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["http://localhost:5500", "http://127.0.0.1:5500"],
+    allow_credentials=False,
+    allow_methods=["GET", "POST", "PUT", "DELETE"],
+    allow_headers=["Content-Type", "X-API-Key"],
+)
+
+# 教学用内存存储:重启后丢失;单进程使用,不能用多个 worker 共享数据。
+app.state.notes = {}
+app.state.next_id = 1
+
+
+class NoteCreate(BaseModel):
+    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
+
+    title: str = Field(min_length=1, max_length=120)
+    content: str = ""
+    pinned: bool = False
+
+
+class NoteReplace(BaseModel):
+    """PUT 完整替换:三个可修改字段都必须提交。"""
+
+    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
+
+    title: str = Field(min_length=1, max_length=120)
+    content: str
+    pinned: bool
+
+
+class NoteOut(NoteCreate):
+    id: int
+
+
+class PromptIn(BaseModel):
+    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
+
+    prompt: str = Field(min_length=1, max_length=200)
+
+
+@app.exception_handler(StarletteHTTPException)
+async def http_error_handler(request: Request, exc: StarletteHTTPException):
+    return JSONResponse(
+        status_code=exc.status_code,
+        content={"error": {"code": "http_error", "message": str(exc.detail), "details": []}},
+        headers=exc.headers,
+    )
+
+
+@app.exception_handler(RequestValidationError)
+async def validation_error_handler(request: Request, exc: RequestValidationError):
+    # 只返回定位问题需要的字段,避免回传原始请求、内部异常或不可序列化的 ctx。
+    details = [
+        {"loc": list(error["loc"]), "msg": error["msg"], "type": error["type"]}
+        for error in exc.errors()
+    ]
+    return JSONResponse(
+        status_code=422,
+        content={"error": {"code": "validation_error", "message": "请求参数校验失败", "details": details}},
+    )
+
+
+def find_note(note_id: int) -> NoteOut:
+    note = app.state.notes.get(note_id)
+    if note is None:
+        raise HTTPException(status_code=404, detail="Note not found")
+    return note
+
+
+@app.get("/health", tags=["起步"])
+async def health():
+    return {"status": "ok"}
+
+
+@app.get("/notes", response_model=list[NoteOut], tags=["笔记 CRUD"])
+async def list_notes(
+    q: Annotated[str | None, Query(max_length=120)] = None,
+    limit: Annotated[int, Query(ge=1, le=100)] = 10,
+    pinned: bool | None = None,
+):
+    notes = list(app.state.notes.values())
+    if q:
+        query = q.casefold()
+        notes = [note for note in notes if query in note.title.casefold() or query in note.content.casefold()]
+    if pinned is not None:
+        notes = [note for note in notes if note.pinned == pinned]
+    return notes[:limit]
+
+
+@app.post("/notes", response_model=NoteOut, status_code=201, tags=["笔记 CRUD"])
+async def create_note(body: NoteCreate, response: Response):
+    note = NoteOut(id=app.state.next_id, **body.model_dump())
+    app.state.notes[note.id] = note
+    app.state.next_id += 1
+    response.headers["Location"] = f"/notes/{note.id}"
+    return note
+
+
+@app.get("/notes/{note_id}", response_model=NoteOut, responses={404: {"model": ErrorResponse}}, tags=["笔记 CRUD"])
+async def get_note(note_id: Annotated[int, Path(ge=1)]):
+    return find_note(note_id)
+
+
+@app.put("/notes/{note_id}", response_model=NoteOut, responses={404: {"model": ErrorResponse}}, tags=["笔记 CRUD"])
+async def replace_note(note_id: Annotated[int, Path(ge=1)], body: NoteReplace):
+    find_note(note_id)
+    note = NoteOut(id=note_id, **body.model_dump())
+    app.state.notes[note_id] = note
+    return note
+
+
+@app.delete("/notes/{note_id}", status_code=204, response_class=Response, responses={404: {"model": ErrorResponse}}, tags=["笔记 CRUD"])
+async def delete_note(note_id: Annotated[int, Path(ge=1)]):
+    find_note(note_id)
+    del app.state.notes[note_id]
+    return Response(status_code=204)
+
+
+async def require_api_key(x_api_key: Annotated[str | None, Header(alias="X-API-Key")] = None):
+    expected = os.getenv("DEMO_API_KEY")
+    if not expected:
+        raise HTTPException(status_code=503, detail="Set DEMO_API_KEY before using /private")
+    if x_api_key is None or not secrets.compare_digest(x_api_key.encode(), expected.encode()):
+        raise HTTPException(
+            status_code=401,
+            detail="Invalid or missing API key",
+            headers={"WWW-Authenticate": "APIKey"},
+        )
+
+
+@app.get(
+    "/private",
+    dependencies=[Depends(require_api_key)],
+    responses={401: {"model": ErrorResponse}, 503: {"model": ErrorResponse}},
+    tags=["依赖与请求头"],
+)
+async def private_example():
+    return {"message": "API key accepted"}
+
+
+@app.get("/slow", tags=["异步等待"])
+async def slow(delay: Annotated[float, Query(ge=0, le=5)] = 1):
+    await asyncio.sleep(delay)
+    return {"delay": delay, "status": "done"}
+
+
+@app.get(
+    "/upstream-demo",
+    responses={502: {"model": ErrorResponse}, 504: {"model": ErrorResponse}},
+    tags=["调用其他 HTTP 接口"],
+)
+async def upstream_demo(
+    request: Request,
+    delay: Annotated[float, Query(ge=0, le=5)] = 1,
+):
+    # 固定调用本机的 /slow,不接受用户提供的出站 URL。
+    try:
+        response = await request.app.state.http.get("/slow", params={"delay": delay})
+        response.raise_for_status()
+        return {"upstream": response.json()}
+    except httpx.TimeoutException as exc:
+        raise HTTPException(504, "上游响应超时") from exc
+    except httpx.HTTPStatusError as exc:
+        raise HTTPException(502, "上游返回非成功状态") from exc
+    except httpx.RequestError as exc:
+        raise HTTPException(502, "无法连接上游服务") from exc
+
+
+@app.post("/generate", tags=["普通响应与流式响应"])
+async def generate(payload: PromptIn):
+    # 确定性输出,无模型 SDK 或模型密钥;等待三步完成后一次返回。
+    parts = []
+    for text in ["收到:", payload.prompt, "。处理完成。"]:
+        await asyncio.sleep(0.4)
+        parts.append(text)
+    return {"text": "".join(parts)}
+
+
+def encode_sse(event: str, data: dict) -> str:
+    payload = json.dumps(data, ensure_ascii=False)
+    return f"event: {event}\ndata: {payload}\n\n"
+
+
+async def token_events(request: Request, prompt: str):
+    try:
+        for text in ["收到:", prompt, "。处理完成。"]:
+            await asyncio.sleep(0.4)
+            if await request.is_disconnected():
+                return
+            yield encode_sse("token", {"text": text})
+        yield encode_sse("done", {})
+    finally:
+        print("stream closed")
+
+
+@app.get("/stream", tags=["普通响应与流式响应"])
+async def stream(
+    request: Request,
+    prompt: Annotated[str, Query(min_length=1, max_length=200)] = "hello",
+):
+    return StreamingResponse(
+        token_events(request, prompt),
+        media_type="text/event-stream",
+        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
+    )
+
+
+@app.websocket("/ws")
+async def websocket_echo(websocket: WebSocket):
+    await websocket.accept()
+    try:
+        while True:
+            message = await websocket.receive_text()
+            if message == "ping":
+                await websocket.send_json({"type": "pong"})
+            else:
+                await websocket.send_json({"type": "echo", "text": message})
+    except WebSocketDisconnect:
+        return

+ 23 - 0
examples/requirements.txt

@@ -0,0 +1,23 @@
+annotated-doc==0.0.5
+annotated-types==0.8.0
+anyio==4.15.1
+certifi==2026.7.22
+click==8.5.0
+colorama==0.4.6
+fastapi==0.141.1
+h11==0.16.0
+httpcore==1.0.9
+httpx==0.28.1
+idna==3.19
+iniconfig==2.3.0
+packaging==26.3
+pluggy==1.6.0
+pydantic==2.13.5
+pydantic_core==2.46.5
+Pygments==2.21.0
+pytest==9.1.1
+starlette==0.52.1
+typing-inspection==0.4.4
+typing_extensions==4.16.0
+uvicorn==0.52.4
+websockets==15.0.1

+ 220 - 0
examples/test_main.py

@@ -0,0 +1,220 @@
+import json
+
+import httpx
+import pytest
+from fastapi.testclient import TestClient
+
+from main import app
+
+
+@pytest.fixture
+def client():
+    app.state.notes.clear()
+    app.state.next_id = 1
+    with TestClient(app) as test_client:
+        yield test_client
+
+
+def test_health_and_docs(client):
+    assert client.get("/health").json() == {"status": "ok"}
+    assert client.get("/docs").status_code == 200
+    paths = client.get("/openapi.json").json()["paths"]
+    assert "/notes" in paths
+    assert paths["/notes"]["post"]["responses"]["422"]["content"]["application/json"]["schema"] == {
+        "$ref": "#/components/schemas/ErrorResponse"
+    }
+
+
+def test_crud_lifecycle(client):
+    created = client.post("/notes", json={"title": "REST 笔记"})
+    assert created.status_code == 201
+    assert created.json() == {"id": 1, "title": "REST 笔记", "content": "", "pinned": False}
+    assert created.headers["location"] == "/notes/1"
+    assert client.get(created.headers["location"]).json() == created.json()
+    assert client.get("/notes/1").json() == created.json()
+    replacement = {"title": "修改后的标题", "content": "PUT 完整替换", "pinned": True}
+    updated = client.put("/notes/1", json=replacement)
+    assert updated.status_code == 200
+    assert updated.json() == {"id": 1, **replacement}
+    removed = client.delete("/notes/1")
+    assert removed.status_code == 204
+    assert removed.content == b""
+    assert client.get("/notes").json() == []
+    assert client.get("/notes/1").status_code == 404
+    assert client.post("/notes", json={"title": "新笔记"}).json()["id"] == 2
+
+
+def test_list_filter_and_limit(client):
+    client.post("/notes", json={"title": "FastAPI", "content": "request"})
+    client.post("/notes", json={"title": "第二篇", "content": "fastapi response"})
+    client.post("/notes", json={"title": "Docker"})
+    assert len(client.get("/notes", params={"q": "FASTAPI"}).json()) == 2
+    assert len(client.get("/notes", params={"q": "fastapi", "limit": 1}).json()) == 1
+    assert client.get("/notes", params={"q": "unknown"}).json() == []
+
+
+def test_default_limit_and_combined_pinned_filter(client):
+    for index in range(12):
+        client.post("/notes", json={
+            "title": f"note {index}",
+            "content": "match" if index in (1, 2, 3) else "",
+            "pinned": index % 2 == 0,
+        })
+    assert len(client.get("/notes").json()) == 10
+    pinned = client.get("/notes", params={"pinned": "true"}).json()
+    unpinned = client.get("/notes", params={"pinned": "false"}).json()
+    assert len(pinned) == len(unpinned) == 6
+    assert all(note["pinned"] is True for note in pinned)
+    assert all(note["pinned"] is False for note in unpinned)
+    combined = client.get("/notes", params={"q": "match", "pinned": "false", "limit": 1}).json()
+    assert [note["id"] for note in combined] == [2]
+
+
+@pytest.mark.parametrize("title", ["", "   ", "x" * 121])
+def test_invalid_title_reports_safe_validation_errors(client, title):
+    response = client.post("/notes", json={"title": title})
+    assert response.status_code == 422
+    error = response.json()["error"]
+    assert error["code"] == "validation_error"
+    assert error["details"][0]["loc"] == ["body", "title"]
+    assert set(error["details"][0]) == {"loc", "msg", "type"}
+    assert client.get("/notes").json() == []
+
+
+@pytest.mark.parametrize("missing", ["title", "content", "pinned"])
+def test_put_requires_every_mutable_field(client, missing):
+    client.post("/notes", json={"title": "保留的笔记"})
+    body = {"title": "替换", "content": "内容", "pinned": False}
+    del body[missing]
+    response = client.put("/notes/1", json=body)
+    assert response.status_code == 422
+    assert client.get("/notes/1").json()["title"] == "保留的笔记"
+
+
+@pytest.mark.parametrize("method,kwargs", [
+    ("get", {}),
+    ("put", {"json": {"title": "missing", "content": "", "pinned": False}}),
+    ("delete", {}),
+])
+def test_missing_note(client, method, kwargs):
+    response = getattr(client, method)("/notes/999", **kwargs)
+    assert response.status_code == 404
+    assert response.json() == {"error": {"code": "http_error", "message": "Note not found", "details": []}}
+
+
+@pytest.mark.parametrize("url", ["/notes?limit=0", "/notes?limit=101", "/notes?pinned=invalid", "/notes/no-number", "/slow?delay=-1", "/slow?delay=6"])
+def test_query_and_path_validation(client, url):
+    response = client.get(url)
+    assert response.status_code == 422
+    assert response.json()["error"]["code"] == "validation_error"
+
+
+def test_private_key_is_required_and_environment_is_read(monkeypatch, client):
+    monkeypatch.delenv("DEMO_API_KEY", raising=False)
+    assert client.get("/private", headers={"X-API-Key": "classroom-key"}).status_code == 503
+    monkeypatch.setenv("DEMO_API_KEY", "classroom-key")
+    missing = client.get("/private")
+    assert missing.status_code == 401
+    assert missing.headers["www-authenticate"] == "APIKey"
+    assert client.get("/private", headers={"X-API-Key": "wrong"}).status_code == 401
+    assert client.get("/private", headers={"X-API-Key": "classroom-key"}).status_code == 200
+    assert client.get("/health").status_code == 200
+
+
+def test_generate_and_sse_protocol(client):
+    prompt = '你好 "world"\n下一行'
+    response = client.post("/generate", json={"prompt": prompt})
+    assert response.status_code == 200
+    expected = response.json()["text"]
+    stream = client.get("/stream", params={"prompt": prompt})
+    assert stream.status_code == 200
+    assert stream.headers["content-type"].startswith("text/event-stream")
+    assert stream.text.endswith("\n\n")
+    frames = stream.text.rstrip("\n").split("\n\n")
+    chunks = []
+    for frame in frames[:-1]:
+        event, data = frame.split("\n")
+        assert event == "event: token"
+        assert data.startswith("data: ")
+        chunks.append(json.loads(data.removeprefix("data: "))["text"])
+    assert "".join(chunks) == expected
+    assert frames[-1] == "event: done\ndata: {}"
+
+
+def test_empty_generate_prompt_is_rejected(client):
+    assert client.post("/generate", json={"prompt": " "}).status_code == 422
+    assert client.get("/stream", params={"prompt": ""}).status_code == 422
+
+
+def test_websocket_echo_ping_and_reconnect(client):
+    with client.websocket_connect("/ws") as websocket:
+        websocket.send_text("你好")
+        assert websocket.receive_json() == {"type": "echo", "text": "你好"}
+        websocket.send_text("ping")
+        assert websocket.receive_json() == {"type": "pong"}
+    with client.websocket_connect("/ws") as websocket:
+        websocket.send_text("再次连接")
+        assert websocket.receive_json()["text"] == "再次连接"
+
+
+def test_cors_preflight_permits_local_client_only(client):
+    headers = {
+        "Origin": "http://localhost:5500",
+        "Access-Control-Request-Method": "POST",
+        "Access-Control-Request-Headers": "content-type",
+    }
+    accepted = client.options("/notes", headers=headers)
+    assert accepted.status_code == 200
+    assert accepted.headers["access-control-allow-origin"] == "http://localhost:5500"
+    headers["Origin"] = "https://example.org"
+    rejected = client.options("/notes", headers=headers)
+    assert rejected.status_code == 400
+    assert "access-control-allow-origin" not in rejected.headers
+
+
+def test_slow_completes(client):
+    assert client.get("/slow?delay=0").json() == {"delay": 0, "status": "done"}
+
+
+def test_lifespan_opens_and_closes_http_client():
+    with TestClient(app):
+        http_client = app.state.http
+        assert not http_client.is_closed
+    assert http_client.is_closed
+
+
+@pytest.mark.parametrize("scenario,status", [
+    ("success", 200), ("timeout", 504), ("http_failure", 502), ("connection_failure", 502),
+])
+def test_upstream_result_and_error_mapping(client, monkeypatch, scenario, status):
+    def handler(request):
+        assert request.url.path == "/slow"
+        assert request.url.params["delay"] == "1.0"
+        if scenario == "timeout":
+            raise httpx.ReadTimeout("simulated timeout", request=request)
+        if scenario == "connection_failure":
+            raise httpx.ConnectError("simulated connection failure", request=request)
+        if scenario == "http_failure":
+            return httpx.Response(503, json={"message": "unavailable"})
+        return httpx.Response(200, json={"delay": 1, "status": "done"})
+
+    async def mocked_get(path, *, params):
+        async with httpx.AsyncClient(
+            transport=httpx.MockTransport(handler), base_url="http://upstream.test",
+        ) as upstream:
+            return await upstream.get(path, params=params)
+
+    monkeypatch.setattr(app.state.http, "get", mocked_get)
+    response = client.get("/upstream-demo?delay=1")
+    assert response.status_code == status
+    if status == 200:
+        assert response.json() == {"upstream": {"delay": 1, "status": "done"}}
+    else:
+        error = response.json()["error"]
+        assert error["code"] == "http_error"
+        expected_messages = {
+            "timeout": "上游响应超时",
+            "http_failure": "上游返回非成功状态",
+            "connection_failure": "无法连接上游服务",
+        }
+        assert error["message"] == expected_messages[scenario]

+ 170 - 0
examples/web-client.html

@@ -0,0 +1,170 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>API Lab · 浏览器调用实验</title>
+  <style>
+    :root { font-family: system-ui, sans-serif; color: #182c39; background: #f0f3f5; }
+    body { max-width: 900px; margin: 40px auto; padding: 0 20px; }
+    h1 { font-size: 30px; } h2 { font-size: 20px; margin-top: 0; }
+    p { line-height: 1.7; } code { color: #23526c; }
+    section { background: white; padding: 24px; border: 1px solid #d5dee3; border-radius: 12px; margin: 20px 0; }
+    label { display: block; margin: 14px 0 8px; }
+    input { width: min(95%, 500px); padding: 11px; border: 1px solid #a8b8c1; border-radius: 6px; font: inherit; }
+    button { padding: 10px 16px; margin: 12px 8px 0 0; background: #194e6e; color: white; border: 0; border-radius: 6px; cursor: pointer; font: inherit; }
+    button:disabled { opacity: .45; cursor: default; }
+    button.secondary { background: #e5ecef; color: #182c39; }
+    pre { background: #edf2f5; padding: 16px; min-height: 48px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; }
+    .hint { font-size: 14px; color: #566b78; }
+    :focus-visible { outline: 3px solid #df9a23; outline-offset: 3px; }
+  </style>
+</head>
+<body>
+  <p class="hint">API 服务开发 / 配套实验</p>
+  <h1>让浏览器调用你的 FastAPI</h1>
+  <p>先启动 <code>http://127.0.0.1:8000</code> 的 API,再通过 <code>http://localhost:5500/web-client.html</code> 打开此页。在开发者工具的 Network 面板中观察请求。</p>
+  <section>
+    <h2>01 · POST /notes</h2>
+    <p>浏览器发送 JSON 请求体,服务创建笔记并返回 <code>201</code>。</p>
+    <label for="note-title">笔记标题</label>
+    <input id="note-title" value="我的第一条 API 笔记" maxlength="120">
+    <div><button id="create-note" type="button">创建笔记</button></div>
+    <pre id="note-output" aria-live="polite">等待请求</pre>
+  </section>
+  <section>
+    <h2>02 · GET /stream · SSE</h2>
+    <p>服务连续发送 <code>token</code> 事件,最后发送 <code>done</code>。客户端收到结束事件后主动关闭连接。</p>
+    <label for="prompt">输入文本</label>
+    <input id="prompt" value="分块返回这段文本,观察连接何时结束。" maxlength="200">
+    <div><button id="start-stream" type="button">开始接收</button><button id="stop-stream" class="secondary" type="button" disabled>停止</button></div>
+    <p id="stream-status" class="hint" role="status">未连接</p>
+    <pre id="stream-output" aria-live="polite">收到的文本会显示在这里</pre>
+  </section>
+  <section>
+    <h2>03 · /ws · WebSocket</h2>
+    <p>连接保持期间,客户端可以多次发送消息。这里的文本 <code>ping</code> 是应用约定,不是协议控制帧。</p>
+    <div><button id="connect-ws" type="button">连接</button><button id="close-ws" class="secondary" type="button" disabled>断开</button></div>
+    <label for="ws-message">消息</label>
+    <input id="ws-message" value="你好,FastAPI">
+    <div><button id="send-ws" type="button" disabled>发送消息</button><button id="ping-ws" class="secondary" type="button" disabled>发送 ping</button></div>
+    <p id="ws-status" class="hint" role="status">未连接</p>
+    <pre id="ws-output" aria-live="polite">服务端消息会显示在这里</pre>
+  </section>
+  <p class="hint">本页只在本机练习。CORS 允许来源为 localhost:5500 和 127.0.0.1:5500;请勿双击 HTML 使用 file:// 打开。</p>
+  <script>
+    const API = 'http://127.0.0.1:8000';
+    const $ = (id) => document.getElementById(id);
+    let stream = null;
+    let socket = null;
+
+    $('create-note').onclick = async () => {
+      $('create-note').disabled = true;
+      $('note-output').textContent = '发送中…';
+      try {
+        const response = await fetch(`${API}/notes`, {
+          method: 'POST',
+          headers: {'Content-Type': 'application/json'},
+          body: JSON.stringify({title: $('note-title').value, content: '', pinned: false}),
+        });
+        const data = await response.json();
+        $('note-output').textContent = `HTTP ${response.status}\n${JSON.stringify(data, null, 2)}`;
+      } catch (error) {
+        $('note-output').textContent = `请求失败:${error.message}\n检查服务是否启动、页面是否来自 localhost:5500。`;
+      } finally {
+        $('create-note').disabled = false;
+      }
+    };
+
+    function stopStream(message = '已停止') {
+      if (stream) stream.close();
+      stream = null;
+      $('stream-status').textContent = message;
+      $('stop-stream').disabled = true;
+    }
+
+    $('start-stream').onclick = () => {
+      stopStream(); // 重复开始时先关闭上一条连接。
+      const prompt = $('prompt').value.trim();
+      if (!prompt) {
+        $('stream-status').textContent = '请先输入文本';
+        return;
+      }
+      $('stream-output').textContent = '';
+      $('stream-status').textContent = '连接中…';
+      $('stop-stream').disabled = false;
+      const current = new EventSource(`${API}/stream?${new URLSearchParams({prompt})}`);
+      stream = current;
+      current.onopen = () => {
+        if (stream === current) $('stream-status').textContent = '正在接收';
+      };
+      current.addEventListener('token', (event) => {
+        if (stream !== current) return;
+        try {
+          $('stream-output').textContent += JSON.parse(event.data).text;
+        } catch {
+          stopStream('数据格式错误,连接已关闭');
+        }
+      });
+      current.addEventListener('done', () => {
+        if (stream === current) stopStream('已完成,连接已关闭');
+      });
+      current.onerror = () => {
+        if (stream === current) stopStream('连接失败或中断,请检查服务后重新开始');
+      };
+    };
+    $('stop-stream').onclick = () => stopStream();
+
+    function setSocketControls(connected) {
+      $('send-ws').disabled = !connected;
+      $('ping-ws').disabled = !connected;
+      $('close-ws').disabled = !socket;
+      $('connect-ws').disabled = Boolean(socket);
+    }
+
+    function closeSocket() {
+      const previous = socket;
+      socket = null;
+      if (previous) previous.close(1000, 'Client closed');
+      $('ws-status').textContent = '已断开';
+      setSocketControls(false);
+    }
+
+    $('connect-ws').onclick = () => {
+      closeSocket();
+      const current = new WebSocket(`${API.replace('http:', 'ws:')}/ws`);
+      socket = current;
+      $('ws-output').textContent = '';
+      $('ws-status').textContent = '连接中…';
+      setSocketControls(false);
+      current.onopen = () => {
+        if (socket !== current) return;
+        $('ws-status').textContent = '已连接';
+        setSocketControls(true);
+      };
+      current.onmessage = (event) => {
+        if (socket === current) $('ws-output').textContent += `${event.data}\n`;
+      };
+      current.onerror = () => {
+        if (socket === current) $('ws-status').textContent = '连接错误,检查服务是否启动';
+      };
+      current.onclose = (event) => {
+        if (socket !== current) return;
+        socket = null;
+        $('ws-status').textContent = `连接已关闭(code ${event.code})`;
+        setSocketControls(false);
+      };
+    };
+    function sendMessage(text) {
+      if (socket?.readyState === WebSocket.OPEN) socket.send(text);
+    }
+    $('send-ws').onclick = () => sendMessage($('ws-message').value);
+    $('ping-ws').onclick = () => sendMessage('ping');
+    $('close-ws').onclick = closeSocket;
+    window.addEventListener('beforeunload', () => {
+      stopStream();
+      closeSocket();
+    });
+  </script>
+</body>
+</html>

+ 21 - 0
hooks/use-mobile.ts

@@ -0,0 +1,21 @@
+import * as React from 'react';
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
+    undefined,
+  );
+
+  React.useEffect(() => {
+    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+    const onChange = () => {
+      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+    };
+    mql.addEventListener('change', onChange);
+    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+    return () => mql.removeEventListener('change', onChange);
+  }, []);
+
+  return !!isMobile;
+}

+ 6 - 0
lib/utils.ts

@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs));
+}

+ 5 - 0
next.config.ts

@@ -0,0 +1,5 @@
+import type { NextConfig } from 'next';
+
+const nextConfig: NextConfig = {};
+
+export default nextConfig;

+ 9881 - 0
package-lock.json

@@ -0,0 +1,9881 @@
+{
+  "name": "sites-project",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "sites-project",
+      "version": "0.1.0",
+      "dependencies": {
+        "@base-ui/react": "1.7.0",
+        "@shadcn/react": "0.3.0",
+        "class-variance-authority": "0.7.1",
+        "clsx": "2.1.1",
+        "cmdk": "1.1.1",
+        "date-fns": "4.1.0",
+        "embla-carousel-react": "8.5.2",
+        "input-otp": "1.4.2",
+        "lucide-react": "1.31.0",
+        "react": "19.2.6",
+        "react-day-picker": "9.8.1",
+        "react-dom": "19.2.6",
+        "react-resizable-panels": "4.5.8",
+        "react-server-dom-webpack": "19.2.6",
+        "recharts": "3.8.0",
+        "shadcn": "4.18.0",
+        "tailwind-merge": "3.6.0",
+        "tw-animate-css": "1.4.0",
+        "vinext": "1.0.0-beta.5"
+      },
+      "devDependencies": {
+        "@cloudflare/vite-plugin": "1.37.1",
+        "@cloudflare/workers-types": "4.20260515.1",
+        "@openai/sites-vite-plugin": "0.2.0",
+        "@tailwindcss/postcss": "4.2.1",
+        "@types/node": "22.19.19",
+        "@types/react": "19.2.14",
+        "@types/react-dom": "19.2.3",
+        "@vitejs/plugin-react": "6.0.2",
+        "@vitejs/plugin-rsc": "0.5.26",
+        "oxfmt": "0.61.0",
+        "oxlint": "1.76.0",
+        "oxlint-tsgolint": "7.0.2001",
+        "tailwindcss": "4.2.1",
+        "typescript": "5.9.3",
+        "vite": "8.0.13",
+        "wrangler": "4.92.0"
+      },
+      "engines": {
+        "node": ">=22.13.0"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz",
+      "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+      "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+      "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.7",
+        "@babel/helper-compilation-targets": "^7.29.7",
+        "@babel/helper-module-transforms": "^7.29.7",
+        "@babel/helpers": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/template": "^7.29.7",
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+      "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@babel/types": "^7.29.8",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz",
+      "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+      "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.29.7",
+        "@babel/helper-validator-option": "^7.29.7",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-create-class-features-plugin": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz",
+      "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.29.7",
+        "@babel/helper-member-expression-to-functions": "^7.29.7",
+        "@babel/helper-optimise-call-expression": "^7.29.7",
+        "@babel/helper-replace-supers": "^7.29.7",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+        "@babel/traverse": "^7.29.7",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+      "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-member-expression-to-functions": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz",
+      "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+      "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+      "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "@babel/traverse": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-optimise-call-expression": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz",
+      "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+      "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-replace-supers": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
+      "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-member-expression-to-functions": "^7.29.7",
+        "@babel/helper-optimise-call-expression": "^7.29.7",
+        "@babel/traverse": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz",
+      "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+      "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+      "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+      "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.8"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-jsx": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
+      "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-typescript": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
+      "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-modules-commonjs": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz",
+      "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-transforms": "^7.29.7",
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-typescript": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
+      "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.29.7",
+        "@babel/helper-create-class-features-plugin": "^7.29.7",
+        "@babel/helper-plugin-utils": "^7.29.7",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+        "@babel/plugin-syntax-typescript": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/preset-typescript": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz",
+      "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7",
+        "@babel/helper-validator-option": "^7.29.7",
+        "@babel/plugin-syntax-jsx": "^7.29.7",
+        "@babel/plugin-transform-modules-commonjs": "^7.29.7",
+        "@babel/plugin-transform-typescript": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+      "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+      "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+      "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+      "license": "MIT",
+      "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.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+      "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@base-ui/react": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.7.0.tgz",
+      "integrity": "sha512-j+8QjX44C32jrXD/qyEAGpFr70FRpGL2CY61mQd9nBPWN737CK0xxD1ceJ055rW4RtdvFDT1e7otzdlfxvsYug==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.29.2",
+        "@base-ui/utils": "0.3.2",
+        "@floating-ui/react-dom": "^2.1.9",
+        "@floating-ui/utils": "^0.2.12",
+        "use-sync-external-store": "^1.6.0"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@date-fns/tz": "^1.2.0",
+        "@types/react": "^17 || ^18 || ^19",
+        "date-fns": "^4.0.0",
+        "react": "^17 || ^18 || ^19",
+        "react-dom": "^17 || ^18 || ^19"
+      },
+      "peerDependenciesMeta": {
+        "@date-fns/tz": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        },
+        "date-fns": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@base-ui/utils": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz",
+      "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.29.2",
+        "@floating-ui/utils": "^0.2.12",
+        "reselect": "^5.2.0",
+        "use-sync-external-store": "^1.6.0"
+      },
+      "peerDependencies": {
+        "@types/react": "^17 || ^18 || ^19",
+        "react": "^17 || ^18 || ^19",
+        "react-dom": "^17 || ^18 || ^19"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@cloudflare/kv-asset-handler": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
+      "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "engines": {
+        "node": ">=22.0.0"
+      }
+    },
+    "node_modules/@cloudflare/unenv-preset": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",
+      "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "peerDependencies": {
+        "unenv": "2.0.0-rc.24",
+        "workerd": ">1.20260305.0 <2.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "workerd": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@cloudflare/vite-plugin": {
+      "version": "1.37.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.37.1.tgz",
+      "integrity": "sha512-FldhsmkXyLsX3yI+p0aJDo/kGyY69pDdynz2JjcuH3eCiQ+G9XaY3CrN+UYEfCbWv8CiR2wL95lNg0fT/HfwWA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@cloudflare/unenv-preset": "2.16.1",
+        "miniflare": "4.20260515.0",
+        "unenv": "2.0.0-rc.24",
+        "wrangler": "4.92.0",
+        "ws": "8.18.0"
+      },
+      "peerDependencies": {
+        "vite": "^6.1.0 || ^7.0.0 || ^8.0.0",
+        "wrangler": "^4.92.0"
+      }
+    },
+    "node_modules/@cloudflare/workerd-darwin-64": {
+      "version": "1.20260515.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260515.1.tgz",
+      "integrity": "sha512-Wtw44el2pNbzixvTkWdfeBDTrQwQbJRz7/JUvPKV27I0pQWXbhNJPpM8cstq/pbrU5AGcA/HjFH6yPMRTIRKig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workerd-darwin-arm64": {
+      "version": "1.20260515.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260515.1.tgz",
+      "integrity": "sha512-X8EqkZej6FfmhF9AVAQ3FhyQRr9acS4RcDunMU2YiuxKHF1IU8zzH3vY30/POaG+rUu9vGDp/VgUl49VPenHJQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workerd-linux-64": {
+      "version": "1.20260515.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260515.1.tgz",
+      "integrity": "sha512-CDC89QxQ7Y7t7RG1Jd9vj/qolE1sQRkI2OSEuV5BMJi0vW/gV4OVG6xjpdK3b1OYnSWDzF7NpvlR5Yg86q7k4g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workerd-linux-arm64": {
+      "version": "1.20260515.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260515.1.tgz",
+      "integrity": "sha512-WxbW/PToYES4fvHXzsr/5qOiETQs/Z9iZ0mjSZAiEwq5cMLZemzGN0COx+uFb9OvQwzh6Pg159qPFnw3+i9FuA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workerd-windows-64": {
+      "version": "1.20260515.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260515.1.tgz",
+      "integrity": "sha512-WmV/iv+MHjYsvkcMVzpM2B5/mf06UUkdpVhZrtMfV9graWjBGPYFvE/eab8748RPVGKh1Xe1vXofLzDSwc08lA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@cloudflare/workers-types": {
+      "version": "4.20260515.1",
+      "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260515.1.tgz",
+      "integrity": "sha512-ruz+9tcG2iIKq5mXU2F00lL6KF5d4QfbAk7fNgvvqf5BelxDXaieyZmSnvA4sQBuemUstt4AEDN9CmJKmfCsDw==",
+      "dev": true,
+      "license": "MIT OR Apache-2.0"
+    },
+    "node_modules/@cspotcode/source-map-support": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "0.3.9"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.0.3",
+        "@jridgewell/sourcemap-codec": "^1.4.10"
+      }
+    },
+    "node_modules/@date-fns/tz": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz",
+      "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==",
+      "license": "MIT"
+    },
+    "node_modules/@dotenvx/dotenvx": {
+      "version": "1.75.1",
+      "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz",
+      "integrity": "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@dotenvx/primitives": "^0.8.0",
+        "commander": "^11.1.0",
+        "conf": "^10.2.0",
+        "dotenv": "^17.2.1",
+        "enquirer": "^2.4.1",
+        "env-paths": "^2.2.1",
+        "execa": "^5.1.1",
+        "fdir": "^6.2.0",
+        "ignore": "^5.3.0",
+        "object-treeify": "1.1.33",
+        "open": "^8.4.2",
+        "picomatch": "^4.0.4",
+        "systeminformation": "^5.22.11",
+        "undici": "^7.11.0",
+        "which": "^4.0.0",
+        "yocto-spinner": "^1.1.0"
+      },
+      "bin": {
+        "dotenvx": "src/cli/dotenvx.js"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/commander": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+      "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/define-lazy-prop": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+      "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/execa": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+      "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+      "license": "MIT",
+      "dependencies": {
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^6.0.0",
+        "human-signals": "^2.1.0",
+        "is-stream": "^2.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^4.0.1",
+        "onetime": "^5.1.2",
+        "signal-exit": "^3.0.3",
+        "strip-final-newline": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/human-signals": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.17.0"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/is-docker": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+      "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/is-wsl": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+      "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/open": {
+      "version": "8.4.2",
+      "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+      "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+      "license": "MIT",
+      "dependencies": {
+        "define-lazy-prop": "^2.0.0",
+        "is-docker": "^2.1.1",
+        "is-wsl": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "license": "ISC"
+    },
+    "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@dotenvx/primitives": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@dotenvx/primitives/-/primitives-0.8.0.tgz",
+      "integrity": "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@emnapi/core": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+      "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.2.1",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.11.3",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+      "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+      "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+      "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+      "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+      "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+      "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+      "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+      "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+      "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+      "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+      "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+      "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+      "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+      "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+      "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+      "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+      "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+      "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+      "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+      "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+      "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+      "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+      "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+      "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+      "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
+      "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.12"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
+      "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.8.0",
+        "@floating-ui/utils": "^0.2.12"
+      }
+    },
+    "node_modules/@floating-ui/react-dom": {
+      "version": "2.1.9",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz",
+      "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/dom": "^1.8.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0",
+        "react-dom": ">=16.8.0"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.12",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
+      "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
+      "license": "MIT"
+    },
+    "node_modules/@hono/node-server": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz",
+      "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      },
+      "peerDependencies": {
+        "hono": "^4"
+      }
+    },
+    "node_modules/@img/colour": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+      "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+      "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+      "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+      "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+      "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+      "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+      "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+      "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-riscv64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+      "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+      "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+      "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+      "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+      "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+      "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+      "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-ppc64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+      "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-ppc64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-riscv64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+      "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-riscv64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+      "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+      "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+      "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+      "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+      "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.7.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+      "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+      "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+      "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/source-map": {
+      "version": "0.3.11",
+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+      "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+      "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk": {
+      "version": "1.30.0",
+      "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
+      "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
+      "license": "MIT",
+      "dependencies": {
+        "@hono/node-server": "^1.19.9 || ^2.0.5",
+        "ajv": "^8.17.1",
+        "ajv-formats": "^3.0.1",
+        "content-type": "^1.0.5",
+        "cors": "^2.8.5",
+        "cross-spawn": "^7.0.5",
+        "eventsource": "^3.0.2",
+        "eventsource-parser": "^3.0.0",
+        "express": "^5.2.1",
+        "express-rate-limit": "^8.2.1",
+        "hono": "^4.11.4",
+        "jose": "^6.1.3",
+        "json-schema-typed": "^8.0.2",
+        "pkce-challenge": "^5.0.0",
+        "raw-body": "^3.0.0",
+        "zod": "^3.25 || ^4.0",
+        "zod-to-json-schema": "^3.25.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@cfworker/json-schema": "^4.1.1",
+        "zod": "^3.25 || ^4.0"
+      },
+      "peerDependenciesMeta": {
+        "@cfworker/json-schema": {
+          "optional": true
+        },
+        "zod": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
+      "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@tybys/wasm-util": "^0.10.3"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "peerDependencies": {
+        "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
+        "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@openai/sites-vite-plugin": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/@openai/sites-vite-plugin/-/sites-vite-plugin-0.2.0.tgz",
+      "integrity": "sha512-4ThLMW9eaf2gd+SqBwHmqBONKBAbQ2BuOFev7mHq7Y2W5mm/rfVJOSjcgQIsY2PqdXxXNZ4Yd0+s0DlSUYehag==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=22.13.0"
+      },
+      "peerDependencies": {
+        "vite": "^8.0.0"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.130.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
+      "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@oxfmt/binding-android-arm-eabi": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.61.0.tgz",
+      "integrity": "sha512-BaS+1OVvg9sr+Xav0+KdWedQRcAzrdoEcwMZeqoc2F6ieC1s/t5eM35YQoRPQ7vAqkZ+p3tbQb1r9I9mrV5oGA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-android-arm64": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.61.0.tgz",
+      "integrity": "sha512-of8atAV0M1egGcVOMbgZCvc10sFOP3ayQBNQV5h5G3fNq8gACdEswfFk9bzGrdbM23rtg0Coxi7np7oPLcueNw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-darwin-arm64": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.61.0.tgz",
+      "integrity": "sha512-7l8+5ov4BGwtAcmpzvEik/TG3bciwyw/S3e6j5GKH7pcQqcgCVxD3AuJeP6upto+SOTBKQ4wrrdbMt0gq8fHSQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-darwin-x64": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.61.0.tgz",
+      "integrity": "sha512-Fnz4dDDXBb7udk+DmwelNjxbD6yptyxwCqwCH2ebo4RVLxVsRfFsn/AHJC49KIltPrVokamGv4SSOsiV50DTxQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-freebsd-x64": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.61.0.tgz",
+      "integrity": "sha512-mddOebKNCP+AucmzfNsk3jgbr681qAUvgMqi865GW5gWLJ/AnzXbvjQRrny0e++NAN8aphav/aRSrfFxNsNjpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.61.0.tgz",
+      "integrity": "sha512-svx59iYL+DbaZGZUIoice4W0CjRXGExnbz7Re+awIb60rVxBS2KrU7Hnlx+nZYanLGLpjneUEgo/VFEKkSZAyQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-arm-musleabihf": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.61.0.tgz",
+      "integrity": "sha512-BYK9MPJPCf6d+fLKMTruThmEyCtHzQ1zLcsrTlUVkmnoXIaHAbfpeLYQwX1tkjs7W11dyzoi6HFvKcdnvX1zNg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-arm64-gnu": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.61.0.tgz",
+      "integrity": "sha512-QUaCNLq2/EC6G5ljOuFanl9Lgw6ZWp4co7rs4+KOMUzbGfA4Lq58FHRjjF9sVIG+93XSbo343MxFATrOU1qctA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-arm64-musl": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.61.0.tgz",
+      "integrity": "sha512-S6uvJ6MXnRXl+zTs0CARNDvkE+cymj0EVWEKKsyKnlLlqTyQJBjw5s4D2pSIOZc+S46cy4STefzcr/sm0VzVPA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-ppc64-gnu": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.61.0.tgz",
+      "integrity": "sha512-6VDlRcytvZG6UlSIdAFKDLbppo9tvPxrWzle6vHldYFMeuDPQEfMKrkwezp7FaBq1wik9ra554ZZeRPsyIkFpg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-riscv64-gnu": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.61.0.tgz",
+      "integrity": "sha512-KkBTYbzExpbmn15XjKPLu2fRV2PVlq+KWt+brad5rwIa03vdYoaDRWiS7raHII/dCTR6Ro4UpYUCH4t6lif4WQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-riscv64-musl": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.61.0.tgz",
+      "integrity": "sha512-69tzIq7sJLVB9dxYYtvMzcSSsnZHSO+U2U19O2RqDqgj6+Q4O7HjSXdaszbcgqzhsUwzSH7z5kWvk8nmf6BHTg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-s390x-gnu": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.61.0.tgz",
+      "integrity": "sha512-Oqi/N0OvtOVXsPKAOOhKgGH3msRYF8BLJaNBbWiupRiKoKVyc8JRKPCfarkQJC+RgP9U8raUKLe+bNwd0HUMiA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-x64-gnu": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.61.0.tgz",
+      "integrity": "sha512-3TKwv/ed4uwJSemAA8P9XcoqETpjQI4waquF9UilhA9Mn/dhr1PdUEXWlL74mtc6ZNfmKPA9+NEJm01nRF8CVA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-linux-x64-musl": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.61.0.tgz",
+      "integrity": "sha512-uFso4u4nLkVSlMCpgjyvWV60Gt7GvDQHnk1mmRxHIkZTMB0ljpUKwCD9FYGgN9H97x2wYl0UwEjgRZaPIuhEhw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-openharmony-arm64": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.61.0.tgz",
+      "integrity": "sha512-keGLkzeOvkMpNmPp4hffXWpfoSsY6e1K8++KXD4mSSfxdvM8q9QUDsYY689TB1k6Co832DZn1MnaaVx6cIBMWQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-win32-arm64-msvc": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.61.0.tgz",
+      "integrity": "sha512-VzsAISkFxmNhJ5LBDEL9VuH6tJsVJMtqYit2LyIUf/HLnsCe4Pg9SMOjjVQzGWt0bnpyfJ94CrqTqcpNZzK+ug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-win32-ia32-msvc": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.61.0.tgz",
+      "integrity": "sha512-xv4t7yzwJoYaLB6Zv28B3W+j7brEjsyv50rLTAQgmxJzddce9fAMCxed8dSAkbWES0zz2J29nYK5FaTuD2YBHg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxfmt/binding-win32-x64-msvc": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.61.0.tgz",
+      "integrity": "sha512-6EZXFkqOwxdDYjIn3TSNnPk3ST5E5GiYd4FiM6UF/mCL/LZSfr6D6UygTfW3R1PCQP2quCKpCEGRlij8E3VYbg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint-tsgolint/darwin-arm64": {
+      "version": "7.0.2001",
+      "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2001.tgz",
+      "integrity": "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@oxlint-tsgolint/darwin-x64": {
+      "version": "7.0.2001",
+      "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2001.tgz",
+      "integrity": "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@oxlint-tsgolint/linux-arm64": {
+      "version": "7.0.2001",
+      "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2001.tgz",
+      "integrity": "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@oxlint-tsgolint/linux-x64": {
+      "version": "7.0.2001",
+      "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2001.tgz",
+      "integrity": "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@oxlint-tsgolint/win32-arm64": {
+      "version": "7.0.2001",
+      "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2001.tgz",
+      "integrity": "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@oxlint-tsgolint/win32-x64": {
+      "version": "7.0.2001",
+      "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2001.tgz",
+      "integrity": "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@oxlint/binding-android-arm-eabi": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.76.0.tgz",
+      "integrity": "sha512-ZHIE5Zt9AsPDcY4nOlofXt0YfneEeo+QrKMPcPzLf2Z6Q8VtV2W73d7SFJ920WUwyik783u/doKCs3KXdwG+7w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-android-arm64": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.76.0.tgz",
+      "integrity": "sha512-shm/ngQilHK6bs+ElJWa4oHfNj5vL1Gl/iVEJldTQjpr0/67oSgr0KUpbmcnLig5Fo0v/l6j2567A7TOL89ONA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-darwin-arm64": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.76.0.tgz",
+      "integrity": "sha512-rvJmrAPKSQ9aWJ6wIS6CK2tJjwzfW0ApQH9qokq6sfDvmHwoyIHxHFMq7z7i7GiV6fdE6s8qvBqWKPTu8RmT6Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-darwin-x64": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.76.0.tgz",
+      "integrity": "sha512-U/zYdb7VYKGY6pA9Vd2rYl9O/HlCylcOlb5PGPvVLtg+oLGsk6H3XGKEMHKyqD3nmmtmlmwb/8SwU2vfSAtvMw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-freebsd-x64": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.76.0.tgz",
+      "integrity": "sha512-WvKG9CAriuo0XNiFzpXjDngUZcRGFNpaK2kLyMUsnJlShxkT96u+BpJQ3KqdQwGOrvI14L6V8bAwXwAYNNY6Jg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.76.0.tgz",
+      "integrity": "sha512-qJ5+RH99TqFRq3UCDxkW0zJJu9c+OAHFY72vGlxZLEpuO+MpKo3POgqb8sYipL9KYm8XY6ofb0HsOuvY6hQNqQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-arm-musleabihf": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.76.0.tgz",
+      "integrity": "sha512-PvPCVptkgVARsucgIqFQQcSmJ6xc6GtnVB5bRBekRahTc9eObMtjHfMjy5M+C2tHt5UCMttWM9RuSk/H9NqYeg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-arm64-gnu": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.76.0.tgz",
+      "integrity": "sha512-3KeFDx8Bu4HPAXbuHZOr/oHvN+QT+JQhMw/NYPz7Z071xLSsG27Jfh9PIQVEY7hk1I+jr43ExqRIeJ6VKk2yLw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-arm64-musl": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.76.0.tgz",
+      "integrity": "sha512-oPFkkKTgl0K/EIg9fQ8oA3IGcI05/Mq1en04iFa41mmNPT+6KEiByVazTOZZJiHMBBrbsns1YJ2e1Scqwzesjw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-ppc64-gnu": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.76.0.tgz",
+      "integrity": "sha512-gN7yZ0eqflA5Fhf1wvHxGUltIV3FsvmB1zhNMDEK9vSHhc7E6qg9CuPeBgPZab66Tjzq6w6kHAtNEvnTHf4cyw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-riscv64-gnu": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.76.0.tgz",
+      "integrity": "sha512-S/HqMbn22mQrjtErUxEoS/a55u8kIeXvreIxiJu5G7Le3UecEd6SQZxrDIpuhtgaFnsY/nVra3ytP+pRljDilA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-riscv64-musl": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.76.0.tgz",
+      "integrity": "sha512-ZIga3097VJZolGZk6SrIAUokIGfRkxRlhiHDUznZptGBfwrhD7pNfD1rzEzsCwvk/1DX0A1bLz+liuNh5QKIVQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-s390x-gnu": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.76.0.tgz",
+      "integrity": "sha512-ZGiiA7pFzMJSyMWYZTVlPgbTsx+Vl8ihLGMIujPwaslUF7kIPPWAbVmAlTc+9lWDV+DCiB8Ikixu+lSHeOIIWQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-x64-gnu": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.76.0.tgz",
+      "integrity": "sha512-JLiy5WuvEBFTT6ErIFV35SLzi0R7Iri6MKU6dZbTxfIx8pndbbPs3Mj780nMipBFcPkti+okAPOJ9POKkHFEgg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-linux-x64-musl": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.76.0.tgz",
+      "integrity": "sha512-z7lgKQtbo/I1NIe8G5NHLesxJDv0tRSUWTpXKb9Pm3E9nKFKfO4IOSDtFroKgXtOYb0jQbcdH+0wzTyMXVes+A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-openharmony-arm64": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.76.0.tgz",
+      "integrity": "sha512-JOjKymIpb9QcYfEhZsN6h4V9Ivd474W38cNIBRv6bg2TbIvogbMTH0Mg6YWW9TiRDqfcX+/Hyfsbo5vcSE5guQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-win32-arm64-msvc": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.76.0.tgz",
+      "integrity": "sha512-pqDWZiwcmByWUEm1NFUBNiT6aentCcaoMWJv0HbXEmuYermJ4sg8ppVrshubYP2MZ6SHccJJcpr6x469PuDFIw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-win32-ia32-msvc": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.76.0.tgz",
+      "integrity": "sha512-Ba0O659kgMv6pwO3z9PdO+K3aMxQRaw9HnG+e6AtOfgwcKFvYilciQYBoUBmxfQvOCKZe1SwjMkuB542NkuDMQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@oxlint/binding-win32-x64-msvc": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.76.0.tgz",
+      "integrity": "sha512-5qcirPHO8nKfkoowEVWtpAoVTcYDy6g0UT0NGic450Qv8J2NrOqg4uQ8QppRP4MDTC7Xx47lbZnmadTH03CGGA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@poppinss/colors": {
+      "version": "4.1.6",
+      "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
+      "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "kleur": "^4.1.5"
+      }
+    },
+    "node_modules/@poppinss/dumper": {
+      "version": "0.6.5",
+      "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
+      "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@poppinss/colors": "^4.1.5",
+        "@sindresorhus/is": "^7.0.2",
+        "supports-color": "^10.0.0"
+      }
+    },
+    "node_modules/@poppinss/dumper/node_modules/supports-color": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
+      "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/@poppinss/exception": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",
+      "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@radix-ui/primitive": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz",
+      "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==",
+      "license": "MIT"
+    },
+    "node_modules/@radix-ui/react-compose-refs": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz",
+      "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-context": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz",
+      "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-dialog": {
+      "version": "1.1.23",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz",
+      "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.7",
+        "@radix-ui/react-compose-refs": "1.1.5",
+        "@radix-ui/react-context": "1.2.2",
+        "@radix-ui/react-dismissable-layer": "1.1.19",
+        "@radix-ui/react-focus-guards": "1.1.6",
+        "@radix-ui/react-focus-scope": "1.1.16",
+        "@radix-ui/react-id": "1.1.4",
+        "@radix-ui/react-portal": "1.1.17",
+        "@radix-ui/react-presence": "1.1.10",
+        "@radix-ui/react-primitive": "2.1.10",
+        "@radix-ui/react-slot": "1.3.3",
+        "@radix-ui/react-use-controllable-state": "1.2.6",
+        "@radix-ui/react-use-layout-effect": "1.1.4",
+        "aria-hidden": "^1.2.4",
+        "react-remove-scroll": "^2.7.2"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-dismissable-layer": {
+      "version": "1.1.19",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz",
+      "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.7",
+        "@radix-ui/react-compose-refs": "1.1.5",
+        "@radix-ui/react-primitive": "2.1.10",
+        "@radix-ui/react-use-callback-ref": "1.1.4",
+        "@radix-ui/react-use-effect-event": "0.0.5"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-focus-guards": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz",
+      "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-focus-scope": {
+      "version": "1.1.16",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz",
+      "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.5",
+        "@radix-ui/react-primitive": "2.1.10",
+        "@radix-ui/react-use-callback-ref": "1.1.4"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-id": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz",
+      "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-layout-effect": "1.1.4"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-portal": {
+      "version": "1.1.17",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz",
+      "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-primitive": "2.1.10",
+        "@radix-ui/react-use-layout-effect": "1.1.4"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-presence": {
+      "version": "1.1.10",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz",
+      "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-layout-effect": "1.1.4"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-primitive": {
+      "version": "2.1.10",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz",
+      "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-slot": "1.3.3"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-slot": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz",
+      "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.5"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-callback-ref": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz",
+      "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-controllable-state": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz",
+      "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.7",
+        "@radix-ui/react-use-effect-event": "0.0.5",
+        "@radix-ui/react-use-layout-effect": "1.1.4"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-effect-event": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz",
+      "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-layout-effect": "1.1.4"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-layout-effect": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz",
+      "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@reduxjs/toolkit": {
+      "version": "2.12.0",
+      "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
+      "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.0.0",
+        "@standard-schema/utils": "^0.3.0",
+        "immer": "^11.0.0",
+        "redux": "^5.0.1",
+        "redux-thunk": "^3.1.0",
+        "reselect": "^5.1.0"
+      },
+      "peerDependencies": {
+        "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+        "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "react": {
+          "optional": true
+        },
+        "react-redux": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@reduxjs/toolkit/node_modules/immer": {
+      "version": "11.1.18",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz",
+      "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
+    "node_modules/@resvg/resvg-wasm": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz",
+      "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==",
+      "license": "MPL-2.0",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
+      "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
+      "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-x64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
+      "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-freebsd-x64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
+      "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
+      "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
+      "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
+      "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
+      "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
+      "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
+      "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
+      "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
+      "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-wasm32-wasi": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
+      "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "1.10.0",
+        "@emnapi/runtime": "1.10.0",
+        "@napi-rs/wasm-runtime": "^1.1.4"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+      "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
+      "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
+      "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+      "license": "MIT"
+    },
+    "node_modules/@sec-ant/readable-stream": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
+      "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+      "license": "MIT"
+    },
+    "node_modules/@shadcn/react": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@shadcn/react/-/react-0.3.0.tgz",
+      "integrity": "sha512-iKN0NuYe850VDHlxEfvppFrpa7CMV3zArTHusXlaSmeFeQhohGbIqclv6WwPTs/44I8EkXQpcuRt6sXEVKkWqQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": ">=19",
+        "react": ">=19"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@shuding/opentype.js": {
+      "version": "1.4.0-beta.0",
+      "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
+      "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==",
+      "license": "MIT",
+      "dependencies": {
+        "fflate": "^0.7.3",
+        "string.prototype.codepointat": "^0.2.1"
+      },
+      "bin": {
+        "ot": "bin/ot"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      }
+    },
+    "node_modules/@sindresorhus/is": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
+      "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/is?sponsor=1"
+      }
+    },
+    "node_modules/@sindresorhus/merge-streams": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+      "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@speed-highlight/core": {
+      "version": "1.2.24",
+      "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz",
+      "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "license": "MIT"
+    },
+    "node_modules/@standard-schema/utils": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+      "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+      "license": "MIT"
+    },
+    "node_modules/@tailwindcss/node": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
+      "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "enhanced-resolve": "^5.19.0",
+        "jiti": "^2.6.1",
+        "lightningcss": "1.31.1",
+        "magic-string": "^0.30.21",
+        "source-map-js": "^1.2.1",
+        "tailwindcss": "4.2.1"
+      }
+    },
+    "node_modules/@tailwindcss/oxide": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
+      "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20"
+      },
+      "optionalDependencies": {
+        "@tailwindcss/oxide-android-arm64": "4.2.1",
+        "@tailwindcss/oxide-darwin-arm64": "4.2.1",
+        "@tailwindcss/oxide-darwin-x64": "4.2.1",
+        "@tailwindcss/oxide-freebsd-x64": "4.2.1",
+        "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
+        "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
+        "@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
+        "@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
+        "@tailwindcss/oxide-linux-x64-musl": "4.2.1",
+        "@tailwindcss/oxide-wasm32-wasi": "4.2.1",
+        "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
+        "@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-android-arm64": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
+      "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-darwin-arm64": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
+      "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-darwin-x64": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
+      "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-freebsd-x64": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
+      "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
+      "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
+      "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
+      "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
+      "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
+      "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
+      "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
+      "bundleDependencies": [
+        "@napi-rs/wasm-runtime",
+        "@emnapi/core",
+        "@emnapi/runtime",
+        "@tybys/wasm-util",
+        "@emnapi/wasi-threads",
+        "tslib"
+      ],
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.8.1",
+        "@emnapi/runtime": "^1.8.1",
+        "@emnapi/wasi-threads": "^1.1.0",
+        "@napi-rs/wasm-runtime": "^1.1.1",
+        "@tybys/wasm-util": "^0.10.1",
+        "tslib": "^2.8.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
+      "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
+      "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/postcss": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz",
+      "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "@tailwindcss/node": "4.2.1",
+        "@tailwindcss/oxide": "4.2.1",
+        "postcss": "^8.5.6",
+        "tailwindcss": "4.2.1"
+      }
+    },
+    "node_modules/@ts-morph/common": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz",
+      "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-glob": "^3.3.3",
+        "minimatch": "^10.0.1",
+        "path-browserify": "^1.0.1"
+      }
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.3",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+      "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/d3-array": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+      "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-color": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+      "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-ease": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+      "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-interpolate": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+      "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-path": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+      "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-path": "*"
+      }
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-timer": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+      "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/@types/node": {
+      "version": "22.19.19",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
+      "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/react": {
+      "version": "19.2.14",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+      "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "19.2.3",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+      "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+      "devOptional": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^19.2.0"
+      }
+    },
+    "node_modules/@types/use-sync-external-store": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+      "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/validate-npm-package-name": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
+      "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==",
+      "license": "MIT"
+    },
+    "node_modules/@unpic/core": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@unpic/core/-/core-1.0.3.tgz",
+      "integrity": "sha512-aum9YNVUGso7MjGLD0Rp/08kywCGLqZ03/q6VQBFFakDBOXWEc8D4kPGcZ8v5wEnGRex3lE+++bOuucBp3KJ/w==",
+      "license": "MIT",
+      "dependencies": {
+        "unpic": "^4.2.2"
+      }
+    },
+    "node_modules/@unpic/react": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@unpic/react/-/react-1.0.2.tgz",
+      "integrity": "sha512-5RmRfELwTF8w+4zjtQGqjpvX+RU2VLvis3xDCS1O2uWk0PZN2cvatL+3/KAR3mshAuRrkFGTX1XwyAezSXaoCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@unpic/core": "^1.0.3"
+      },
+      "peerDependencies": {
+        "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "next": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vercel/og": {
+      "version": "0.8.6",
+      "resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.8.6.tgz",
+      "integrity": "sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==",
+      "license": "MPL-2.0",
+      "dependencies": {
+        "@resvg/resvg-wasm": "2.4.0",
+        "satori": "0.16.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@vinext/types": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/@vinext/types/-/types-1.0.0-beta.2.tgz",
+      "integrity": "sha512-kbKY/H1RWlf7yox4G6Vwk1JeM6x0Tv1EAv28f3tDYkK3brq+89vp5eZHbdw3L/pZMbAhG4oFyhT9C976y+jkhQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=22"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
+      "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "^1.0.0"
+      },
+      "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",
+        "vite": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@rolldown/plugin-babel": {
+          "optional": true
+        },
+        "babel-plugin-react-compiler": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitejs/plugin-rsc": {
+      "version": "0.5.26",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-rsc/-/plugin-rsc-0.5.26.tgz",
+      "integrity": "sha512-T8W8ODEutblw9qXQB512LDPyv1tAbJRD/Gf0QEGsAoydl4nxEtIrghnhoI9oLY9R+7aw+cLk1ZEltxWHWf4aHw==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-rc.18",
+        "es-module-lexer": "^2.1.0",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.21",
+        "srvx": "^0.11.15",
+        "strip-literal": "^3.1.0",
+        "turbo-stream": "^3.2.0",
+        "vitefu": "^1.1.3"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*",
+        "react-server-dom-webpack": "*",
+        "vite": "*"
+      },
+      "peerDependenciesMeta": {
+        "react-server-dom-webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitejs/plugin-rsc/node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.18",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz",
+      "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/ast": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+      "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/helper-numbers": "1.13.2",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+      }
+    },
+    "node_modules/@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+      "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/helper-api-error": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+      "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/helper-buffer": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+      "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/helper-numbers": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+      "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+        "@webassemblyjs/helper-api-error": "1.13.2",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+      "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/helper-wasm-section": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+      "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/wasm-gen": "1.14.1"
+      }
+    },
+    "node_modules/@webassemblyjs/ieee754": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+      "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "node_modules/@webassemblyjs/leb128": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+      "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+      "license": "Apache-2.0",
+      "peer": true,
+      "dependencies": {
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/utf8": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+      "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/wasm-edit": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+      "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/helper-wasm-section": "1.14.1",
+        "@webassemblyjs/wasm-gen": "1.14.1",
+        "@webassemblyjs/wasm-opt": "1.14.1",
+        "@webassemblyjs/wasm-parser": "1.14.1",
+        "@webassemblyjs/wast-printer": "1.14.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-gen": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+      "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/ieee754": "1.13.2",
+        "@webassemblyjs/leb128": "1.13.2",
+        "@webassemblyjs/utf8": "1.13.2"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-opt": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+      "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/wasm-gen": "1.14.1",
+        "@webassemblyjs/wasm-parser": "1.14.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-parser": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+      "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-api-error": "1.13.2",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/ieee754": "1.13.2",
+        "@webassemblyjs/leb128": "1.13.2",
+        "@webassemblyjs/utf8": "1.13.2"
+      }
+    },
+    "node_modules/@webassemblyjs/wast-printer": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+      "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@xtuc/ieee754": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+      "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+      "license": "BSD-3-Clause",
+      "peer": true
+    },
+    "node_modules/@xtuc/long": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+      "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+      "license": "Apache-2.0",
+      "peer": true
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+      "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-loose": {
+      "version": "8.5.2",
+      "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz",
+      "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==",
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.15.0"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+      "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+      "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ajv": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ajv-keywords": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+      "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3"
+      },
+      "peerDependencies": {
+        "ajv": "^8.8.2"
+      }
+    },
+    "node_modules/ansi-colors": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+      "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "license": "Python-2.0"
+    },
+    "node_modules/aria-hidden": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+      "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ast-types": {
+      "version": "0.16.3",
+      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.3.tgz",
+      "integrity": "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/atomically": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
+      "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.12.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/base64-js": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+      "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.11.21",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
+      "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/blake3-wasm": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
+      "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/body-parser": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+      "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^2.0.0",
+        "debug": "^4.4.3",
+        "http-errors": "^2.0.1",
+        "iconv-lite": "^0.7.2",
+        "on-finished": "^2.4.1",
+        "qs": "^6.15.2",
+        "raw-body": "^3.0.2",
+        "type-is": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/body-parser/node_modules/content-type": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+      "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "5.0.9",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+      "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.9",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
+      "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.11.20",
+        "caniuse-lite": "^1.0.30001810",
+        "electron-to-chromium": "^1.5.420",
+        "node-releases": "^2.0.54",
+        "update-browserslist-db": "^1.3.2"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/bundle-name": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+      "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+      "license": "MIT",
+      "dependencies": {
+        "run-applescript": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/camelize": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+      "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001810",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+      "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chalk": {
+      "version": "5.6.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+      "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/chrome-trace-event": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+      "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=6.0"
+      }
+    },
+    "node_modules/class-variance-authority": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+      "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "clsx": "^2.1.1"
+      },
+      "funding": {
+        "url": "https://polar.sh/cva"
+      }
+    },
+    "node_modules/cli-cursor": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+      "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-spinners": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+      "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/cmdk": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+      "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "^1.1.1",
+        "@radix-ui/react-dialog": "^1.1.6",
+        "@radix-ui/react-id": "^1.1.0",
+        "@radix-ui/react-primitive": "^2.0.2"
+      },
+      "peerDependencies": {
+        "react": "^18 || ^19 || ^19.0.0-rc",
+        "react-dom": "^18 || ^19 || ^19.0.0-rc"
+      }
+    },
+    "node_modules/code-block-writer": {
+      "version": "13.0.3",
+      "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
+      "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
+      "license": "MIT"
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/commander": {
+      "version": "14.0.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+      "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/conf": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz",
+      "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.6.3",
+        "ajv-formats": "^2.1.1",
+        "atomically": "^1.7.0",
+        "debounce-fn": "^4.0.0",
+        "dot-prop": "^6.0.1",
+        "env-paths": "^2.2.1",
+        "json-schema-typed": "^7.0.3",
+        "onetime": "^5.1.2",
+        "pkg-up": "^3.1.0",
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/conf/node_modules/ajv-formats": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+      "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ajv": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/conf/node_modules/json-schema-typed": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz",
+      "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/conf/node_modules/semver": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+      "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/cors": {
+      "version": "2.8.6",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+      "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/cosmiconfig": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
+      "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
+      "license": "MIT",
+      "dependencies": {
+        "env-paths": "^2.2.1",
+        "import-fresh": "^3.3.0",
+        "js-yaml": "^4.1.0",
+        "parse-json": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/d-fischer"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.9.5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cross-spawn/node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/cross-spawn/node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/css-background-parser": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
+      "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==",
+      "license": "MIT"
+    },
+    "node_modules/css-box-shadow": {
+      "version": "1.0.0-3",
+      "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
+      "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==",
+      "license": "MIT"
+    },
+    "node_modules/css-color-keywords": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+      "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/css-gradient-parser": {
+      "version": "0.0.16",
+      "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz",
+      "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/css-to-react-native": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+      "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+      "license": "MIT",
+      "dependencies": {
+        "camelize": "^1.0.0",
+        "css-color-keywords": "^1.0.0",
+        "postcss-value-parser": "^4.0.2"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "license": "ISC",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+      "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/date-fns": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+      "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/kossnocorp"
+      }
+    },
+    "node_modules/date-fns-jalali": {
+      "version": "4.1.0-0",
+      "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
+      "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
+      "license": "MIT"
+    },
+    "node_modules/debounce-fn": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
+      "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decimal.js-light": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+      "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+      "license": "MIT"
+    },
+    "node_modules/dedent": {
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
+      "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "babel-plugin-macros": "^3.1.0"
+      },
+      "peerDependenciesMeta": {
+        "babel-plugin-macros": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/default-browser": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz",
+      "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==",
+      "license": "MIT",
+      "dependencies": {
+        "bundle-name": "^4.1.0",
+        "default-browser-id": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/default-browser-id": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+      "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/define-lazy-prop": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+      "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/detect-node-es": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+      "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+      "license": "MIT"
+    },
+    "node_modules/diff": {
+      "version": "8.0.4",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
+      "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/dot-prop": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
+      "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-obj": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "17.4.2",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+      "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.425",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.425.tgz",
+      "integrity": "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g==",
+      "license": "ISC"
+    },
+    "node_modules/embla-carousel": {
+      "version": "8.5.2",
+      "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.2.tgz",
+      "integrity": "sha512-xQ9oVLrun/eCG/7ru3R+I5bJ7shsD8fFwLEY7yPe27/+fDHCNj0OT5EoG5ZbFyOxOcG6yTwW8oTz/dWyFnyGpg==",
+      "license": "MIT"
+    },
+    "node_modules/embla-carousel-react": {
+      "version": "8.5.2",
+      "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.5.2.tgz",
+      "integrity": "sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==",
+      "license": "MIT",
+      "dependencies": {
+        "embla-carousel": "8.5.2",
+        "embla-carousel-reactive-utils": "8.5.2"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+      }
+    },
+    "node_modules/embla-carousel-reactive-utils": {
+      "version": "8.5.2",
+      "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.2.tgz",
+      "integrity": "sha512-QC8/hYSK/pEmqEdU1IO5O+XNc/Ptmmq7uCB44vKplgLKhB/l0+yvYx0+Cv0sF6Ena8Srld5vUErZkT+yTahtDg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "embla-carousel": "8.5.2"
+      }
+    },
+    "node_modules/emoji-regex": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+      "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex-xs": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz",
+      "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/enhanced-resolve": {
+      "version": "5.24.5",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
+      "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==",
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.3.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/enquirer": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
+      "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-colors": "^4.1.1",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/env-paths": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+      "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/error-ex": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+      "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "node_modules/error-stack-parser-es": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
+      "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+      "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+      "license": "MIT"
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-toolkit": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz",
+      "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==",
+      "license": "MIT",
+      "workspaces": [
+        "docs",
+        "benchmarks",
+        "tests/types",
+        "tests/browser-compat"
+      ]
+    },
+    "node_modules/esbuild": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+      "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+      "devOptional": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.27.3",
+        "@esbuild/android-arm": "0.27.3",
+        "@esbuild/android-arm64": "0.27.3",
+        "@esbuild/android-x64": "0.27.3",
+        "@esbuild/darwin-arm64": "0.27.3",
+        "@esbuild/darwin-x64": "0.27.3",
+        "@esbuild/freebsd-arm64": "0.27.3",
+        "@esbuild/freebsd-x64": "0.27.3",
+        "@esbuild/linux-arm": "0.27.3",
+        "@esbuild/linux-arm64": "0.27.3",
+        "@esbuild/linux-ia32": "0.27.3",
+        "@esbuild/linux-loong64": "0.27.3",
+        "@esbuild/linux-mips64el": "0.27.3",
+        "@esbuild/linux-ppc64": "0.27.3",
+        "@esbuild/linux-riscv64": "0.27.3",
+        "@esbuild/linux-s390x": "0.27.3",
+        "@esbuild/linux-x64": "0.27.3",
+        "@esbuild/netbsd-arm64": "0.27.3",
+        "@esbuild/netbsd-x64": "0.27.3",
+        "@esbuild/openbsd-arm64": "0.27.3",
+        "@esbuild/openbsd-x64": "0.27.3",
+        "@esbuild/openharmony-arm64": "0.27.3",
+        "@esbuild/sunos-x64": "0.27.3",
+        "@esbuild/win32-arm64": "0.27.3",
+        "@esbuild/win32-ia32": "0.27.3",
+        "@esbuild/win32-x64": "0.27.3"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "license": "BSD-2-Clause",
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+      "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+      "license": "MIT"
+    },
+    "node_modules/events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=0.8.x"
+      }
+    },
+    "node_modules/eventsource": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+      "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+      "license": "MIT",
+      "dependencies": {
+        "eventsource-parser": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/eventsource-parser": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
+      "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/execa": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
+      "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
+      "license": "MIT",
+      "dependencies": {
+        "@sindresorhus/merge-streams": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "figures": "^6.1.0",
+        "get-stream": "^9.0.0",
+        "human-signals": "^8.0.1",
+        "is-plain-obj": "^4.1.0",
+        "is-stream": "^4.0.1",
+        "npm-run-path": "^6.0.0",
+        "pretty-ms": "^9.2.0",
+        "signal-exit": "^4.1.0",
+        "strip-final-newline": "^4.0.0",
+        "yoctocolors": "^2.1.1"
+      },
+      "engines": {
+        "node": "^18.19.0 || >=20.5.0"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+      "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.1",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express-rate-limit": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
+      "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.3",
+        "ip-address": "^10.2.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/express-rate-limit"
+      },
+      "peerDependencies": {
+        "express": ">= 4.11"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-uri": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
+      "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/fastq": {
+      "version": "1.20.3",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
+      "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==",
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fflate": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.5.tgz",
+      "integrity": "sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g==",
+      "license": "MIT"
+    },
+    "node_modules/figures": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+      "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-unicode-supported": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+      "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+      "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/fs-extra": {
+      "version": "11.4.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz",
+      "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/fuzzysort": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz",
+      "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==",
+      "license": "MIT"
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-east-asian-width": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
+      "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-nonce": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+      "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/get-own-enumerable-keys": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz",
+      "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+      "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
+      "license": "MIT",
+      "dependencies": {
+        "@sec-ant/readable-stream": "^0.4.1",
+        "is-stream": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "license": "ISC"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hex-rgb": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz",
+      "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/hono": {
+      "version": "4.13.7",
+      "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz",
+      "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16.9.0"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/human-signals": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
+      "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.7.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+      "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/image-size": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
+      "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==",
+      "license": "MIT",
+      "bin": {
+        "image-size": "bin/image-size.js"
+      },
+      "engines": {
+        "node": ">=16.x"
+      }
+    },
+    "node_modules/immer": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+      "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/input-otp": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
+      "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
+      }
+    },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/ip-address": {
+      "version": "10.7.0",
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
+      "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "license": "MIT"
+    },
+    "node_modules/is-docker": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+      "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-in-ssh": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
+      "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-inside-container": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+      "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^3.0.0"
+      },
+      "bin": {
+        "is-inside-container": "cli.js"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-interactive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+      "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-obj": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+      "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-plain-obj": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+      "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-regexp": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz",
+      "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-stream": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+      "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+      "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-wsl": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+      "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-inside-container": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
+      "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/jest-worker": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+      "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@types/node": "*",
+        "merge-stream": "^2.0.0",
+        "supports-color": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+      "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+      "devOptional": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
+      }
+    },
+    "node_modules/jose": {
+      "version": "6.2.12",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
+      "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+      "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/nodeca"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "license": "MIT"
+    },
+    "node_modules/json-schema-typed": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+      "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsonfile": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+      "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/kleur": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
+      "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
+      "dev": true,
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.31.1",
+        "lightningcss-darwin-arm64": "1.31.1",
+        "lightningcss-darwin-x64": "1.31.1",
+        "lightningcss-freebsd-x64": "1.31.1",
+        "lightningcss-linux-arm-gnueabihf": "1.31.1",
+        "lightningcss-linux-arm64-gnu": "1.31.1",
+        "lightningcss-linux-arm64-musl": "1.31.1",
+        "lightningcss-linux-x64-gnu": "1.31.1",
+        "lightningcss-linux-x64-musl": "1.31.1",
+        "lightningcss-win32-arm64-msvc": "1.31.1",
+        "lightningcss-win32-x64-msvc": "1.31.1"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
+      "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-arm64": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
+      "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-x64": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
+      "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-freebsd-x64": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
+      "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
+      "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
+      "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
+      "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
+      "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
+      "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
+      "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.31.1",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
+      "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/linebreak": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
+      "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "0.0.8",
+        "unicode-trie": "^2.0.0"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "license": "MIT"
+    },
+    "node_modules/locate-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+      "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^3.0.0",
+        "path-exists": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/log-symbols": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
+      "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^5.3.0",
+        "is-unicode-supported": "^1.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-symbols/node_modules/is-unicode-supported": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+      "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/lucide-react": {
+      "version": "1.31.0",
+      "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.31.0.tgz",
+      "integrity": "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==",
+      "license": "ISC",
+      "peerDependencies": {
+        "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
+      "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "license": "MIT"
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/micromatch/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+      "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
+      "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/mimic-function": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+      "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/miniflare": {
+      "version": "4.20260515.0",
+      "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260515.0.tgz",
+      "integrity": "sha512-2j0oQWizk1Eu4Cm8tDX7Z+Nsjd0nebIj1TQcQ+Oy1QKeo0Ay9+bdn8wfLAtOj9znDCybDCUlnS1+nYvKXEdfNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@cspotcode/source-map-support": "0.8.1",
+        "sharp": "^0.34.5",
+        "undici": "7.24.8",
+        "workerd": "1.20260515.1",
+        "ws": "8.18.0",
+        "youch": "4.1.0-beta.10"
+      },
+      "bin": {
+        "miniflare": "bootstrap.js"
+      },
+      "engines": {
+        "node": ">=22.0.0"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "10.2.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+      "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.8"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minimizer-webpack-plugin": {
+      "version": "5.10.1",
+      "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.10.1.tgz",
+      "integrity": "sha512-+dsZEyTcy1lkq41lxdiOqvb26gXbYGgwY+30w37f9PqUVkuEBejBLDotsHOTjuga9xJyGLW2DqzKDUyJ4W/PMQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.31",
+        "jest-worker": "^27.4.5",
+        "schema-utils": "^4.3.3",
+        "terser": "^5.51.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.1.0"
+      },
+      "peerDependenciesMeta": {
+        "@minify-html/node": {
+          "optional": true
+        },
+        "@napi-rs/image": {
+          "optional": true
+        },
+        "@swc/core": {
+          "optional": true
+        },
+        "@swc/css": {
+          "optional": true
+        },
+        "@swc/html": {
+          "optional": true
+        },
+        "clean-css": {
+          "optional": true
+        },
+        "cssnano": {
+          "optional": true
+        },
+        "csso": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "html-minifier-terser": {
+          "optional": true
+        },
+        "imagemin": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "postcss": {
+          "optional": true
+        },
+        "sharp": {
+          "optional": true
+        },
+        "svgo": {
+          "optional": true
+        },
+        "uglify-js": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.18",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+      "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/negotiator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
+      "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/negotiator/node_modules/content-type": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+      "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "license": "MIT"
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.55",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz",
+      "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/npm-run-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
+      "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^4.0.0",
+        "unicorn-magic": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/npm-run-path/node_modules/path-key": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-treeify": {
+      "version": "1.1.33",
+      "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz",
+      "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/onetime/node_modules/mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/open": {
+      "version": "11.0.2",
+      "resolved": "https://registry.npmjs.org/open/-/open-11.0.2.tgz",
+      "integrity": "sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "default-browser": "^5.5.1",
+        "define-lazy-prop": "^3.0.0",
+        "is-in-ssh": "^1.0.0",
+        "is-inside-container": "^1.0.0",
+        "powershell-utils": "^0.2.1",
+        "wsl-utils": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
+      "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^5.3.0",
+        "cli-cursor": "^5.0.0",
+        "cli-spinners": "^2.9.2",
+        "is-interactive": "^2.0.0",
+        "is-unicode-supported": "^2.0.0",
+        "log-symbols": "^6.0.0",
+        "stdin-discarder": "^0.2.2",
+        "string-width": "^7.2.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora/node_modules/ansi-regex": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
+      "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ora/node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/oxfmt": {
+      "version": "0.61.0",
+      "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.61.0.tgz",
+      "integrity": "sha512-DxdHBEMYpcEnHoUHjjOigUqV2TYKsvxLwUPXnVYBjgFdqrcQ/91OtwubtZ2PUodCs3sStI8R5Qw3fKNGK4e8wQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinypool": "2.1.0"
+      },
+      "bin": {
+        "oxfmt": "bin/oxfmt"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      },
+      "optionalDependencies": {
+        "@oxfmt/binding-android-arm-eabi": "0.61.0",
+        "@oxfmt/binding-android-arm64": "0.61.0",
+        "@oxfmt/binding-darwin-arm64": "0.61.0",
+        "@oxfmt/binding-darwin-x64": "0.61.0",
+        "@oxfmt/binding-freebsd-x64": "0.61.0",
+        "@oxfmt/binding-linux-arm-gnueabihf": "0.61.0",
+        "@oxfmt/binding-linux-arm-musleabihf": "0.61.0",
+        "@oxfmt/binding-linux-arm64-gnu": "0.61.0",
+        "@oxfmt/binding-linux-arm64-musl": "0.61.0",
+        "@oxfmt/binding-linux-ppc64-gnu": "0.61.0",
+        "@oxfmt/binding-linux-riscv64-gnu": "0.61.0",
+        "@oxfmt/binding-linux-riscv64-musl": "0.61.0",
+        "@oxfmt/binding-linux-s390x-gnu": "0.61.0",
+        "@oxfmt/binding-linux-x64-gnu": "0.61.0",
+        "@oxfmt/binding-linux-x64-musl": "0.61.0",
+        "@oxfmt/binding-openharmony-arm64": "0.61.0",
+        "@oxfmt/binding-win32-arm64-msvc": "0.61.0",
+        "@oxfmt/binding-win32-ia32-msvc": "0.61.0",
+        "@oxfmt/binding-win32-x64-msvc": "0.61.0"
+      },
+      "peerDependencies": {
+        "svelte": "^5.0.0",
+        "vite-plus": "*"
+      },
+      "peerDependenciesMeta": {
+        "svelte": {
+          "optional": true
+        },
+        "vite-plus": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/oxlint": {
+      "version": "1.76.0",
+      "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.76.0.tgz",
+      "integrity": "sha512-6QoFioEU4fNdiUx/2Eo6TRd6NG7H7njnRCz8rhB66cZmMHDTqcm1Rjvl8Wry+ZTQMBAmyb4Mlf62Mk5X+eHSOw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "oxlint": "bin/oxlint"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      },
+      "optionalDependencies": {
+        "@oxlint/binding-android-arm-eabi": "1.76.0",
+        "@oxlint/binding-android-arm64": "1.76.0",
+        "@oxlint/binding-darwin-arm64": "1.76.0",
+        "@oxlint/binding-darwin-x64": "1.76.0",
+        "@oxlint/binding-freebsd-x64": "1.76.0",
+        "@oxlint/binding-linux-arm-gnueabihf": "1.76.0",
+        "@oxlint/binding-linux-arm-musleabihf": "1.76.0",
+        "@oxlint/binding-linux-arm64-gnu": "1.76.0",
+        "@oxlint/binding-linux-arm64-musl": "1.76.0",
+        "@oxlint/binding-linux-ppc64-gnu": "1.76.0",
+        "@oxlint/binding-linux-riscv64-gnu": "1.76.0",
+        "@oxlint/binding-linux-riscv64-musl": "1.76.0",
+        "@oxlint/binding-linux-s390x-gnu": "1.76.0",
+        "@oxlint/binding-linux-x64-gnu": "1.76.0",
+        "@oxlint/binding-linux-x64-musl": "1.76.0",
+        "@oxlint/binding-openharmony-arm64": "1.76.0",
+        "@oxlint/binding-win32-arm64-msvc": "1.76.0",
+        "@oxlint/binding-win32-ia32-msvc": "1.76.0",
+        "@oxlint/binding-win32-x64-msvc": "1.76.0"
+      },
+      "peerDependencies": {
+        "oxlint-tsgolint": ">=7.0.2001",
+        "vite-plus": "*"
+      },
+      "peerDependenciesMeta": {
+        "oxlint-tsgolint": {
+          "optional": true
+        },
+        "vite-plus": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/oxlint-tsgolint": {
+      "version": "7.0.2001",
+      "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2001.tgz",
+      "integrity": "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "tsgolint": "bin/tsgolint.js"
+      },
+      "optionalDependencies": {
+        "@oxlint-tsgolint/darwin-arm64": "7.0.2001",
+        "@oxlint-tsgolint/darwin-x64": "7.0.2001",
+        "@oxlint-tsgolint/linux-arm64": "7.0.2001",
+        "@oxlint-tsgolint/linux-x64": "7.0.2001",
+        "@oxlint-tsgolint/win32-arm64": "7.0.2001",
+        "@oxlint-tsgolint/win32-x64": "7.0.2001"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+      "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/pako": {
+      "version": "0.2.9",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+      "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+      "license": "MIT"
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-css-color": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz",
+      "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "^1.1.4",
+        "hex-rgb": "^4.1.0"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse-ms": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+      "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "license": "MIT"
+    },
+    "node_modules/path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.4.2",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+      "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+      "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pkce-challenge": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+      "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16.20.0"
+      }
+    },
+    "node_modules/pkg-up": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
+      "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
+      "license": "MIT",
+      "dependencies": {
+        "find-up": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.28",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+      "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.18",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz",
+      "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==",
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "license": "MIT"
+    },
+    "node_modules/powershell-utils": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz",
+      "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/pretty-ms": {
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz",
+      "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==",
+      "license": "MIT",
+      "dependencies": {
+        "parse-ms": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/prompts": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+      "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+      "license": "MIT",
+      "dependencies": {
+        "kleur": "^3.0.3",
+        "sisteransi": "^1.0.5"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/prompts/node_modules/kleur": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+      "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.16.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+      "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "es-define-property": "^1.0.1",
+        "side-channel": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/range-parser": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
+      "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+      "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.7.0",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/react": {
+      "version": "19.2.6",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
+      "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-day-picker": {
+      "version": "9.8.1",
+      "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.1.tgz",
+      "integrity": "sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw==",
+      "license": "MIT",
+      "dependencies": {
+        "@date-fns/tz": "^1.2.0",
+        "date-fns": "^4.1.0",
+        "date-fns-jalali": "^4.1.0-0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://github.com/sponsors/gpbl"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.2.6",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
+      "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
+      "license": "MIT",
+      "dependencies": {
+        "scheduler": "^0.27.0"
+      },
+      "peerDependencies": {
+        "react": "^19.2.6"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "19.3.0",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz",
+      "integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/react-redux": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
+      "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/use-sync-external-store": "^0.0.6",
+        "use-sync-external-store": "^1.4.0"
+      },
+      "peerDependencies": {
+        "@types/react": "^18.2.25 || ^19",
+        "react": "^18.0 || ^19",
+        "redux": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "redux": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-remove-scroll": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+      "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+      "license": "MIT",
+      "dependencies": {
+        "react-remove-scroll-bar": "^2.3.7",
+        "react-style-singleton": "^2.2.3",
+        "tslib": "^2.1.0",
+        "use-callback-ref": "^1.3.3",
+        "use-sidecar": "^1.1.3"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/react-remove-scroll-bar": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+      "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+      "license": "MIT",
+      "dependencies": {
+        "react-style-singleton": "^2.2.2",
+        "tslib": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-resizable-panels": {
+      "version": "4.5.8",
+      "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.5.8.tgz",
+      "integrity": "sha512-X2S5YoYWbjd7Ove6e6T/kzOrjiUD6ccz55a+XW0H3JXbrPb+Gmz7YRAJy4ysOkua/U5jSOG9SoySbebMBjtQJQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/react-server-dom-webpack": {
+      "version": "19.2.6",
+      "resolved": "https://registry.npmjs.org/react-server-dom-webpack/-/react-server-dom-webpack-19.2.6.tgz",
+      "integrity": "sha512-762YXjBPc6hw2V16k4x1sdnw0mxghcSPzoiOhefGYjiTguJLCImGBUz6EjznPIfqKhWgLtpaQMlBhp66N8dKrw==",
+      "license": "MIT",
+      "dependencies": {
+        "acorn-loose": "^8.3.0",
+        "neo-async": "^2.6.1",
+        "webpack-sources": "^3.2.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "peerDependencies": {
+        "react": "^19.2.6",
+        "react-dom": "^19.2.6",
+        "webpack": "^5.59.0"
+      }
+    },
+    "node_modules/react-style-singleton": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+      "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+      "license": "MIT",
+      "dependencies": {
+        "get-nonce": "^1.0.0",
+        "tslib": "^2.0.0"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/recast": {
+      "version": "0.23.21",
+      "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz",
+      "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==",
+      "license": "MIT",
+      "dependencies": {
+        "ast-types": "^0.16.1",
+        "esprima": "~4.0.0",
+        "source-map": "~0.6.1",
+        "tiny-invariant": "^1.3.3",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/recharts": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz",
+      "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==",
+      "license": "MIT",
+      "workspaces": [
+        "www"
+      ],
+      "dependencies": {
+        "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+        "clsx": "^2.1.1",
+        "decimal.js-light": "^2.5.1",
+        "es-toolkit": "^1.39.3",
+        "eventemitter3": "^5.0.1",
+        "immer": "^10.1.1",
+        "react-redux": "8.x.x || 9.x.x",
+        "reselect": "5.1.1",
+        "tiny-invariant": "^1.3.3",
+        "use-sync-external-store": "^1.2.2",
+        "victory-vendor": "^37.0.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/recharts/node_modules/reselect": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+      "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+      "license": "MIT"
+    },
+    "node_modules/redux": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+      "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+      "license": "MIT"
+    },
+    "node_modules/redux-thunk": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+      "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "redux": "^5.0.0"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/reselect": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz",
+      "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==",
+      "license": "MIT"
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+      "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^7.0.0",
+        "signal-exit": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/restore-cursor/node_modules/onetime": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+      "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-function": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rolldown": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
+      "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.130.0",
+        "@rolldown/pluginutils": "^1.0.0"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.0.1",
+        "@rolldown/binding-darwin-arm64": "1.0.1",
+        "@rolldown/binding-darwin-x64": "1.0.1",
+        "@rolldown/binding-freebsd-x64": "1.0.1",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.1",
+        "@rolldown/binding-linux-arm64-musl": "1.0.1",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.1",
+        "@rolldown/binding-linux-x64-gnu": "1.0.1",
+        "@rolldown/binding-linux-x64-musl": "1.0.1",
+        "@rolldown/binding-openharmony-arm64": "1.0.1",
+        "@rolldown/binding-wasm32-wasi": "1.0.1",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.1",
+        "@rolldown/binding-win32-x64-msvc": "1.0.1"
+      }
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/run-applescript": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+      "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/satori": {
+      "version": "0.16.0",
+      "resolved": "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz",
+      "integrity": "sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==",
+      "license": "MPL-2.0",
+      "dependencies": {
+        "@shuding/opentype.js": "1.4.0-beta.0",
+        "css-background-parser": "^0.1.0",
+        "css-box-shadow": "1.0.0-3",
+        "css-gradient-parser": "^0.0.16",
+        "css-to-react-native": "^3.0.0",
+        "emoji-regex-xs": "^2.0.1",
+        "escape-html": "^1.0.3",
+        "linebreak": "^1.1.0",
+        "parse-css-color": "^0.2.1",
+        "postcss-value-parser": "^4.2.0",
+        "yoga-layout": "^3.2.1"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+      "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+      "license": "MIT"
+    },
+    "node_modules/schema-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.4.0.tgz",
+      "integrity": "sha512-ZzWFVzFgyRHi/T2Ecxm37lL6J9+hZO0P9L7LCuLxAbC6XWxkvxcaQBCXhQxMGn/Tdt9nD7zIBk0ELwhMQ3UEDg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.15",
+        "ajv": "^8.20.0",
+        "ajv-formats": "^3.0.1",
+        "ajv-keywords": "^5.1.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.3",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.1",
+        "mime-types": "^3.0.2",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+      "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shadcn": {
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.18.0.tgz",
+      "integrity": "sha512-tUFZgkYmfVNQVm3xX7lhSzOvDsp+O14ac5dwgXIr5mIsr79ISueb/Mu+ZtWMz0DH6v77u4eYyvbQ9TTMpSn3aw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.0",
+        "@babel/parser": "^7.28.0",
+        "@babel/plugin-transform-typescript": "^7.28.0",
+        "@babel/preset-typescript": "^7.27.1",
+        "@dotenvx/dotenvx": "^1.48.4",
+        "@modelcontextprotocol/sdk": "^1.26.0",
+        "@types/validate-npm-package-name": "^4.0.2",
+        "browserslist": "^4.26.2",
+        "commander": "^14.0.0",
+        "cosmiconfig": "^9.0.0",
+        "dedent": "^1.6.0",
+        "deepmerge": "^4.3.1",
+        "diff": "^8.0.2",
+        "execa": "^9.6.0",
+        "fast-glob": "^3.3.3",
+        "fs-extra": "^11.3.1",
+        "fuzzysort": "^3.1.0",
+        "kleur": "^4.1.5",
+        "open": "^11.0.0",
+        "ora": "^8.2.0",
+        "postcss": "^8.5.6",
+        "postcss-selector-parser": "^7.1.0",
+        "prompts": "^2.4.2",
+        "recast": "^0.23.11",
+        "socks": "^2.8.8",
+        "stringify-object": "^5.0.0",
+        "tailwind-merge": "^3.0.1",
+        "ts-morph": "^26.0.0",
+        "tsconfig-paths": "^4.2.0",
+        "undici": "^7.27.2",
+        "validate-npm-package-name": "^7.0.1",
+        "zod": "^3.24.1",
+        "zod-to-json-schema": "^3.24.6"
+      },
+      "bin": {
+        "shadcn": "dist/index.js"
+      },
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
+    "node_modules/shadcn/node_modules/undici": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
+      "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+      "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@img/colour": "^1.0.0",
+        "detect-libc": "^2.1.2",
+        "semver": "^7.7.3"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.34.5",
+        "@img/sharp-darwin-x64": "0.34.5",
+        "@img/sharp-libvips-darwin-arm64": "1.2.4",
+        "@img/sharp-libvips-darwin-x64": "1.2.4",
+        "@img/sharp-libvips-linux-arm": "1.2.4",
+        "@img/sharp-libvips-linux-arm64": "1.2.4",
+        "@img/sharp-libvips-linux-ppc64": "1.2.4",
+        "@img/sharp-libvips-linux-riscv64": "1.2.4",
+        "@img/sharp-libvips-linux-s390x": "1.2.4",
+        "@img/sharp-libvips-linux-x64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+        "@img/sharp-linux-arm": "0.34.5",
+        "@img/sharp-linux-arm64": "0.34.5",
+        "@img/sharp-linux-ppc64": "0.34.5",
+        "@img/sharp-linux-riscv64": "0.34.5",
+        "@img/sharp-linux-s390x": "0.34.5",
+        "@img/sharp-linux-x64": "0.34.5",
+        "@img/sharp-linuxmusl-arm64": "0.34.5",
+        "@img/sharp-linuxmusl-x64": "0.34.5",
+        "@img/sharp-wasm32": "0.34.5",
+        "@img/sharp-win32-arm64": "0.34.5",
+        "@img/sharp-win32-ia32": "0.34.5",
+        "@img/sharp-win32-x64": "0.34.5"
+      }
+    },
+    "node_modules/sharp/node_modules/semver": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+      "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+      "license": "MIT",
+      "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"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/sisteransi": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+      "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+      "license": "MIT"
+    },
+    "node_modules/smart-buffer": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+      "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/socks": {
+      "version": "2.8.10",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz",
+      "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ip-address": "^10.1.1",
+        "smart-buffer": "^4.2.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.21",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/srvx": {
+      "version": "0.11.22",
+      "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.22.tgz",
+      "integrity": "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==",
+      "devOptional": true,
+      "license": "MIT",
+      "bin": {
+        "srvx": "bin/srvx.mjs"
+      },
+      "engines": {
+        "node": ">=20.16.0"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/stdin-discarder": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
+      "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width/node_modules/ansi-regex": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
+      "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/string-width/node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/string.prototype.codepointat": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
+      "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==",
+      "license": "MIT"
+    },
+    "node_modules/stringify-object": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz",
+      "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "get-own-enumerable-keys": "^1.0.0",
+        "is-obj": "^3.0.0",
+        "is-regexp": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/yeoman/stringify-object?sponsor=1"
+      }
+    },
+    "node_modules/stringify-object/node_modules/is-obj": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz",
+      "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/strip-final-newline": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
+      "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-literal": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+      "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^9.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/strip-literal/node_modules/js-tokens": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+      "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/systeminformation": {
+      "version": "5.33.10",
+      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.10.tgz",
+      "integrity": "sha512-/NXbMVASt2UbSVgmWto4aBTIAeuYY7zOELpZybmNxqpsrbo5uYHpuEPf5nkyrPng0uG5YOb+Yv6s0FyKY4mDQg==",
+      "license": "MIT",
+      "os": [
+        "darwin",
+        "linux",
+        "win32",
+        "freebsd",
+        "openbsd",
+        "netbsd",
+        "sunos",
+        "android"
+      ],
+      "bin": {
+        "systeminformation": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "funding": {
+        "type": "Buy me a coffee",
+        "url": "https://www.buymeacoffee.com/systeminfo"
+      }
+    },
+    "node_modules/tailwind-merge": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+      "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/dcastil"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
+      "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tapable": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+      "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/terser": {
+      "version": "5.51.2",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz",
+      "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==",
+      "license": "BSD-2-Clause",
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/source-map": "^0.3.3",
+        "acorn": "^8.15.0",
+        "commander": "^2.20.0",
+        "source-map-support": "~0.5.20"
+      },
+      "bin": {
+        "terser": "bin/terser"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/terser/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/tiny-inflate": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+      "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
+      "license": "MIT"
+    },
+    "node_modules/tiny-invariant": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+      "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+      "license": "MIT"
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinypool": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz",
+      "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.0.0 || >=22.0.0"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/ts-morph": {
+      "version": "26.0.0",
+      "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz",
+      "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==",
+      "license": "MIT",
+      "dependencies": {
+        "@ts-morph/common": "~0.27.0",
+        "code-block-writer": "^13.0.3"
+      }
+    },
+    "node_modules/tsconfig-paths": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
+      "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
+      "license": "MIT",
+      "dependencies": {
+        "json5": "^2.2.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/turbo-stream": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-3.2.1.tgz",
+      "integrity": "sha512-BaX0eWz3IrypOVDFqPv3VGD6uZZW8y3oFAOJqZQ+oopmOyQ8/xNm7IMwAJFvxax2DBa8zSk+/AeqA6D6b1PXGg==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/tw-animate-css": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
+      "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Wombosvideo"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+      "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^2.0.0",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/type-is/node_modules/content-type": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+      "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici": {
+      "version": "7.24.8",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz",
+      "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "license": "MIT"
+    },
+    "node_modules/unenv": {
+      "version": "2.0.0-rc.24",
+      "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
+      "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/unicode-trie": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+      "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "pako": "^0.2.5",
+        "tiny-inflate": "^1.0.0"
+      }
+    },
+    "node_modules/unicorn-magic": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+      "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/unpic": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/unpic/-/unpic-4.2.2.tgz",
+      "integrity": "sha512-z6T2ScMgRV2y2H8MwwhY5xHZWXhUx/YxtOCGJwfURSl7ypVy4HpLIMWoIZKnnxQa/RKzM0kg8hUh0paIrpLfvw==",
+      "license": "MIT"
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
+      "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/use-callback-ref": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+      "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.0"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/use-sidecar": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+      "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+      "license": "MIT",
+      "dependencies": {
+        "detect-node-es": "^1.1.0",
+        "tslib": "^2.0.0"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/use-sync-external-store": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz",
+      "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/validate-npm-package-name": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz",
+      "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==",
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/victory-vendor": {
+      "version": "37.3.6",
+      "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+      "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+      "license": "MIT AND ISC",
+      "dependencies": {
+        "@types/d3-array": "^3.0.3",
+        "@types/d3-ease": "^3.0.0",
+        "@types/d3-interpolate": "^3.0.1",
+        "@types/d3-scale": "^4.0.2",
+        "@types/d3-shape": "^3.1.0",
+        "@types/d3-time": "^3.0.0",
+        "@types/d3-timer": "^3.0.0",
+        "d3-array": "^3.1.6",
+        "d3-ease": "^3.0.1",
+        "d3-interpolate": "^3.0.1",
+        "d3-scale": "^4.0.2",
+        "d3-shape": "^3.1.0",
+        "d3-time": "^3.0.0",
+        "d3-timer": "^3.0.1"
+      }
+    },
+    "node_modules/vinext": {
+      "version": "1.0.0-beta.5",
+      "resolved": "https://registry.npmjs.org/vinext/-/vinext-1.0.0-beta.5.tgz",
+      "integrity": "sha512-c0I1UB70/pGwD9y4/iXWrGfz/LnowSbkQpUw+lK9t08tes5WHX7Vl0sZAY4MIzGaVCyvrD4/xZo7HWpMje6SwA==",
+      "license": "MIT",
+      "dependencies": {
+        "@unpic/react": "^1.0.2",
+        "@vercel/og": "^0.8.6",
+        "@vinext/types": "^1.0.0-beta.2",
+        "image-size": "2.0.2",
+        "ipaddr.js": "^2.1.0",
+        "magic-string": "^0.30.21",
+        "vite-plugin-commonjs": "^0.10.4",
+        "web-vitals": "^4.2.4"
+      },
+      "bin": {
+        "vinext": "dist/cli.js"
+      },
+      "engines": {
+        "node": ">=22"
+      },
+      "peerDependencies": {
+        "@mdx-js/rollup": "^3.0.0",
+        "@vitejs/plugin-react": "^5.1.4 || ^6.0.0",
+        "@vitejs/plugin-rsc": "^0.5.26",
+        "react": "^19.2.6",
+        "react-dom": "^19.2.6",
+        "react-server-dom-webpack": "^19.2.6",
+        "vite": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@mdx-js/rollup": {
+          "optional": true
+        },
+        "@vitejs/plugin-rsc": {
+          "optional": true
+        },
+        "react-server-dom-webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vinext/node_modules/ipaddr.js": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz",
+      "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/vite": {
+      "version": "8.0.13",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
+      "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.32.0",
+        "picomatch": "^4.0.4",
+        "postcss": "^8.5.14",
+        "rolldown": "1.0.1",
+        "tinyglobby": "^0.2.16"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.1.18",
+        "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
+        }
+      }
+    },
+    "node_modules/vite-plugin-commonjs": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/vite-plugin-commonjs/-/vite-plugin-commonjs-0.10.4.tgz",
+      "integrity": "sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==",
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.12.1",
+        "magic-string": "^0.30.11",
+        "vite-plugin-dynamic-import": "^1.6.0"
+      }
+    },
+    "node_modules/vite-plugin-dynamic-import": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.6.0.tgz",
+      "integrity": "sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg==",
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.12.1",
+        "es-module-lexer": "^1.5.4",
+        "fast-glob": "^3.3.2",
+        "magic-string": "^0.30.11"
+      }
+    },
+    "node_modules/vite-plugin-dynamic-import/node_modules/es-module-lexer": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+      "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+      "license": "MIT"
+    },
+    "node_modules/vite/node_modules/lightningcss": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+      "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "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"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-android-arm64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+      "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-darwin-arm64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+      "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-darwin-x64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+      "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-freebsd-x64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+      "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+      "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+      "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+      "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+      "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+      "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+      "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+      "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/vitefu": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
+      "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
+      "devOptional": true,
+      "license": "MIT",
+      "workspaces": [
+        "tests/deps/*",
+        "tests/projects/*",
+        "tests/projects/workspace/packages/*"
+      ],
+      "peerDependencies": {
+        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/watchpack": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
+      "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "graceful-fs": "^4.1.2"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/web-vitals": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
+      "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/webpack": {
+      "version": "5.110.3",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.3.tgz",
+      "integrity": "sha512-GuizBzRvo9YPpyoNMf3ag7AzxbaW85qrRSqTha345KyJbAFPt3/cMzBM0h+RWg7SK/7DdzRLINP3LvQ0hvr4hg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@types/estree": "^1.0.8",
+        "@types/json-schema": "^7.0.15",
+        "@webassemblyjs/ast": "^1.14.1",
+        "@webassemblyjs/wasm-edit": "^1.14.1",
+        "@webassemblyjs/wasm-parser": "^1.14.1",
+        "acorn": "^8.16.0",
+        "browserslist": "^4.28.1",
+        "chrome-trace-event": "^1.0.2",
+        "enhanced-resolve": "^5.24.4",
+        "es-module-lexer": "^2.1.0",
+        "events": "^3.2.0",
+        "graceful-fs": "^4.2.11",
+        "mime-db": "^1.54.0",
+        "minimizer-webpack-plugin": "^5.7.0",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^4.3.3",
+        "tapable": "^2.3.0",
+        "watchpack": "^2.5.2",
+        "webpack-sources": "^3.5.1"
+      },
+      "bin": {
+        "webpack": "bin/webpack.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependenciesMeta": {
+        "webpack-cli": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-sources": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz",
+      "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+      "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^3.1.1"
+      },
+      "bin": {
+        "node-which": "bin/which.js"
+      },
+      "engines": {
+        "node": "^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/workerd": {
+      "version": "1.20260515.1",
+      "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260515.1.tgz",
+      "integrity": "sha512-MjKOJLcvU45xXedQowvuiHtJTxu4WTHYQeIlF7YmjuqhiI6dImTFxWCEoRQHiskztxuVSNEmdO7/0UfDu6OMnQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "workerd": "bin/workerd"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "optionalDependencies": {
+        "@cloudflare/workerd-darwin-64": "1.20260515.1",
+        "@cloudflare/workerd-darwin-arm64": "1.20260515.1",
+        "@cloudflare/workerd-linux-64": "1.20260515.1",
+        "@cloudflare/workerd-linux-arm64": "1.20260515.1",
+        "@cloudflare/workerd-windows-64": "1.20260515.1"
+      }
+    },
+    "node_modules/wrangler": {
+      "version": "4.92.0",
+      "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.92.0.tgz",
+      "integrity": "sha512-/DKpQHPxkuZbQsO9dFW2700VTD/4DSZMHjy92fO/frNoDRi/zQsFCAd2ONCV6TGqcUoXcP3D8Bo2gj/L4M0qQQ==",
+      "dev": true,
+      "license": "MIT OR Apache-2.0",
+      "dependencies": {
+        "@cloudflare/kv-asset-handler": "0.5.0",
+        "@cloudflare/unenv-preset": "2.16.1",
+        "blake3-wasm": "2.1.5",
+        "esbuild": "0.27.3",
+        "miniflare": "4.20260515.0",
+        "path-to-regexp": "6.3.0",
+        "unenv": "2.0.0-rc.24",
+        "workerd": "1.20260515.1"
+      },
+      "bin": {
+        "wrangler": "bin/wrangler.js",
+        "wrangler2": "bin/wrangler.js"
+      },
+      "engines": {
+        "node": ">=22.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      },
+      "peerDependencies": {
+        "@cloudflare/workers-types": "^4.20260515.1"
+      },
+      "peerDependenciesMeta": {
+        "@cloudflare/workers-types": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/wrangler/node_modules/path-to-regexp": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/ws": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+      "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/wsl-utils": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz",
+      "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-wsl": "^3.1.0",
+        "powershell-utils": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/wsl-utils/node_modules/powershell-utils": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
+      "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "license": "ISC"
+    },
+    "node_modules/yocto-spinner": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.2.tgz",
+      "integrity": "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==",
+      "license": "MIT",
+      "dependencies": {
+        "yoctocolors": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=18.19"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yoctocolors": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz",
+      "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yoga-layout": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
+      "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
+      "license": "MIT"
+    },
+    "node_modules/youch": {
+      "version": "4.1.0-beta.10",
+      "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
+      "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@poppinss/colors": "^4.1.5",
+        "@poppinss/dumper": "^0.6.4",
+        "@speed-highlight/core": "^1.2.7",
+        "cookie": "^1.0.2",
+        "youch-core": "^0.3.3"
+      }
+    },
+    "node_modules/youch-core": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
+      "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@poppinss/exception": "^1.2.2",
+        "error-stack-parser-es": "^1.0.5"
+      }
+    },
+    "node_modules/youch/node_modules/cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/zod": {
+      "version": "3.25.76",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+      "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
+    "node_modules/zod-to-json-schema": {
+      "version": "3.25.2",
+      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+      "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
+      "license": "ISC",
+      "peerDependencies": {
+        "zod": "^3.25.28 || ^4"
+      }
+    }
+  }
+}

+ 55 - 0
package.json

@@ -0,0 +1,55 @@
+{
+  "name": "sites-project",
+  "version": "0.1.0",
+  "private": true,
+  "engines": {
+    "node": ">=22.13.0"
+  },
+  "scripts": {
+    "dev": "vinext dev",
+    "build": "vinext build",
+    "start": "wrangler dev --config dist/server/wrangler.json",
+    "lint": "oxlint",
+    "format": "oxfmt"
+  },
+  "dependencies": {
+    "react": "19.2.6",
+    "react-dom": "19.2.6",
+    "react-server-dom-webpack": "19.2.6",
+    "vinext": "1.0.0-beta.5",
+    "@base-ui/react": "1.7.0",
+    "@shadcn/react": "0.3.0",
+    "class-variance-authority": "0.7.1",
+    "clsx": "2.1.1",
+    "cmdk": "1.1.1",
+    "date-fns": "4.1.0",
+    "embla-carousel-react": "8.5.2",
+    "input-otp": "1.4.2",
+    "lucide-react": "1.31.0",
+    "react-day-picker": "9.8.1",
+    "react-resizable-panels": "4.5.8",
+    "recharts": "3.8.0",
+    "shadcn": "4.18.0",
+    "tailwind-merge": "3.6.0",
+    "tw-animate-css": "1.4.0"
+  },
+  "devDependencies": {
+    "@cloudflare/vite-plugin": "1.37.1",
+    "@cloudflare/workers-types": "4.20260515.1",
+    "@openai/sites-vite-plugin": "0.2.0",
+    "@tailwindcss/postcss": "4.2.1",
+    "@types/node": "22.19.19",
+    "@types/react": "19.2.14",
+    "@types/react-dom": "19.2.3",
+    "@vitejs/plugin-react": "6.0.2",
+    "@vitejs/plugin-rsc": "0.5.26",
+    "oxfmt": "0.61.0",
+    "oxlint": "1.76.0",
+    "oxlint-tsgolint": "7.0.2001",
+    "tailwindcss": "4.2.1",
+    "typescript": "5.9.3",
+    "vite": "8.0.13",
+    "wrangler": "4.92.0"
+  },
+  "type": "module"
+}

BIN
public/downloads/api-lab.zip


+ 1 - 0
public/favicon.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" rx="8" fill="#245b47"/><text x="20" y="28" text-anchor="middle" font-family="monospace" font-weight="600" font-size="26" fill="white">A</text></svg>

+ 30 - 0
tsconfig.json

@@ -0,0 +1,30 @@
+{
+  "compilerOptions": {
+    "target": "ES2017",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "types": ["node", "@cloudflare/workers-types", "vinext/types"],
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "react-jsx",
+    "incremental": true,
+    "paths": {
+      "@/*": ["./*"]
+    }
+  },
+  "include": [
+    "next-env.d.ts",
+    "**/*.ts",
+    "**/*.tsx",
+    ".next/types/**/*.ts",
+    ".next/dev/types/**/*.ts",
+    "**/*.mts"
+  ],
+  "exclude": ["node_modules"]
+}

+ 61 - 0
vite.config.ts

@@ -0,0 +1,61 @@
+import { sites } from '@openai/sites-vite-plugin';
+import tailwindcss from '@tailwindcss/postcss';
+import vinext from 'vinext';
+import { defineConfig } from 'vite';
+import hostingConfig from './.openai/hosting.json';
+
+const SITE_CREATOR_PLACEHOLDER_DATABASE_ID =
+  '00000000-0000-4000-8000-000000000000';
+
+const { d1, r2 } = hostingConfig;
+
+// macOS Seatbelt blocks FSEvents, so Codex previews need polling for HMR.
+const isCodexSeatbeltSandbox = process.env.CODEX_SANDBOX === 'seatbelt';
+
+const localBindingConfig = {
+  main: 'vinext/server/fetch-handler',
+  compatibility_flags: ['nodejs_compat'],
+  d1_databases: d1
+    ? [
+        {
+          binding: d1,
+          database_name: 'site-creator-d1',
+          database_id: SITE_CREATOR_PLACEHOLDER_DATABASE_ID,
+        },
+      ]
+    : [],
+  r2_buckets: r2
+    ? [
+        {
+          binding: r2,
+          bucket_name: 'site-creator-r2',
+        },
+      ]
+    : [],
+};
+
+export default defineConfig(async () => {
+  // Keep Wrangler and Miniflare state project-local. These are non-secret tool
+  // settings; application environment belongs in ignored `.env*` files.
+  process.env.WRANGLER_WRITE_LOGS ??= 'false';
+  process.env.WRANGLER_LOG_PATH ??= '.wrangler/logs';
+  process.env.MINIFLARE_REGISTRY_PATH ??= '.wrangler/registry';
+
+  // Wrangler snapshots its log path while the Cloudflare plugin is imported.
+  const { cloudflare } = await import('@cloudflare/vite-plugin');
+
+  return {
+    css: { postcss: { plugins: [tailwindcss()] } },
+    server: isCodexSeatbeltSandbox
+      ? { watch: { useFsEvents: false, usePolling: true } }
+      : undefined,
+    plugins: [
+      vinext(),
+      sites(),
+      cloudflare({
+        viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] },
+        config: localBindingConfig,
+      }),
+    ],
+  };
+});