diff --git a/frontend/delivery_admin/.env.example b/frontend/delivery_admin/.env.example index 1ee0a4d..f6facdf 100644 --- a/frontend/delivery_admin/.env.example +++ b/frontend/delivery_admin/.env.example @@ -1,3 +1,4 @@ -VITE_API_BASE_URL=http://localhost:12426/heqi/delivery/v1 +# 可选覆盖;开发默认 http://localhost:12426/heqi/delivery/v1,生产构建默认 http://rest.heqiapp.com/heqi/delivery/v1。 +VITE_API_BASE_URL= # 受保护 API 使用 BSM JWT;本地联调时由认证服务签发,不要提交真实令牌。 VITE_DEV_JWT= diff --git a/frontend/delivery_admin/README.md b/frontend/delivery_admin/README.md index 170405e..a80ee91 100644 --- a/frontend/delivery_admin/README.md +++ b/frontend/delivery_admin/README.md @@ -1,6 +1,6 @@ # 和气配送点管理系统 -配送点管理端使用 Vue 3、TypeScript、Vite、Pinia 和 Arco Design,开发端口为 `5176`,默认 API 为 `http://localhost:12426/heqi/delivery/v1`。 +配送点管理端使用 Vue 3、TypeScript、Vite、Pinia 和 Arco Design,开发端口为 `5176`。开发默认 API 为 `http://localhost:12426/heqi/delivery/v1`,生产构建默认 API 为 `http://rest.heqiapp.com/heqi/delivery/v1`。 ```bash pnpm install diff --git a/frontend/delivery_admin/config/vite.config.ts b/frontend/delivery_admin/config/vite.config.ts index f779c18..59788da 100644 --- a/frontend/delivery_admin/config/vite.config.ts +++ b/frontend/delivery_admin/config/vite.config.ts @@ -5,7 +5,7 @@ import { resolve } from 'path'; import visualizer from 'rollup-plugin-visualizer'; import { ArcoResolver } from 'unplugin-vue-components/resolvers'; import Components from 'unplugin-vue-components/vite'; -import { defineConfig, type PluginOption } from 'vite'; +import { defineConfig, loadEnv, type PluginOption } from 'vite'; import compressPlugin from 'vite-plugin-compression'; import svgLoader from 'vite-svg-loader'; @@ -29,6 +29,10 @@ function manualChunks(id: string) { export default defineConfig(({ command, mode }) => { const isDev = command === 'serve'; const isReport = mode === 'report'; + const env = loadEnv(mode, process.cwd(), ''); + const apiBaseURL = + env.VITE_API_BASE_URL || + `${isDev ? 'http://localhost:12426' : 'http://rest.heqiapp.com'}/heqi/delivery/v1`; const plugins: PluginOption[] = [ vue(), @@ -60,6 +64,9 @@ export default defineConfig(({ command, mode }) => { } return { + define: { + 'import.meta.env.VITE_API_BASE_URL': JSON.stringify(apiBaseURL), + }, plugins, resolve: { alias: [ diff --git a/frontend/gas_admin/.env.example b/frontend/gas_admin/.env.example index ab83e84..22eccbe 100644 --- a/frontend/gas_admin/.env.example +++ b/frontend/gas_admin/.env.example @@ -1,3 +1,4 @@ -VITE_API_BASE_URL=http://localhost:12426/heqi/gas/v1 +# 可选覆盖;开发默认 http://localhost:12426/heqi/gas/v1,生产构建默认 http://rest.heqiapp.com/heqi/gas/v1。 +VITE_API_BASE_URL= # 受保护 API 使用 BSM JWT;本地联调时由认证服务签发,不要提交真实令牌。 VITE_DEV_JWT= diff --git a/frontend/gas_admin/README.md b/frontend/gas_admin/README.md index f55b34f..e986c4d 100644 --- a/frontend/gas_admin/README.md +++ b/frontend/gas_admin/README.md @@ -23,5 +23,5 @@ pnpm build | 变量 | 说明 | | --- | --- | -| `VITE_API_BASE_URL` | 后端 API 地址,默认 `http://localhost:12426/heqi/gas/v1` | +| `VITE_API_BASE_URL` | 可选覆盖后端 API 地址;开发默认 `http://localhost:12426/heqi/gas/v1`,生产构建默认 `http://rest.heqiapp.com/heqi/gas/v1` | | `VITE_ERROR_REPORT_URL` | 可选的前端错误上报地址 | diff --git a/frontend/gas_admin/config/vite.config.ts b/frontend/gas_admin/config/vite.config.ts index 447faa4..74dec60 100644 --- a/frontend/gas_admin/config/vite.config.ts +++ b/frontend/gas_admin/config/vite.config.ts @@ -5,7 +5,7 @@ import { resolve } from 'path'; import visualizer from 'rollup-plugin-visualizer'; import { ArcoResolver } from 'unplugin-vue-components/resolvers'; import Components from 'unplugin-vue-components/vite'; -import { defineConfig, type PluginOption } from 'vite'; +import { defineConfig, loadEnv, type PluginOption } from 'vite'; import compressPlugin from 'vite-plugin-compression'; import svgLoader from 'vite-svg-loader'; @@ -29,6 +29,10 @@ function manualChunks(id: string) { export default defineConfig(({ command, mode }) => { const isDev = command === 'serve'; const isReport = mode === 'report'; + const env = loadEnv(mode, process.cwd(), ''); + const apiBaseURL = + env.VITE_API_BASE_URL || + `${isDev ? 'http://localhost:12426' : 'http://rest.heqiapp.com'}/heqi/gas/v1`; const plugins: PluginOption[] = [ vue(), @@ -60,6 +64,9 @@ export default defineConfig(({ command, mode }) => { } return { + define: { + 'import.meta.env.VITE_API_BASE_URL': JSON.stringify(apiBaseURL), + }, plugins, resolve: { alias: [ diff --git a/frontend/platform_admin/.env.example b/frontend/platform_admin/.env.example index 6697367..44686e9 100644 --- a/frontend/platform_admin/.env.example +++ b/frontend/platform_admin/.env.example @@ -1,3 +1,4 @@ -VITE_API_BASE_URL=http://localhost:12426/heqi/v1 +# 可选覆盖;开发默认 http://localhost:12426/heqi/platform/v1,生产构建默认 http://rest.heqiapp.com/heqi/platform/v1。 +VITE_API_BASE_URL= # 受保护 API 使用 BSM JWT;本地联调时由认证服务签发,不要提交真实令牌。 VITE_DEV_JWT= diff --git a/frontend/platform_admin/README.md b/frontend/platform_admin/README.md index d72355e..6cf9705 100644 --- a/frontend/platform_admin/README.md +++ b/frontend/platform_admin/README.md @@ -36,5 +36,5 @@ pnpm build | 变量 | 说明 | | --- | --- | -| `VITE_API_BASE_URL` | 后端 API 地址,默认 `http://localhost:12426/heqi/platform/v1` | +| `VITE_API_BASE_URL` | 可选覆盖后端 API 地址;开发默认 `http://localhost:12426/heqi/platform/v1`,生产构建默认 `http://rest.heqiapp.com/heqi/platform/v1` | | `VITE_ERROR_REPORT_URL` | 可选的前端错误上报地址 | diff --git a/frontend/platform_admin/config/vite.config.ts b/frontend/platform_admin/config/vite.config.ts index 0fc6655..e50399f 100644 --- a/frontend/platform_admin/config/vite.config.ts +++ b/frontend/platform_admin/config/vite.config.ts @@ -5,7 +5,7 @@ import { resolve } from 'path'; import visualizer from 'rollup-plugin-visualizer'; import { ArcoResolver } from 'unplugin-vue-components/resolvers'; import Components from 'unplugin-vue-components/vite'; -import { defineConfig, type PluginOption } from 'vite'; +import { defineConfig, loadEnv, type PluginOption } from 'vite'; import compressPlugin from 'vite-plugin-compression'; import svgLoader from 'vite-svg-loader'; @@ -29,6 +29,10 @@ function manualChunks(id: string) { export default defineConfig(({ command, mode }) => { const isDev = command === 'serve'; const isReport = mode === 'report'; + const env = loadEnv(mode, process.cwd(), ''); + const apiBaseURL = + env.VITE_API_BASE_URL || + `${isDev ? 'http://localhost:12426' : 'http://rest.heqiapp.com'}/heqi/platform/v1`; const plugins: PluginOption[] = [ vue(), @@ -60,6 +64,9 @@ export default defineConfig(({ command, mode }) => { } return { + define: { + 'import.meta.env.VITE_API_BASE_URL': JSON.stringify(apiBaseURL), + }, plugins, resolve: { alias: [ diff --git a/frontend/site/AGENTS.md b/frontend/site/AGENTS.md index dafbf3c..e5e590f 100644 --- a/frontend/site/AGENTS.md +++ b/frontend/site/AGENTS.md @@ -11,6 +11,8 @@ Build app UI in `src/`. Keep `.openai/hosting.json`, `worker/index.js`, `scripts ## 和气门户设计约束 - 本项目是品牌化管理入口门户,不承载统一登录、运营数据或业务编辑。 +- 门户使用常规三行布局:头部展示 Logo、名称与口号,中间为广告语与滚动图 Banner,底部为三个管理系统入口。 +- Banner 保持紧凑,不占满页面剩余高度;三张轮播图分别对应平台、气站和配送场景。 - 桌面端优先一屏完成,移动端允许自然纵向滚动。 - 视觉语言延续三套管理系统登录页的半写实等距插画,并使用平台蓝、气站橙、配送青绿区分入口。 - 三个入口始终在新标签页打开,分别指向 `p.heqiapp.com`、`z.heqiapp.com` 与 `d.heqiapp.com`。 diff --git a/frontend/site/public/assets/banner-delivery.png b/frontend/site/public/assets/banner-delivery.png new file mode 100644 index 0000000..c2586b4 Binary files /dev/null and b/frontend/site/public/assets/banner-delivery.png differ diff --git a/frontend/site/public/assets/banner-gas-station.png b/frontend/site/public/assets/banner-gas-station.png new file mode 100644 index 0000000..291a930 Binary files /dev/null and b/frontend/site/public/assets/banner-gas-station.png differ diff --git a/frontend/site/public/assets/banner-platform.png b/frontend/site/public/assets/banner-platform.png new file mode 100644 index 0000000..09a2557 Binary files /dev/null and b/frontend/site/public/assets/banner-platform.png differ diff --git a/frontend/site/public/assets/heqi-ecosystem-hero.jpg b/frontend/site/public/assets/heqi-ecosystem-hero.jpg deleted file mode 100644 index ea9177c..0000000 Binary files a/frontend/site/public/assets/heqi-ecosystem-hero.jpg and /dev/null differ diff --git a/frontend/site/src/App.jsx b/frontend/site/src/App.jsx index af56a29..840a410 100644 --- a/frontend/site/src/App.jsx +++ b/frontend/site/src/App.jsx @@ -1,9 +1,34 @@ +import { useEffect, useState } from "react"; import { ArrowRight, ArrowSquareOut, ShieldCheck, } from "@phosphor-icons/react"; +const slides = [ + { + eyebrow: "平台协同", + title: "一屏统筹,安全连接每一程", + description: "连接智能瓶阀、气站运营与配送履约,让治理更清晰、协同更高效。", + image: "/assets/banner-platform.png", + position: "center center", + }, + { + eyebrow: "智慧气站", + title: "让每一次供气更稳、更安心", + description: "以数字化能力贯通站点经营、安全服务与履约管理。", + image: "/assets/banner-gas-station.png", + position: "center center", + }, + { + eyebrow: "高效配送", + title: "从调度到送达,全程清晰可追溯", + description: "智能连接配送资源与服务现场,让每一程都有迹可循。", + image: "/assets/banner-delivery.png", + position: "center center", + }, +]; + const portals = [ { number: "01", @@ -37,64 +62,104 @@ function PortalLink({ portal }) { rel="noreferrer" aria-label={`进入${portal.title},将在新标签页打开`} > -
- -
+ + + {portal.title} + {portal.description} + 进入系统 - ); } export function App() { + const [activeSlide, setActiveSlide] = useState(0); + + useEffect(() => { + const timer = window.setInterval(() => { + setActiveSlide((current) => (current + 1) % slides.length); + }, 5000); + + return () => window.clearInterval(timer); + }, []); + return (
- -
- 和气 + 和气 -
-
-
-

HEQI SMART GAS PLATFORM

-

安全连接每一程

-

- 智能瓶阀、气站运营与配送履约 - 的一体化管理入口 -

+
+
+ {slides.map((slide, index) => ( +
+ +
+
+

{slide.eyebrow}

+

{slide.title}

+ {slide.description} +
+
+ ))} +
+ +
+ {slides.map((slide, index) => ( +
- +
); } diff --git a/frontend/site/src/styles.css b/frontend/site/src/styles.css index 3c0088e..04ffe65 100644 --- a/frontend/site/src/styles.css +++ b/frontend/site/src/styles.css @@ -4,15 +4,12 @@ font-family: "Noto Sans SC Variable", "PingFang SC", "Microsoft YaHei", sans-serif; color: #202936; - background: #f7faff; + background: #f4f7fb; font-synthesis: none; text-rendering: optimizeLegibility; --platform: #1265e8; - --platform-deep: #0c4fbd; --station: #dc7100; - --station-deep: #b85a00; - --delivery: #068f8b; - --delivery-deep: #08746f; + --delivery: #078e89; } * { @@ -21,54 +18,42 @@ html { min-width: 320px; - background: #f7faff; + background: #f4f7fb; } body { - margin: 0; min-width: 320px; min-height: 100vh; + margin: 0; color: #202936; - background: #f7faff; + background: #f4f7fb; -webkit-font-smoothing: antialiased; } -#root { - min-height: 100vh; +button, +a { + font: inherit; } -a { - color: inherit; +#root, +.site-shell { + min-height: 100vh; } .site-shell { - position: relative; - isolation: isolate; - min-height: 100vh; - min-height: 100dvh; - padding: 38px clamp(28px, 4.6vw, 72px) 30px; - overflow: hidden; - background: #f7faff; -} - -.ecosystem-scene { - position: absolute; - inset: 108px 0 0; - z-index: -1; - width: 100%; - height: calc(100% - 108px); - object-fit: cover; - object-position: center bottom; - user-select: none; - pointer-events: none; + display: grid; + grid-template-rows: auto clamp(230px, 32vh, 300px) auto; + gap: clamp(18px, 2.4vh, 28px); + width: min(100%, 1440px); + margin: 0 auto; + padding: clamp(22px, 3.2vh, 38px) clamp(24px, 4.5vw, 68px) + clamp(24px, 3.4vh, 40px); } .site-header { display: flex; align-items: center; justify-content: space-between; - width: min(100%, 1320px); - margin: 0 auto; } .brand { @@ -76,401 +61,397 @@ a { gap: 12px; align-items: center; color: #17212d; - font-weight: 650; - font-size: 27px; - line-height: 1; - letter-spacing: 0.04em; text-decoration: none; } .brand img { - width: 43px; - height: 43px; + width: 44px; + height: 44px; } -.trust-line { - display: inline-flex; - gap: 9px; +.brand__name { + font-weight: 700; + font-size: 29px; + line-height: 1; + letter-spacing: 0.08em; +} + +.brand-slogan { + display: grid; + grid-template-columns: auto auto; + gap: 2px 9px; align-items: center; - color: #687586; - font-size: 14px; - line-height: 22px; - letter-spacing: 0.02em; + color: #27394e; + text-align: right; } -.hero { +.brand-slogan svg { + grid-row: 1 / 3; + color: var(--platform); +} + +.brand-slogan strong { + font-size: 15px; + line-height: 21px; +} + +.brand-slogan span { + color: #7b8796; + font-size: 11px; + line-height: 17px; + letter-spacing: 0.04em; +} + +.banner { + position: relative; + min-height: 230px; + overflow: hidden; + background: #dfe8f4; + border: 1px solid rgba(103, 126, 154, 0.14); + border-radius: 22px; + box-shadow: 0 22px 60px rgba(44, 66, 92, 0.12); +} + +.banner__track { + display: flex; + height: 100%; + transition: transform 650ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.banner__slide { + position: relative; + min-width: 100%; + min-height: 230px; + overflow: hidden; +} + +.banner__slide img, +.banner__shade { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.banner__slide img { + object-fit: cover; + transform: scale(1.01); +} + +.banner__shade { + background: + linear-gradient(90deg, rgba(12, 27, 47, 0.74) 0%, rgba(14, 36, 65, 0.42) 40%, rgba(17, 40, 65, 0.04) 72%), + linear-gradient(0deg, rgba(8, 27, 48, 0.16), transparent 55%); +} + +.banner__copy { position: relative; z-index: 1; - width: min(100%, 900px); - margin: clamp(34px, 4.5vh, 46px) auto 0; - text-align: center; + display: flex; + width: min(54%, 650px); + height: 100%; + min-height: 230px; + flex-direction: column; + justify-content: center; + padding: clamp(34px, 5vw, 74px); + color: #fff; } -.hero__eyebrow { - margin: 0 0 12px; - color: #6d7f95; - font-weight: 620; +.banner__copy p { + margin: 0 0 11px; + color: #bcd8ff; + font-weight: 650; + font-size: 13px; + line-height: 20px; + letter-spacing: 0.16em; +} + +.banner__copy h1 { + margin: 0; + font-weight: 700; + font-size: clamp(30px, 3.2vw, 46px); + line-height: 1.22; + letter-spacing: 0.025em; + text-wrap: balance; +} + +.banner__copy span { + max-width: 520px; + margin-top: 17px; + color: rgba(255, 255, 255, 0.82); + font-size: clamp(14px, 1.25vw, 17px); + line-height: 1.8; +} + +.banner__dots { + position: absolute; + bottom: 24px; + left: clamp(34px, 5vw, 74px); + z-index: 2; + display: flex; + gap: 8px; +} + +.banner__dots button { + width: 24px; + height: 4px; + padding: 0; + cursor: pointer; + background: rgba(255, 255, 255, 0.46); + border: 0; + border-radius: 999px; + transition: + width 180ms ease, + background-color 180ms ease; +} + +.banner__dots button.is-active { + width: 48px; + background: #fff; +} + +.banner__dots button:focus-visible { + outline: 3px solid rgba(255, 255, 255, 0.55); + outline-offset: 4px; +} + +.site-footer { + display: grid; + gap: 14px; +} + +.footer-heading { + display: flex; + align-items: end; + justify-content: space-between; +} + +.footer-heading p, +.footer-heading h2 { + margin: 0; +} + +.footer-heading p { + color: #8793a3; + font-weight: 650; + font-size: 10px; + line-height: 16px; + letter-spacing: 0.17em; +} + +.footer-heading h2 { + margin-top: 2px; + color: #253244; + font-weight: 680; + font-size: 20px; + line-height: 30px; +} + +.footer-heading > span { + display: inline-flex; + gap: 7px; + align-items: center; + color: #7d8998; font-size: 12px; line-height: 20px; - letter-spacing: 0.2em; -} - -.hero h1 { - margin: 0; - color: #202936; - font-weight: 680; - font-size: clamp(48px, 4.2vw, 66px); - line-height: 1.16; - letter-spacing: 0.035em; -} - -.hero__description { - margin: 18px auto 0; - color: #637083; - font-size: clamp(16px, 1.35vw, 19px); - line-height: 30px; - letter-spacing: 0.025em; -} - -.hero__description span { - display: inline; } .portal-grid { - position: relative; - z-index: 2; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: clamp(30px, 4vw, 64px); - width: min(100%, 1280px); - margin: clamp(48px, 6.5vh, 66px) auto 0; + gap: 14px; } .portal { --accent: var(--platform); - --accent-deep: var(--platform-deep); - display: flex; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 15px; + align-items: center; min-width: 0; - flex-direction: column; - gap: 24px; - padding: 8px 10px 12px; - background: rgba(255, 255, 255, 0.62); + min-height: 92px; + padding: 17px 18px; + color: inherit; + background: #fff; + border: 1px solid rgba(93, 117, 145, 0.14); border-radius: 14px; + box-shadow: 0 9px 24px rgba(48, 68, 91, 0.06); text-decoration: none; outline: none; transition: - background-color 180ms ease, + border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease; } .portal--station { --accent: var(--station); - --accent-deep: var(--station-deep); } .portal--delivery { --accent: var(--delivery); - --accent-deep: var(--delivery-deep); } -.portal:hover { - background: rgba(255, 255, 255, 0.82); - box-shadow: 0 18px 46px rgba(37, 58, 84, 0.12); - transform: translateY(-4px); +.portal:hover, +.portal:focus-visible { + border-color: color-mix(in srgb, var(--accent) 38%, transparent); + box-shadow: 0 14px 34px color-mix(in srgb, var(--accent) 13%, transparent); + transform: translateY(-3px); } .portal:focus-visible { - background: rgba(255, 255, 255, 0.9); box-shadow: - 0 0 0 4px rgba(18, 101, 232, 0.18), - 0 18px 46px rgba(37, 58, 84, 0.12); -} - -.portal__heading { - display: grid; - grid-template-columns: auto 1px minmax(0, 1fr); - gap: 20px; - align-items: center; - min-height: 68px; + 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent), + 0 14px 34px color-mix(in srgb, var(--accent) 13%, transparent); } .portal__number { color: var(--accent); - font-weight: 660; - font-size: clamp(46px, 4vw, 60px); + font-weight: 700; + font-size: 28px; line-height: 1; - letter-spacing: -0.035em; } -.portal__divider { - width: 1px; - height: 54px; - background: color-mix(in srgb, var(--accent) 35%, transparent); +.portal__content { + display: grid; + min-width: 0; + gap: 4px; } -.portal h2 { - margin: 0; - color: #202936; - font-weight: 650; - font-size: clamp(20px, 1.7vw, 25px); - line-height: 1.35; +.portal__content strong { + color: #253244; + font-size: 17px; + line-height: 24px; } -.portal p { - margin: 7px 0 0; - color: #6b7787; - font-size: 13px; - line-height: 21px; +.portal__content small { + overflow: hidden; + color: #7b8796; + font-size: 11px; + line-height: 18px; + text-overflow: ellipsis; white-space: nowrap; } .portal__action { display: inline-flex; - width: min(100%, 194px); - min-height: 50px; - gap: 18px; + gap: 8px; align-items: center; - justify-content: center; - margin-left: 78px; - color: #fff; - background: var(--accent); - border-radius: 9px; + color: var(--accent); font-weight: 600; - font-size: 15px; - line-height: 22px; - box-shadow: 0 9px 22px color-mix(in srgb, var(--accent) 24%, transparent); - transition: background-color 180ms ease; + font-size: 13px; + white-space: nowrap; } -.portal:hover .portal__action, -.portal:focus-visible .portal__action { - background: var(--accent-deep); -} - -.external-note { - position: absolute; - right: clamp(28px, 4.6vw, 72px); - bottom: 24px; - z-index: 2; - display: inline-flex; - gap: 7px; - align-items: center; - margin: 0; - color: rgba(60, 76, 94, 0.72); - font-size: 12px; - line-height: 20px; -} - -@media (max-width: 1020px) { +@media (max-width: 980px) { .site-shell { - padding-inline: 28px; + grid-template-rows: auto clamp(240px, 34vh, 310px) auto; } - .portal-grid { - gap: 20px; + .banner__copy { + width: 64%; } .portal { - padding-inline: 4px; - } - - .portal__heading { - gap: 13px; - } - - .portal__number { - font-size: 44px; - } - - .portal h2 { - font-size: 19px; - } - - .portal p { - font-size: 12px; + grid-template-columns: auto minmax(0, 1fr); } .portal__action { - margin-left: 58px; + display: none; } } -@media (max-width: 760px) { +@media (max-width: 720px) { .site-shell { display: flex; min-height: 100dvh; flex-direction: column; - padding: 24px 18px 0; - overflow: hidden; - background: #f7faff; - } - - .ecosystem-scene { - position: relative; - inset: auto; - order: 5; - width: calc(100% + 36px); - height: auto; - margin: 34px -18px 0; - object-fit: contain; - } - - .brand { - gap: 9px; - font-size: 22px; + gap: 18px; + padding: 20px 16px 24px; } .brand img { - width: 36px; - height: 36px; + width: 38px; + height: 38px; } - .trust-line { + .brand__name { + font-size: 23px; + } + + .brand-slogan { + display: flex; + gap: 7px; + } + + .brand-slogan strong { + font-size: 13px; + } + + .brand-slogan span { display: none; } - .hero { - margin-top: 48px; + .banner, + .banner__slide, + .banner__copy { + min-height: 310px; } - .hero__eyebrow { - margin-bottom: 9px; - font-size: 10px; - letter-spacing: 0.15em; + .banner { + border-radius: 17px; } - .hero h1 { - font-size: clamp(37px, 11vw, 48px); - line-height: 1.18; + .banner__slide img { + object-position: center center !important; } - .hero__description { - max-width: 330px; - margin-top: 14px; - font-size: 15px; - line-height: 25px; + .banner__shade { + background: linear-gradient(0deg, rgba(10, 28, 49, 0.82) 0%, rgba(13, 34, 58, 0.32) 72%, rgba(13, 34, 58, 0.08) 100%); } - .hero__description span { - display: block; + .banner__copy { + width: 100%; + justify-content: end; + padding: 36px 26px 62px; + } + + .banner__copy h1 { + font-size: clamp(29px, 9vw, 38px); + } + + .banner__copy span { + font-size: 14px; + } + + .banner__dots { + bottom: 25px; + left: 26px; + } + + .footer-heading > span { + display: none; } .portal-grid { grid-template-columns: 1fr; - gap: 12px; - margin-top: 38px; + gap: 10px; } .portal { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 16px; - align-items: center; - padding: 15px; - background: rgba(255, 255, 255, 0.84); - border: 1px solid rgba(104, 132, 164, 0.14); - border-radius: 12px; - box-shadow: 0 10px 28px rgba(35, 60, 88, 0.08); + min-height: 82px; + padding: 15px 17px; } .portal:hover { transform: none; } - - .portal__heading { - grid-template-columns: auto 1px minmax(0, 1fr); - gap: 12px; - min-height: auto; - } - - .portal__number { - font-size: 32px; - } - - .portal__divider { - height: 44px; - } - - .portal h2 { - font-size: 17px; - } - - .portal p { - margin-top: 4px; - overflow: hidden; - font-size: 11px; - line-height: 18px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .portal__action { - width: 46px; - min-height: 46px; - margin: 0; - border-radius: 50%; - font-size: 0; - } - - .portal__action svg { - width: 20px; - height: 20px; - } - - .external-note { - position: static; - order: 4; - justify-content: center; - margin: 18px 0 0; - font-size: 11px; - } -} - -@media (max-width: 380px) { - .site-shell { - padding-inline: 14px; - } - - .ecosystem-scene { - width: calc(100% + 28px); - margin-inline: -14px; - } - - .portal { - padding-inline: 12px; - } - - .portal__heading { - gap: 9px; - } - - .portal__number { - font-size: 29px; - } - - .portal h2 { - font-size: 16px; - } -} - -@media (max-height: 790px) and (min-width: 761px) { - .site-shell { - padding-top: 26px; - } - - .hero { - margin-top: 38px; - } - - .portal-grid { - margin-top: 40px; - } } @media (prefers-reduced-motion: reduce) { - .portal, - .portal__action { + .banner__track, + .banner__dots button, + .portal { transition: none; } } diff --git a/ui/.gitignore b/ui/.gitignore deleted file mode 100644 index 7c0264b..0000000 --- a/ui/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -dist/ -test-results/ -playwright-report/ -.design-qa/ diff --git a/ui/.npmrc b/ui/.npmrc deleted file mode 100644 index 1223b5c..0000000 --- a/ui/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -fund=false -audit=false diff --git a/ui/.openai/hosting.json b/ui/.openai/hosting.json deleted file mode 100644 index 47c28cb..0000000 --- a/ui/.openai/hosting.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "d1": null, - "r2": null -} diff --git a/ui/AGENTS.md b/ui/AGENTS.md deleted file mode 100644 index 1c335ed..0000000 --- a/ui/AGENTS.md +++ /dev/null @@ -1,71 +0,0 @@ -# Mobile Prototype Agent Guide - -## Prototype Instructions - -In ChatGPT Work Mode, run `sites-preview start "$PWD"`, open `http://terminal.local:4173/` in the cloud browser, and verify the rendered app and its primary interactions. Keep that preview open and tell the user to inspect it in the cloud browser; do not present the local URL as a user-facing chat link. In Codex Desktop, run the local server yourself, open the preview in the in-app browser, and provide the clickable local URL. Do not deploy to Sites unless the user explicitly asks to share, publish, or deploy. Do not give the user server-start instructions when you can run it. - -Before planning or implementing any mobile-app change, read this `AGENTS.md` in full. It is the source of truth for the template's runtime and component guidance. - -Before making substantial visual changes, use the Product Design plugin's `get-context` skill when the visual source is unclear or no longer matches the current goal. When the user gives durable prototype-specific design feedback, preferences, or decisions, record them in `AGENTS.md`. - -When implementing from a selected generated mock, treat that image as the source of truth for layout, component anatomy, density, spacing, color, typography, visible content, and hierarchy. - -## Editing Boundary - -- Build app-specific UI in `src/Prototype.tsx` and `src/prototype.css`. -- Treat `src/App.tsx`, `src/main.tsx`, `src/styles.css`, `src/mobile/`, `public/assets/iphone/`, `public/assets/android/`, `public/assets/status/`, `vite.config.ts`, `worker/index.js`, and `scripts/prepare-sites-build.mjs` as protected runtime files. Do not edit, replace, remove, or recreate them unless the user explicitly asks to change the mobile runtime itself. For an explicit runtime change, update the affected lock hashes only after verifying the new runtime behavior. -- Run `npm run check:runtime` before preview or handoff. If it fails, restore the protected runtime instead of weakening or bypassing the check. -- `npm run build` preserves the mobile runtime and prepares the static Cloudflare Worker output required by Sites. Before a Sites handoff, confirm `dist/client/index.html`, `dist/server/index.js`, `dist/.openai/hosting.json`, and source `.openai/hosting.json` exist, then run `npm run test:sites`. Do not replace this project with a Vinext starter. - -## Runtime Contract - -- Preserve the mobile device runtime unless the user's task explicitly asks otherwise. Do not replace it with a standalone page. Visual fidelity applies to app-owned content inside the device screen, not to template-owned device chrome. -- Keep `App` composed around `PhoneFrame` -> `KeyboardProvider`, with `StatusBar`, app content, `HomeIndicator`, and `KeyboardDock` mounted inside the phone frame. `StatusBar` and the iOS home indicator are overlaid device chrome. When the Android keyboard is closed, the app viewport reserves the protected navigation-bar region instead of painting behind it. When the Android keyboard is open, preserve the current full-screen keyboard layout: its asset includes the IME navigation strip and the separate black navigation bar is hidden. iOS screens continue to paint behind the home-indicator area and own their safe-area content padding. -- Preserve the `iPhone` / `Pixel 10` device picker and both calibrated device presets. The Pixel screen is `427 x 952`; its `32 x 32` camera circle and `public/assets/android/navigation-bar.svg` bottom navigation bar are protected device chrome, not app content. -- Preserve the device picker's intentionally lightweight Codex styling in the top-right corner: its trigger wrapper is borderless and transparent, its trigger sizes to content, and its right-aligned menu uses the compact 3px inset plus the specified hairline and elevation shadow layers. Keep the prototype root and default app screen white. -- Preserve `StatusBar` as live device chrome, including its platform-specific typography, source status-icon assets, and spacing. Pixel 10 uses Roboto, Android indicators, and 32px top, left, and right padding. iPhone uses its iOS indicators, system typography, and calibrated spacing. Do not hardcode screenshot times like `9:41` into the status bar, replace its real-time clock, or move status bar content into app markup unless the user explicitly asks for a fixed/mock device time. -- `PhoneFrame` owns the calibrated device frame, screen portal, device picker, camera cutout, and custom cursor. Keep device assets in `public/assets/iphone/` and `public/assets/android/`; if an asset fails to load, repair the asset path or restore the asset instead of removing the frame, keyboard, or image render. -- Use `MobileScroll` directly for simple single-screen prototypes. Use `FlowStack` for conventional multi-screen flows whose routes can own their fixed header and footer; when using it, define each route as a `FlowScreen`: `{ id, header?, headerHeight?, footer?, footerHeight?, render }`, and use `flow.push(screen)`, `flow.pop()`, and `flow.replace(screen)` from `FlowStack` render callbacks or `useFlow()` instead of introducing another router. -- Use `Carousel` for a carousel, horizontal rail, swipeable cards, image or media strip, horizontally scrollable cards, chip rail, or other horizontal collection. -- For a layered app shell—such as a persistent composer, independently presented sheet, pushed/peek sidebar, or app-wide transition—compose directly in `Prototype.tsx` rather than forcing it through `FlowStack`. Keep app-owned fixed chrome as sibling layers outside `MobileScroll`. -- When using `FlowScreen`, put route-owned fixed headers or footers in `FlowScreen.header` or `FlowScreen.footer`. Set `headerHeight` to the visible app-toolbar height; `FlowStack` adds the device's top safe-area/status-bar inset automatically. Do not include `StatusBar` or its height in the header. Set `footerHeight` to the full app-footer height. `FlowScreen.footer` is an overlay, not reserved layout space; screens using it must add their own bottom content padding such as `padding-bottom: calc(var(--flow-footer-height) + var(--mobile-safe-area-height) + 24px)` so final content can scroll above the footer while still painting behind it. -- Render only scrollable content inside `MobileScroll`; it is for content that should move with scroll and rubber-band overscroll. Keep app-owned headers, nav bars, tabs, composers, and overlays outside it. This keeps scroll physics, safe areas, keyboard insets, scrollbars, and drag click suppression active without letting content paint under fixed chrome. -- Buttons, links, cards, and images inside `MobileScroll` should still allow drag scrolling when the pointer moves beyond tap slop. Use `data-scroll-drag="ignore"` only for rare controls that must own the drag gesture themselves. -- Do not add `var(--keyboard-height)` to ordinary screen/content padding inside `MobileScroll`; the scroll viewport already shrinks above the simulated keyboard. For custom fixed composers, search bars, or toast chrome, use `useKeyboardInsets().bottomInset`. It is relative to the app viewport: Android returns `0` while the closed-keyboard viewport already reserves navigation, then returns the keyboard height while open; iOS continues to clear the home indicator while closed and ride directly above the keyboard while open. Do not pin custom bottom chrome to `bottom: 0` or only `keyboardHeight`. -- Use `KeyboardInput`, `KeyboardTextarea`, or `MobileTextField` for every text-entry control. A raw `input` or `textarea` disconnects focus, keyboard animation, safe-area insets, and attached surfaces. -- Use `BottomSheet` for phone-scoped sheets. Its props are `open`, `onOpenChange`, `title`, optional `description`, optional `snap`, and `children`; it renders through the phone screen portal and dismisses the keyboard before opening. - -## Horizontal Carousels - -- Use `Carousel` for horizontally draggable cards, images, media, chips, or other horizontal collections. Do not recreate these with `overflow-x`, custom pointer handlers, or a generic div. -- `Carousel` can be nested directly inside `MobileScroll`. It owns horizontal gestures and automatically yields vertical gestures to the parent. -- Never put `data-scroll-drag="ignore"` on or around a `Carousel`; doing so prevents vertical parent scrolling when a gesture begins inside it. -- Do not add CSS scroll snapping to `Carousel`; its runtime owns momentum and release motion. -- Use `data-scroll-drag="ignore"` only when a control must prevent parent scrolling in every drag direction. - -See `src/mobile/COMPONENTS.md` for the full component and gesture contract. - -## Keyboard Rule - -The simulated keyboard is a separate top-layer component. Before presenting anything that behaves like iOS navigation or modal UI, dismiss it first. - -Call `keyboard.hide()` before: - -- pushing, popping, or replacing FlowStack routes -- opening bottom sheets, action sheets, dialogs, menus, or navigation sheets -- starting transitions where the destination should not inherit text-input focus - -`FlowStack` already hides the keyboard for `push`, `pop`, and `replace`. `BottomSheet` already hides it before opening. If you add new modal/sheet/navigation primitives, follow the same rule. - -When a composer, search surface, or other keyboard-attached component closes, call `keyboard.hide()` in the same event before changing that component's open state. Position attached surfaces from `useKeyboardInsets()` rather than a separate timer or visibility flag so both dismiss together. - -When any text-entry control loses focus, dismiss the simulated keyboard. If the control is custom or does not use the runtime's keyboard-aware fields, handle its blur event and call `keyboard.hide()` explicitly. Keep the keyboard open only when focus is moving directly to another text-entry control that should share the same keyboard session. - -## Interaction Rules - -- Do not trigger buttons or inputs after a pointer has become a drag. Preserve the drag suppression behavior in `MobileScroll`. -- Do not allow native browser image/file dragging inside the phone frame. Preserve the phone-level `dragstart` suppression and non-draggable image styles so scroll drags that begin on images still scroll the prototype. -- Use `KeyboardInput`, `KeyboardTextarea`, or `MobileTextField` for text entry so the simulated keyboard and safe-area insets stay connected. -- Fixed phone chrome should not animate with pushed screens. Screen content can animate; the status bar, camera cutout, and preview chrome should stay put. -- Keep the keyboard below the home indicator/safe area layer in z-index, and above ordinary app UI while visible. -- Keep the home indicator as the topmost safe-area layer in the z-index above everything else in the prototype. diff --git a/ui/README.md b/ui/README.md deleted file mode 100644 index a5013bf..0000000 --- a/ui/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# 瓶安芯移动端 UI 原型 - -本目录是依据 `doc/*产品设计` 原型图制作的 iOS/Android 交互原型,用于确认信息架构、视觉语言和关键流程,不是生产 Flutter 工程,也不连接真实 API、IoT、支付、定位或对象存储。 - -## 预览入口 - -启动本地预览: - -```bash -npm install -npm run dev -- --host 127.0.0.1 --port 4173 -``` - -入口参数: - -| 入口 | 查询参数 | 主要内容 | -| --- | --- | --- | -| 用户端 | `/?app=user` | 瓶阀控制、商城、订单、个人中心 | -| 配送员 | `/?app=service&role=delivery` | 配送任务、到场、气瓶扫描、随瓶安检、签收 | -| 安装维修员 | `/?app=service&role=installer` | 使用条件、备料、安装、测试、安检、用户确认 | -| 安检员 | `/?app=service&role=inspector` | 现场检查、取证、风险等级、隐患闭环 | - -右上角设备选择器可切换 iPhone 与 Pixel 10,用于检查 iOS/Android 安全区、底部系统区域和键盘行为。 - -## 原型边界 - -- 所有数据均为演示数据,按钮只模拟界面状态,不表示后端业务已实现。 -- 设备命令先进入“等待回执”,不能用请求成功冒充设备执行成功。 -- 工作人员角色切换只改变当前工作上下文;生产实现必须由服务端重新校验岗位、组织、资质、培训、上班状态和服务区域。 -- 现场照片、签名、定位和弱网补传在原型中只展示交互,生产实现须遵循 `docs/04-服务端App需求.md` 的 Flutter 开发说明。 -- 生产 Flutter 工程按仓库规划分别放入 `apps/user_app` 和 `apps/service_app`;在明确进入实现阶段前,不从本原型复制出空壳工程。 - -## 视觉基线 - -- 用户端:安全蓝为主色,瓶阀状态是首页第一层级,危险与待确认状态必须同时使用图标和文字。 -- 服务端:紫色为统一作业框架,安检关键动作使用安全绿色;配送、安装维修、安检共享组件但不共享任务状态机。 -- 中文界面最小正文建议不低于 12sp,触控目标不小于 44×44 logical pixels;金额、时间、单位和更新时间必须明确。 -- 图标使用统一图标库,禁止使用 emoji、字符画或自行绘制的业务图标替代正式资产。 diff --git a/ui/design-qa.md b/ui/design-qa.md deleted file mode 100644 index 3f993f4..0000000 --- a/ui/design-qa.md +++ /dev/null @@ -1,42 +0,0 @@ -# Design QA - -final result: passed - -## Comparison target - -- User source: `doc/用户端APP-产品设计/4.jpg` -- Delivery source: `doc/服务端APP-配送端-产品设计/f777dcf4d618a0f9664d126b32f73682.jpg` -- Installation source: `doc/服务端APP-安装端-产品设计/202c904481a03ff644a8ac0c0d1c9ae4.jpg` -- Inspection source: `doc/服务端APP-安全检查端-产品设计/ff7edcd4f78721beb5f0953ab08c23c4.jpg` -- Implementation: local `/ui` mobile prototype at the matching `app` and `role` query states. -- Reference pixels: 1344×2772. Each reference was proportionally normalized onto a 393×852 comparison canvas. -- Implementation viewport: 393×852 CSS pixels at device scale factor 1, captured from the app-owned phone screen. -- Device-frame, status-bar, home-indicator and Android system navigation differences were excluded because they are template-owned runtime chrome. - -## Pass 1 - -- P2 — User valve hero used a dark navy surface while the source uses a light neutral control panel with a dominant red action. Fixed by changing the panel to a light neutral surface, enlarging the valve control and moving the automatic-close status into a light inset strip. -- P1 — Pixel 10 could retain an open simulated keyboard after a device change during review. Fixed by dismissing the runtime keyboard whenever the selected device changes. -- P3 — The original user screen centers the page title in a mini-program header; the implementation uses a native-app leading title and live iOS/Android status bar. Kept as a deliberate native-shell adaptation. - -## Pass 2 - -- User valve screen: hierarchy, status density, red control emphasis, light panel, bottom navigation and safety feedback match the source direction. -- Delivery screen: purple task shell, schedule context, user/address facts, prominent primary action and fixed four-tab navigation match the source. -- Installation screen: purple workbench, time/shift state, urgent work item, task card and role-specific action match the source. -- Inspection screen: green safety identity, task list, time/shift state and inspection action match the source. -- iPhone and Pixel 10: no content overflow, fixed navigation obstruction, open keyboard, browser console error or missing primary action remained. -- No actionable P0, P1 or P2 differences remained. - -## Interaction checks - -- Valve control: confirmation sheet → waiting for device receipt → confirmed open state. -- User navigation: valve, shop, favorites, orders and profile tabs are reachable. -- Service work: task list → task detail → next required step. -- Role switch: delivery, installation/repair and inspection reload the role-specific workbench. -- Evidence sheet: photo type selection and simulated capture close correctly. - -## Remaining P3 notes - -- Source screenshots contain mini-program chrome and older iconography; the prototype intentionally uses the protected native device runtime and a single Phosphor icon family. -- Mock content is consolidated from multiple source screens, so exact task numbers and timestamps vary while layout, hierarchy and workflow remain aligned. diff --git a/ui/index.html b/ui/index.html deleted file mode 100644 index 3c7dd31..0000000 --- a/ui/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - 瓶安芯移动端 UI 原型 - - -
- - - diff --git a/ui/mobile-runtime.lock.json b/ui/mobile-runtime.lock.json deleted file mode 100644 index 383d553..0000000 --- a/ui/mobile-runtime.lock.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "scripts/check-mobile-runtime.mjs": "ee85738cc2ef889c0b457d8ccac61e017b8c5d16f393e766ec9fb7d11c9aff7f", - "scripts/prepare-sites-build.mjs": "b6a6adaa4fab3234676116dd1c9cb6611275ab9d92dd26f5bf402393e3744bf6", - "scripts/update-mobile-runtime-lock.mjs": "b4e1aa41133f19ba8bb32685b3ec05798b1ceabaf72c328419d545aa60fe1f0b", - "vite.config.ts": "da36eba05b845c3b0d69d823a515f6f7d9612992126ed2fe32c2809815ec73ba", - "src/App.tsx": "7054b4e14304ab33153d3a2e860aa43d2a4f69e8a69630b4a37a2cbcd4ad2f16", - "src/main.tsx": "01b7049715148ab645637905ccbbc91fc3f87659198a309541c2d1f03bb537d0", - "src/styles.css": "844c67b27cf5e38ba395153145d058c929c6479199d245cd046f3646badd719d", - "src/mobile/BottomSheet.tsx": "f9c376dc78b8b5feb1c8752aa23414993ab66013d11e81b2ebb8342c570032ac", - "src/mobile/Carousel.tsx": "242f4a7488e18c35e9b43651c9d5877288655a6f9f1734e03e56108f17526bf7", - "src/mobile/Device.tsx": "867de9c65220f2ac7202f895d2b10a64c747f211a4b22cd8f81a1a1d66e52fc2", - "src/mobile/FlowStack.tsx": "1e85744c5f1c1138dd5e8b7b27fbb6c05139d5aac93d94c45084a018eddd4ebc", - "src/mobile/Keyboard.tsx": "862587264375499846385ce817ea1b813db8f118140cdb6da163f989a6946cac", - "src/mobile/MobileCursor.tsx": "be2353205e05d823bc23ecae88179d712781ee9ede8fa317057c194c7c23ee7d", - "src/mobile/MobileRuntime.tsx": "1ffe80a9582460005d66f10fc73deaff08b81431785a506887cc0d8320be747f", - "src/mobile/MobileScroll.tsx": "ea3e3691f587ea13753162b9e64c1981263e5b9beddebea21a6a1551dce76430", - "src/mobile/PhoneFrame.tsx": "73015590b468acbb4b58e3f6fe5eef99c5f68f67722caffd8f00d271ba00a91d", - "src/mobile/assets.ts": "d40cb8e5390fd2b54618266e7694e40349006dfe480f65d60559316ca8c9c234", - "src/mobile/components.tsx": "20b02f6c6de4cfd08aa6a49bf9bc261c0ea000864b37e1001afdcd892afe4bc8", - "src/mobile/geometry.ts": "f1382e5d6cc9adfd7ac142304f2f9a5a2330db1d74b3f0848adbaf311676bf16", - "src/mobile/index.ts": "ee69d9341571db5dee84dd2d7452a62eef09085a84a85f198a3edc48047e0412", - "public/assets/iphone/Bezel.png": "ee2514d74d2f75f3541405fa2f20811f159879a66586778cfd8c9c84c44959df", - "public/assets/iphone/Keyboard.png": "292e2d83d69adaeb663d6a840ce8db8d0a5257710b472c20713e03f56b08ea88", - "public/assets/android/Pixel10.png": "a87c1250ba20756f5d020b5351a7ae71d663dcfc64aca3a1cd56452e8a05233e", - "public/assets/android/Keyboard.png": "6085e50be17ca2705b4351283187be2c5b01b58262c70b90d74b3376efb4adfe", - "public/assets/android/navigation-bar.svg": "077a782a3b6c2915fc518dfb4bba825c8c5861b2572f9522a7024902b9cb2850", - "public/assets/status/status-icons.svg": "1a8bd4207cf34532ae514bdcdca693acc9a2b8ebc9d3182dc163c3e9ddcc4823", - "public/assets/status/ios-status-icons.svg": "40e218b274d0973d519b50f42884e6e05ef0f2b6a0e46e102fda54218784466d", - "worker/index.js": "2dd0615a445143933d88d4271f54f5d63ee951421fcd08c5a7617bb09c564389" -} diff --git a/ui/package-lock.json b/ui/package-lock.json deleted file mode 100644 index 3df913b..0000000 --- a/ui/package-lock.json +++ /dev/null @@ -1,2295 +0,0 @@ -{ - "name": "ui", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ui", - "version": "0.1.0", - "dependencies": { - "@fontsource/roboto": "5.2.10", - "@phosphor-icons/react": "^2.1.10", - "@radix-ui/react-dialog": "1.1.19", - "@radix-ui/react-dropdown-menu": "2.1.20", - "@radix-ui/react-icons": "1.3.2", - "@use-gesture/react": "10.3.1", - "motion": "12.42.2", - "react": "19.2.7", - "react-dom": "19.2.7" - }, - "devDependencies": { - "@playwright/test": "1.61.1", - "@types/react": "19.2.17", - "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "6.0.3", - "typescript": "7.0.2", - "vite": "8.1.3" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@fontsource/roboto": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.10.tgz", - "integrity": "sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@phosphor-icons/react": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", - "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">= 16.8", - "react-dom": ">= 16.8" - } - }, - "node_modules/@playwright/test": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", - "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.61.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.5.tgz", - "integrity": "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.11.tgz", - "integrity": "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.7" - }, - "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-collection": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.12.tgz", - "integrity": "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0" - }, - "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-compose-refs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", - "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", - "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.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz", - "integrity": "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==", - "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.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.19.tgz", - "integrity": "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "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-direction": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", - "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", - "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-dismissable-layer": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.15.tgz", - "integrity": "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-effect-event": "0.0.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-dropdown-menu": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.20.tgz", - "integrity": "sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.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-focus-guards": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", - "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", - "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.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.12.tgz", - "integrity": "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.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-icons": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", - "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", - "license": "MIT", - "peerDependencies": { - "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", - "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "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-menu": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.20.tgz", - "integrity": "sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", - "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-popper": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.3.tgz", - "integrity": "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-rect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/rect": "1.1.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-portal": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.13.tgz", - "integrity": "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-layout-effect": "1.1.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-presence": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.7.tgz", - "integrity": "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.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-primitive": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", - "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.3.0" - }, - "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-roving-focus": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.15.tgz", - "integrity": "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.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-slot": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", - "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3" - }, - "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.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", - "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", - "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.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", - "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "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.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", - "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "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-is-hydrated": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", - "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", - "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-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", - "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-rect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", - "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.2" - }, - "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-size": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", - "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "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/rect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", - "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", - "license": "MIT" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], - "dev": true, - "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==", - "dev": true, - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "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/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@use-gesture/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", - "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", - "license": "MIT" - }, - "node_modules/@use-gesture/react": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", - "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", - "license": "MIT", - "dependencies": { - "@use-gesture/core": "10.3.1" - }, - "peerDependencies": { - "react": ">= 16.8.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "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/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/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/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "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/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/framer-motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", - "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.42.2", - "motion-utils": "^12.39.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "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/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "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.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "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/motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", - "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", - "license": "MIT", - "dependencies": { - "framer-motion": "^12.42.2", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/motion-dom": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", - "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.39.0" - } - }, - "node_modules/motion-utils": { - "version": "12.39.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", - "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, - "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/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/playwright": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", - "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.61.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", - "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", - "dev": true, - "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.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, - "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-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/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.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.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, - "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/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==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "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/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/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc" - }, - "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" - } - }, - "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/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", - "tinyglobby": "^0.2.17" - }, - "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.3.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - } - } -} diff --git a/ui/package.json b/ui/package.json deleted file mode 100644 index e9ae68a..0000000 --- a/ui/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "ui", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "check:runtime": "node scripts/check-mobile-runtime.mjs", - "update:runtime-lock": "node scripts/update-mobile-runtime-lock.mjs", - "test:runtime": "playwright test", - "test:sites": "node --test tests/sites-worker.test.mjs", - "predev": "npm run check:runtime", - "dev": "vite", - "prebuild": "npm run check:runtime", - "build": "tsc && vite build && node scripts/prepare-sites-build.mjs" - }, - "dependencies": { - "@fontsource/roboto": "5.2.10", - "@phosphor-icons/react": "^2.1.10", - "@radix-ui/react-dialog": "1.1.19", - "@radix-ui/react-dropdown-menu": "2.1.20", - "@radix-ui/react-icons": "1.3.2", - "@use-gesture/react": "10.3.1", - "motion": "12.42.2", - "react": "19.2.7", - "react-dom": "19.2.7" - }, - "devDependencies": { - "@playwright/test": "1.61.1", - "@types/react": "19.2.17", - "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "6.0.3", - "typescript": "7.0.2", - "vite": "8.1.3" - } -} diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts deleted file mode 100644 index 04d4c8b..0000000 --- a/ui/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "@playwright/test"; - -const testPort = Number(process.env.MOBILE_RUNTIME_TEST_PORT ?? 4174); - -export default defineConfig({ - testDir: "./tests", - testMatch: "**/*.spec.ts", - timeout: 20_000, - use: { - baseURL: `http://127.0.0.1:${testPort}`, - viewport: { width: 1100, height: 1100 }, - }, - webServer: { - command: `npm run dev -- --port ${testPort}`, - url: `http://127.0.0.1:${testPort}/tests/runtime-fixture.html`, - reuseExistingServer: process.env.MOBILE_RUNTIME_TEST_PORT == null, - }, -}); diff --git a/ui/public/assets/android/Keyboard.png b/ui/public/assets/android/Keyboard.png deleted file mode 100644 index bf959fa..0000000 Binary files a/ui/public/assets/android/Keyboard.png and /dev/null differ diff --git a/ui/public/assets/android/Pixel10.png b/ui/public/assets/android/Pixel10.png deleted file mode 100644 index 2ba4a10..0000000 Binary files a/ui/public/assets/android/Pixel10.png and /dev/null differ diff --git a/ui/public/assets/android/navigation-bar.svg b/ui/public/assets/android/navigation-bar.svg deleted file mode 100644 index 9e79543..0000000 --- a/ui/public/assets/android/navigation-bar.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/ui/public/assets/iphone/Bezel.png b/ui/public/assets/iphone/Bezel.png deleted file mode 100644 index 759e8aa..0000000 Binary files a/ui/public/assets/iphone/Bezel.png and /dev/null differ diff --git a/ui/public/assets/iphone/Keyboard.png b/ui/public/assets/iphone/Keyboard.png deleted file mode 100644 index 80207c3..0000000 Binary files a/ui/public/assets/iphone/Keyboard.png and /dev/null differ diff --git a/ui/public/assets/status/ios-status-icons.svg b/ui/public/assets/status/ios-status-icons.svg deleted file mode 100644 index b75235a..0000000 --- a/ui/public/assets/status/ios-status-icons.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/ui/public/assets/status/status-icons.svg b/ui/public/assets/status/status-icons.svg deleted file mode 100644 index 8a97e81..0000000 --- a/ui/public/assets/status/status-icons.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/ui/scripts/check-mobile-runtime.mjs b/ui/scripts/check-mobile-runtime.mjs deleted file mode 100644 index 72bf53d..0000000 --- a/ui/scripts/check-mobile-runtime.mjs +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node -import { createHash } from "node:crypto"; -import { existsSync, readFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const lockPath = path.join(root, "mobile-runtime.lock.json"); -const lockedFiles = JSON.parse(readFileSync(lockPath, "utf8")); -const failures = []; - -for (const [relativePath, expectedHash] of Object.entries(lockedFiles)) { - const filePath = path.join(root, relativePath); - - if (!existsSync(filePath)) { - failures.push(`${relativePath} is missing`); - continue; - } - - const actualHash = createHash("sha256").update(readFileSync(filePath)).digest("hex"); - if (actualHash !== expectedHash) { - failures.push(`${relativePath} was modified`); - } -} - -if (failures.length > 0) { - console.error("Mobile runtime integrity check failed:\n"); - for (const failure of failures) console.error(`- ${failure}`); - console.error("\nRestore the protected runtime. Put app UI in src/Prototype.tsx and src/prototype.css."); - process.exit(1); -} - -console.log(`Mobile runtime integrity check passed (${Object.keys(lockedFiles).length} protected files).`); diff --git a/ui/scripts/prepare-sites-build.mjs b/ui/scripts/prepare-sites-build.mjs deleted file mode 100644 index 86a9e8c..0000000 --- a/ui/scripts/prepare-sites-build.mjs +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env node -import { copyFileSync, existsSync, mkdirSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const dist = path.join(root, "dist"); -const index = path.join(dist, "client", "index.html"); -const worker = path.join(root, "worker", "index.js"); -const hosting = path.join(root, ".openai", "hosting.json"); - -for (const file of [index, worker, hosting]) { - if (!existsSync(file)) throw new Error("Missing Sites build input: " + file); -} - -mkdirSync(path.join(dist, "server"), { recursive: true }); -mkdirSync(path.join(dist, ".openai"), { recursive: true }); -copyFileSync(worker, path.join(dist, "server", "index.js")); -copyFileSync(hosting, path.join(dist, ".openai", "hosting.json")); - -console.log("Prepared Sites build: dist/server/index.js and dist/.openai/hosting.json"); diff --git a/ui/scripts/update-mobile-runtime-lock.mjs b/ui/scripts/update-mobile-runtime-lock.mjs deleted file mode 100644 index 7d91275..0000000 --- a/ui/scripts/update-mobile-runtime-lock.mjs +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env node -import { createHash } from "node:crypto"; -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const lockPath = path.join(root, "mobile-runtime.lock.json"); -const protectedFiles = [ - "scripts/check-mobile-runtime.mjs", - "scripts/prepare-sites-build.mjs", - "scripts/update-mobile-runtime-lock.mjs", - "vite.config.ts", - "src/App.tsx", - "src/main.tsx", - "src/styles.css", - "src/mobile/BottomSheet.tsx", - "src/mobile/Carousel.tsx", - "src/mobile/Device.tsx", - "src/mobile/FlowStack.tsx", - "src/mobile/Keyboard.tsx", - "src/mobile/MobileCursor.tsx", - "src/mobile/MobileRuntime.tsx", - "src/mobile/MobileScroll.tsx", - "src/mobile/PhoneFrame.tsx", - "src/mobile/assets.ts", - "src/mobile/components.tsx", - "src/mobile/geometry.ts", - "src/mobile/index.ts", - "public/assets/iphone/Bezel.png", - "public/assets/iphone/Keyboard.png", - "public/assets/android/Pixel10.png", - "public/assets/android/Keyboard.png", - "public/assets/android/navigation-bar.svg", - "public/assets/status/status-icons.svg", - "public/assets/status/ios-status-icons.svg", - "worker/index.js", -]; - -const hashes = {}; -for (const relativePath of protectedFiles) { - const filePath = path.join(root, relativePath); - if (!existsSync(filePath)) throw new Error(`Protected runtime file is missing: ${relativePath}`); - hashes[relativePath] = createHash("sha256").update(readFileSync(filePath)).digest("hex"); -} - -writeFileSync(lockPath, `${JSON.stringify(hashes, null, 2)}\n`); -console.log(`Updated mobile-runtime.lock.json (${protectedFiles.length} protected files).`); diff --git a/ui/src/App.tsx b/ui/src/App.tsx deleted file mode 100644 index fe4bedd..0000000 --- a/ui/src/App.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { MobileRuntime } from "./mobile"; -import Prototype from "./Prototype"; - -export default function App() { - return ( - - - - ); -} diff --git a/ui/src/Prototype.tsx b/ui/src/Prototype.tsx deleted file mode 100644 index b30cf4e..0000000 --- a/ui/src/Prototype.tsx +++ /dev/null @@ -1,728 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { - ArrowLeft, - Bank, - BatteryHigh, - Bell, - Bluetooth, - Buildings, - CalendarCheck, - Camera, - CaretRight, - ChartBar, - CheckCircle, - ClipboardText, - Clock, - CloudSlash, - Fire, - FirstAid, - Gauge, - GearSix, - HardHat, - Heart, - HouseLine, - Lifebuoy, - ListChecks, - MagnifyingGlass, - MapPin, - NavigationArrow, - Package, - PhoneCall, - Plus, - Power, - QrCode, - Receipt, - ShieldCheck, - ShoppingCart, - Signature, - Storefront, - ThermometerSimple, - Truck, - User, - UserCircle, - Wallet, - WarningCircle, - WarningDiamond, - WifiHigh, - Wrench, - XCircle, -} from "@phosphor-icons/react"; -import { BottomSheet, MobileScroll, useKeyboard, useMobileDevice } from "./mobile"; - -type UserTab = "valve" | "shop" | "saved" | "orders" | "profile"; -type ServiceTab = "tasks" | "records" | "hazards" | "profile"; -type ServiceRole = "delivery" | "installer" | "inspector"; -type ValveState = "closed" | "pending" | "open"; - -const roleMeta: Record< - ServiceRole, - { - label: string; - title: string; - icon: typeof Truck; - accent: string; - taskLabel: string; - taskId: string; - taskType: string; - address: string; - steps: string[]; - checklist: string[]; - } -> = { - delivery: { - label: "配送员", - title: "配送工作台", - icon: Truck, - accent: "#7c3aed", - taskLabel: "待配送", - taskId: "D001", - taskType: "气瓶配送与空瓶回收", - address: "武侯区红牌楼街道袖海巷 5 栋", - steps: ["订单确认", "导航到场", "扫描气瓶", "随瓶安检", "收款签收"], - checklist: ["核对配送规格与数量", "蓝牙或扫码录入气瓶", "完成随瓶安检照片", "确认线上支付结果"], - }, - installer: { - label: "安装维修员", - title: "安装维修工作台", - icon: Wrench, - accent: "#7c3aed", - taskLabel: "待处理", - taskId: "W001", - taskType: "智能瓶阀安装", - address: "武侯区红牌楼街道袖海巷 5 栋", - steps: ["使用条件", "准备材料", "执行安装", "测试安装", "前期安检", "用户确认"], - checklist: ["通风与安装位置合规", "管道与连接件已备齐", "测漏与压力测试通过", "设备回执与用户签字完整"], - }, - inspector: { - label: "安检员", - title: "安全检查工作台", - icon: ShieldCheck, - accent: "#16a34a", - taskLabel: "待检查", - taskId: "S004", - taskType: "常规入户安检", - address: "武侯区红牌楼街道袖海巷 4 栋", - steps: ["任务详情", "现场执行", "安全检查", "结果确认"], - checklist: ["燃气具与报警器状态", "智能瓶阀与管道连接", "通风及易燃物环境", "照片水印与用户签字"], - }, -}; - -function IconBadge({ - children, - tone = "blue", -}: { - children: React.ReactNode; - tone?: "blue" | "green" | "orange" | "purple" | "red"; -}) { - return {children}; -} - -function StatusPill({ - children, - tone = "success", -}: { - children: React.ReactNode; - tone?: "success" | "warning" | "danger" | "info" | "neutral"; -}) { - return {children}; -} - -function MetricCard({ - icon, - label, - value, - tone = "blue", -}: { - icon: React.ReactNode; - label: string; - value: string; - tone?: "blue" | "green" | "orange" | "purple"; -}) { - return ( -
-
- {icon} - {label} -
- {value} -
- ); -} - -const userNav: Array<{ id: UserTab; label: string; icon: typeof Wrench }> = [ - { id: "valve", label: "角阀控制", icon: Wrench }, - { id: "shop", label: "商城", icon: ShoppingCart }, - { id: "saved", label: "收藏", icon: Heart }, - { id: "orders", label: "订单", icon: ClipboardText }, - { id: "profile", label: "我的", icon: User }, -]; - -function UserValveScreen({ - valveState, - onControl, - onRepair, -}: { - valveState: ValveState; - onControl: () => void; - onRepair: () => void; -}) { - const isPending = valveState === "pending"; - const isOpen = valveState === "open"; - - return ( -
-
-
- 智能瓶阀 -

角阀控制

-
- -
- -
-
设备在线
- 更新于 16:28:02 -
- -
- } label="电池电量" value="85%" tone="green" /> - } label="环境压力" value="101.3 kPa" /> - } label="温度" value="24℃" tone="orange" /> - } label="气瓶余量" value="15 kg" tone="purple" /> -
- -
-
- 厨房 · 主设备 -

{isPending ? "等待设备回执" : isOpen ? "瓶阀已开启" : "瓶阀已关闭"}

-

{isPending ? "命令已发送,请勿重复操作" : isOpen ? "安全检查正常,可随时关阀" : "当前处于安全关闭状态"}

-
- -
- 自动关闭 - 今日 22:00 -
-
- -
-
-
- 安全监测 -

设备状态正常

-
- 无未处理告警 -
-
-
近期未检测到燃气泄漏
-
报警器和瓶阀通信正常
-
设备最近安检已通过
-
-
- -
-
- 紧急服务 -

发现异常?立即处理

-

报修将自动附带设备、地址和采集时间

-
- -
- - -
-
-
- ); -} - -function UserShopScreen() { - const [category, setCategory] = useState("全部"); - const categories = ["全部", "燃气灶具", "热水器", "软管"]; - return ( -
-
-
安全商城

燃气商城

- -
- -
- {categories.map((item) => ( - - ))} -
-
- {[ - { name: "嵌入式燃气灶", price: "¥1,299", note: "一级能效", icon: Fire }, - { name: "智能恒温热水器", price: "¥899", note: "熄火保护", icon: ThermometerSimple }, - { name: "不锈钢燃气软管", price: "¥89", note: "防鼠咬", icon: Wrench }, - { name: "家用燃气报警器", price: "¥199", note: "联动关阀", icon: WarningCircle }, - ].map((product) => { - const ProductIcon = product.icon; - return ( -
- -
-

{product.name}

-

{product.note} · 安装可选

-
{product.price}
-
- ); - })} -
-
- ); -} - -function UserOrdersScreen() { - return ( -
-
-
履约进度

我的订单

- -
-
- -
-
-
- 订单 1785159018310配送中 -
-
- -

用气瓶(50kg×1,15kg×1)

预约:今天 17:30–20:00

- ¥1,000 -
-
-
订单已确认16:31
-
配送员已接单16:42
-
配送中预计 18:20 送达
-
- -
-
-
订单 178505012501已完成
-
- -

智能瓶阀安装服务

已完成 · 2026/07/25

- ¥299 -
-
-
- ); -} - -function UserProfileScreen() { - return ( -
-
-
个人中心

我的

- -
-
-
-

用户6078

139****5078 · 四川省成都市

- -
-
-
账户余额¥21,901
- -
-
- {[ - [Receipt, "我的记录", "订单、押金、报修"], - [ChartBar, "用气统计", "月度与年度"], - [PhoneCall, "紧急联系人", "1 位联系人"], - [ShieldCheck, "安全与隐私", "协议与授权"], - [Lifebuoy, "服务与帮助", "客服、知识库"], - ].map(([Icon, label, note]) => ( - - ))} -
-
- ); -} - -function UserApp() { - const [tab, setTab] = useState("valve"); - const [valveState, setValveState] = useState("closed"); - const [sheet, setSheet] = useState<"valve" | "repair" | null>(null); - - const confirmValve = () => { - setSheet(null); - setValveState("pending"); - window.setTimeout(() => setValveState((current) => (current === "pending" ? "open" : current)), 1400); - }; - - const content = tab === "valve" - ? setSheet("valve")} onRepair={() => setSheet("repair")} /> - : tab === "shop" || tab === "saved" - ? - : tab === "orders" - ? - : ; - - return ( -
- {content} - - - setSheet(open ? "valve" : null)} - title={valveState === "open" ? "确认关闭瓶阀" : "确认开启瓶阀"} - description="平台校验通过后会下发命令,执行结果以设备回执为准。" - snap={0.46} - > -
- -
安全条件正常无高风险事件,设备在线,最近安检合格
-
-
- - -
-
- - setSheet(open ? "repair" : null)} - title="一键报修" - description="请选择最接近的故障类型,现场信息将在提交前再次确认。" - snap={0.62} - > -
- - - - -
-
当前地址成都市武侯区红牌楼街道
- -
-
- ); -} - -const serviceNav: Array<{ id: ServiceTab; label: string; icon: typeof ClipboardText }> = [ - { id: "tasks", label: "任务", icon: ClipboardText }, - { id: "records", label: "记录", icon: Clock }, - { id: "hazards", label: "隐患", icon: WarningDiamond }, - { id: "profile", label: "我的", icon: User }, -]; - -function ServiceTaskList({ - role, - onOpenTask, - onOpenRole, -}: { - role: ServiceRole; - onOpenTask: () => void; - onOpenRole: () => void; -}) { - const meta = roleMeta[role]; - const RoleIcon = meta.icon; - return ( -
-
-
- 瓶安芯服务 -

{meta.title}

-
- -
- -
-
当前时间16:28:07工作中 · 服务区域正常
- 已上班 - -
- -
- -
今日安全培训已完成角色题库 v2026.07 · 5/5 正确
- -
- -
-
-
今日任务

{meta.taskLabel} 3

- -
-
-
- -
{meta.taskId}

{meta.taskType}

- {meta.taskLabel} -
-
张三 · 138****1234
-

{meta.address}

-
-
预约时间今天 17:30
-
优先级紧急
-
预计收入¥180
-
- -
-
-
- -
{role === "delivery" ? "R002" : role === "installer" ? "W002" : "S002"}

{role === "delivery" ? "空瓶回收" : role === "installer" ? "阀门维修" : "专项安全检查"}

- 14:00 -
-

武侯区簇桥街道 12 号

-
-
- -
弱网保护已开启现场材料将加密暂存,恢复后按采集时间补传
-
- ); -} - -function ServiceTaskDetail({ - role, - step, - onBack, - onNext, - onEvidence, -}: { - role: ServiceRole; - step: number; - onBack: () => void; - onNext: () => void; - onEvidence: () => void; -}) { - const meta = roleMeta[role]; - const done = step >= meta.steps.length; - return ( -
-
- -
工单详情{meta.taskId}
- {done ? "待确认" : "执行中"} -
-
- {meta.steps.map((label, index) => ( -
- {index < step ? : index + 1} - {label} -
- ))} -
- - {done ? ( -
- - 现场步骤已完成 -

等待用户或服务端确认

-

当前仅代表材料已提交,最终完成状态以服务端校验结果为准。

- -
- ) : ( - <> -
-
本步骤

{meta.steps[step]}

必填
-
-
任务类型{meta.taskType}
-
服务对象张三
-
服务地址{meta.address}
-
-
- -
-
现场清单

完成前置检查

- {meta.checklist.map((item, index) => ( - - ))} -
- -
-
现场取证照片将写入地址、采集时间与任务号0/6
- -
- -
-
用户签字确认签名与本次检查结论绑定
- -
- - {role === "inspector" && step >= 2 ? ( -
-
安检结论

风险等级

-
-
- ) : null} - - - - )} -
- ); -} - -function ServiceRecords({ role }: { role: ServiceRole }) { - const meta = roleMeta[role]; - return ( -
-
作业留痕

服务记录

-
} label="本月完成" value="18 单" tone="green" />} label="及时完成率" value="96%" tone="purple" />
-
- {[1, 2, 3].map((item) => ( - - ))} -
-
- ); -} - -function ServiceHazards() { - return ( -
-
安全闭环

隐患排查

-
2 项隐患待处理最近更新时间 16:20
- {[ - { id: "H001", title: "管道老化", status: "处理中", tone: "warning" as const }, - { id: "H002", title: "阀门泄漏", status: "待复检", tone: "danger" as const }, - ].map((item) => ( -
-
{item.id}{item.status}
-

{item.title}

武侯区红牌楼街道

- -
- ))} -
- ); -} - -function ServiceProfile({ role, onOpenRole }: { role: ServiceRole; onOpenRole: () => void }) { - const meta = roleMeta[role]; - return ( -
-
个人中心

我的工作台

-
-
-

李四

工号 1023 · 双华公司

-
- -
-
可提现余额¥588.00
-
本月收入¥382本年收入¥588待结算¥180
-
-
- {[[Wallet, "账户管理"], [Bank, "银行卡"], [Buildings, "资质与单位"], [GearSix, "设置"], [Lifebuoy, "帮助中心"]].map(([Icon, label]) => ( - - ))} -
-
- ); -} - -function ServiceApp({ initialRole }: { initialRole: ServiceRole }) { - const [role, setRole] = useState(initialRole); - const [tab, setTab] = useState("tasks"); - const [detail, setDetail] = useState(false); - const [step, setStep] = useState(0); - const [sheet, setSheet] = useState<"role" | "evidence" | null>(null); - - const chooseRole = (nextRole: ServiceRole) => { - setRole(nextRole); - setDetail(false); - setStep(0); - setTab("tasks"); - setSheet(null); - }; - - let content: React.ReactNode; - if (detail) { - content = setDetail(false)} onNext={() => setStep((value) => value + 1)} onEvidence={() => setSheet("evidence")} />; - } else if (tab === "tasks") { - content = { setStep(0); setDetail(true); }} onOpenRole={() => setSheet("role")} />; - } else if (tab === "records") { - content = ; - } else if (tab === "hazards") { - content = ; - } else { - content = setSheet("role")} />; - } - - return ( -
- {content} - - - setSheet(open ? "role" : null)} title="切换服务角色" description="角色切换会重新加载组织、服务区域、资质和待办。" snap={0.52}> -
- {(Object.keys(roleMeta) as ServiceRole[]).map((item) => { - const itemMeta = roleMeta[item]; - const RoleIcon = itemMeta.icon; - return ; - })} -
-
- - setSheet(open ? "evidence" : null)} title="现场取证" description="采集的原始时间、定位、任务号和完整性标记将一并保存。" snap={0.58}> -
选择照片类型后拍摄支持 1–6 张照片,不能用补传时间覆盖采集时间
-
-
-
-
- ); -} - -export default function Prototype() { - const params = useMemo(() => new URLSearchParams(window.location.search), []); - const keyboard = useKeyboard(); - const { device } = useMobileDevice(); - const app = params.get("app") === "service" ? "service" : "user"; - const roleParam = params.get("role"); - const role: ServiceRole = roleParam === "installer" || roleParam === "inspector" ? roleParam : "delivery"; - - useEffect(() => { - keyboard.hide(); - }, [device.id]); - - return app === "service" ? : ; -} diff --git a/ui/src/main.tsx b/ui/src/main.tsx deleted file mode 100644 index 9ff4f46..0000000 --- a/ui/src/main.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import "@fontsource/roboto/latin-500.css"; -import App from "./App"; -import "./styles.css"; -import "./prototype.css"; - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); diff --git a/ui/src/mobile/BottomSheet.tsx b/ui/src/mobile/BottomSheet.tsx deleted file mode 100644 index bd9252e..0000000 --- a/ui/src/mobile/BottomSheet.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { type PropsWithChildren, useEffect, useState } from "react"; -import * as Dialog from "@radix-ui/react-dialog"; -import { useDrag } from "@use-gesture/react"; -import { AnimatePresence, motion } from "motion/react"; -import { useKeyboard, useKeyboardInsets } from "./Keyboard"; -import { useScreenPortal } from "./PhoneFrame"; -import { useMobileDevice } from "./Device"; - -type BottomSheetProps = PropsWithChildren<{ - open: boolean; - onOpenChange: (open: boolean) => void; - title: string; - description?: string; - snap?: number; -}>; - -export function BottomSheet({ - open, - onOpenChange, - title, - description, - snap = 0.72, - children, -}: BottomSheetProps) { - const { device } = useMobileDevice(); - const { screenRef } = useScreenPortal(); - const keyboard = useKeyboard(); - const { keyboardHeight } = useKeyboardInsets(); - const [dragY, setDragY] = useState(0); - - useEffect(() => { - if (open) keyboard.hide(); - }, [open]); - - const handleOpenChange = (nextOpen: boolean) => { - if (nextOpen) { - keyboard.hide(); - } - - onOpenChange(nextOpen); - }; - - const bindDrag = useDrag( - (state) => { - const [, movementY] = state.movement; - const [, velocityY] = state.velocity; - const [, directionY] = state.direction; - const nextY = Math.max(0, movementY); - - if (!state.last) { - setDragY(nextY); - return; - } - - const shouldClose = nextY > 96 || (velocityY > 0.55 && directionY > 0); - setDragY(0); - - if (shouldClose) { - onOpenChange(false); - } - }, - { - axis: "y", - filterTaps: true, - }, - ); - - const sheetHeight = Math.round(device.geometry.screen.height * snap); - const effectiveHeight = Math.max(260, sheetHeight - Math.min(keyboardHeight, 180)); - const sheetBottom = - device.platform === "android" - ? Math.max(device.geometry.safeArea.bottom, keyboardHeight) - : keyboardHeight; - const portalContainer = screenRef.current ?? undefined; - - return ( - - {/* Keep the portal mounted after `open` flips so AnimatePresence can run - the sheet and overlay exit animations before Radix removes them. */} - - - {open ? ( - <> - - - - - -
-
-
-
- {title} - {description ? {description} : null} -
-
{children}
- - - - ) : null} - - - - ); -} diff --git a/ui/src/mobile/COMPONENTS.md b/ui/src/mobile/COMPONENTS.md deleted file mode 100644 index 653194a..0000000 --- a/ui/src/mobile/COMPONENTS.md +++ /dev/null @@ -1,31 +0,0 @@ -# Mobile runtime components - -## Carousel - -`Carousel` is the standard component for horizontal collections: cards, images, media, swipeable items, and chip or filter rails. Place it directly inside `MobileScroll`; consumers should not add gesture wrappers or pointer handlers. - -```tsx - -
- - {cards} - -
-
-``` - -The runtime resolves nested gestures by axis. Horizontal intent stays with `Carousel`; vertical intent is handed to the parent `MobileScroll`. Slight vertical drift after a horizontal gesture is claimed does not move, rubber-band, or add momentum to the parent. Taps remain clickable, while a completed drag suppresses the item click. - -Do not use `data-scroll-drag="ignore"` for carousels or ordinary rails. It is a hard opt-out that prevents parent scrolling in every direction. Do not layer CSS scroll snapping over the runtime's JavaScript momentum. If snapping is added later, it should be a component option so one system owns release motion. - -## Keyboard-linked surfaces - -Use `KeyboardInput`, `KeyboardTextarea`, or `MobileTextField` for all text entry. Position a composer, search surface, or other keyboard-linked UI from `useKeyboardInsets().bottomInset`. The inset is relative to the app viewport: Android's closed-keyboard viewport already ends above its navigation bar, while iOS still needs its overlaid home-indicator inset; both platforms return the keyboard height while the keyboard is open. Never pin those surfaces to only `keyboardHeight`. When that surface closes, call `keyboard.hide()` in the same event before updating its own open state. - -## BottomSheet - -`BottomSheet` dismisses the keyboard before opening and animates both in and out by default. Keep its `open` state controlled through `onOpenChange`; no consumer exit-animation wrapper is needed. diff --git a/ui/src/mobile/Carousel.tsx b/ui/src/mobile/Carousel.tsx deleted file mode 100644 index 28f0997..0000000 --- a/ui/src/mobile/Carousel.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { - useCallback, - useEffect, - useRef, - useState, - type CSSProperties, - type MouseEvent as ReactMouseEvent, - type PointerEvent as ReactPointerEvent, - type PropsWithChildren, -} from "react"; - -export type CarouselProps = PropsWithChildren<{ - className?: string; - contentClassName?: string; - ariaLabel?: string; - showScrollbar?: boolean; - draggingEnabled?: boolean; -}>; - -const physics = { - friction: 2.1, - velocityScale: 890, - velocityTolerance: 18, - bounceTension: 200, - bounceFriction: 40, - overdragScale: 0.5, - maxOverdrag: 96, - sampleWindow: 100, - dragThreshold: 8, -} as const; - -type Sample = { value: number; time: number }; -type DragSession = { - pointerId: number; - startPrimary: number; - startCross: number; - startOffset: number; - captured: boolean; - dragged: boolean; -}; - -export function Carousel({ - className, - contentClassName, - ariaLabel, - showScrollbar = false, - draggingEnabled = true, - children, -}: CarouselProps) { - const scrollRef = useRef(null); - const sessionRef = useRef(null); - const samplesRef = useRef([]); - const frameRef = useRef(null); - const overdragRef = useRef(0); - const suppressClickRef = useRef(false); - const [dragging, setDragging] = useState(false); - const [overdrag, setOverdrag] = useState(0); - const [thumb, setThumb] = useState({ visible: false, offset: 0, size: 0 }); - - const offset = useCallback((node: HTMLDivElement) => node.scrollLeft, []); - const setOffset = useCallback((node: HTMLDivElement, value: number) => { - node.scrollLeft = value; - }, []); - const clientSize = useCallback((node: HTMLDivElement) => node.clientWidth, []); - const scrollSize = useCallback((node: HTMLDivElement) => node.scrollWidth, []); - const maxOffset = useCallback((node: HTMLDivElement) => Math.max(0, scrollSize(node) - clientSize(node)), [clientSize, scrollSize]); - - const stopMotion = useCallback(() => { - if (frameRef.current !== null) window.cancelAnimationFrame(frameRef.current); - frameRef.current = null; - }, []); - const setRubberBand = useCallback((value: number) => { - const next = Math.max(-physics.maxOverdrag, Math.min(physics.maxOverdrag, value)); - overdragRef.current = next; - setOverdrag(next); - }, []); - - const updateThumb = useCallback((visible = true) => { - if (!showScrollbar || !scrollRef.current) return; - const node = scrollRef.current; - const viewport = clientSize(node); - const content = scrollSize(node); - const enabled = content > viewport + 2; - const size = enabled ? Math.max(36, (viewport / content) * viewport) : 0; - const track = Math.max(0, viewport - size - 8); - const progress = offset(node) / Math.max(1, content - viewport); - setThumb({ visible: visible && enabled, size, offset: enabled ? 4 + progress * track : 0 }); - }, [clientSize, offset, scrollSize, showScrollbar]); - - const springBack = useCallback((initialVelocity = 0) => { - stopMotion(); - let position = overdragRef.current; - let velocity = Math.max(-1400, Math.min(1400, initialVelocity)); - let previous: number | null = null; - const tick = (time: number) => { - const seconds = Math.min(0.034, ((previous === null ? 16 : time - previous) || 16) / 1000); - previous = time; - velocity += (-physics.bounceTension * position - physics.bounceFriction * velocity) * seconds; - position += velocity * seconds; - if (Math.abs(position) < 0.5 && Math.abs(velocity) < 0.5) { - setRubberBand(0); - frameRef.current = null; - return; - } - setRubberBand(position); - frameRef.current = window.requestAnimationFrame(tick); - }; - frameRef.current = window.requestAnimationFrame(tick); - }, [setRubberBand, stopMotion]); - - const momentum = useCallback((node: HTMLDivElement, initialVelocity: number) => { - let velocity = initialVelocity; - let previous: number | null = null; - const tick = (time: number) => { - const seconds = (previous === null ? 16 : Math.min(34, time - previous)) / 1000; - previous = time; - velocity *= Math.exp(-physics.friction * seconds); - if (Math.abs(velocity) < physics.velocityTolerance) { - frameRef.current = null; - updateThumb(true); - return; - } - const next = offset(node) + velocity * seconds; - const maximum = maxOffset(node); - if (next < 0 || next > maximum) { - setOffset(node, Math.max(0, Math.min(maximum, next))); - // Match MobileScroll's edge convention: positive displacement at the - // leading edge, negative displacement at the trailing edge. - setRubberBand((next < 0 ? -next : maximum - next) * physics.overdragScale); - springBack(velocity * physics.overdragScale); - return; - } - setOffset(node, next); - updateThumb(true); - frameRef.current = window.requestAnimationFrame(tick); - }; - if (Math.abs(velocity) >= physics.velocityTolerance) frameRef.current = window.requestAnimationFrame(tick); - }, [maxOffset, offset, setOffset, setRubberBand, springBack, updateThumb]); - - useEffect(() => { - const node = scrollRef.current; - if (!node) return; - const onScroll = () => updateThumb(true); - const observer = new ResizeObserver(() => updateThumb(false)); - node.addEventListener("scroll", onScroll, { passive: true }); - observer.observe(node); - if (node.firstElementChild) observer.observe(node.firstElementChild); - updateThumb(false); - return () => { - node.removeEventListener("scroll", onScroll); - observer.disconnect(); - stopMotion(); - }; - }, [stopMotion, updateThumb]); - - const primary = (event: ReactPointerEvent) => event.clientX; - const cross = (event: ReactPointerEvent) => event.clientY; - const record = (value: number) => { - const time = performance.now(); - samplesRef.current = [...samplesRef.current, { value, time }].filter((sample) => time - sample.time <= physics.sampleWindow); - }; - - const onPointerDown = (event: ReactPointerEvent) => { - const node = scrollRef.current; - if (!draggingEnabled || !node || maxOffset(node) <= 2 || (event.pointerType === "mouse" && event.button !== 0)) return; - stopMotion(); - samplesRef.current = []; - record(primary(event)); - setRubberBand(0); - sessionRef.current = { pointerId: event.pointerId, startPrimary: primary(event), startCross: cross(event), startOffset: offset(node), captured: false, dragged: false }; - }; - - const onPointerMove = (event: ReactPointerEvent) => { - const node = scrollRef.current; - const session = sessionRef.current; - if (!node || !session || session.pointerId !== event.pointerId) return; - const delta = primary(event) - session.startPrimary; - const crossDelta = cross(event) - session.startCross; - if (!session.dragged) { - // Keep the gesture pending until it clears tap slop. Pointer-down and - // these early moves must bubble so a parent MobileScroll can still win. - if (Math.max(Math.abs(delta), Math.abs(crossDelta)) < physics.dragThreshold) return; - if (Math.abs(crossDelta) > Math.abs(delta)) { - // The cross axis won. Abandon this session without capturing or - // canceling the event so the parent can handle this move and release. - sessionRef.current = null; - return; - } - // The scroller owns the gesture from this move onward. Capture keeps - // delivery stable outside its bounds; stopping propagation prevents the - // parent from accumulating vertical drift or release momentum. - event.currentTarget.setPointerCapture(event.pointerId); - session.captured = true; - } - event.preventDefault(); - event.stopPropagation(); - session.dragged = true; - setDragging(true); - suppressClickRef.current = true; - record(primary(event)); - const desired = session.startOffset - delta; - const maximum = maxOffset(node); - setOffset(node, Math.max(0, Math.min(maximum, desired))); - setRubberBand(desired < 0 ? -desired * physics.overdragScale : desired > maximum ? -(desired - maximum) * physics.overdragScale : 0); - updateThumb(true); - }; - - const finish = (event: ReactPointerEvent) => { - const node = scrollRef.current; - const session = sessionRef.current; - if (!node || !session || session.pointerId !== event.pointerId) return; - if (session.captured) event.currentTarget.releasePointerCapture(event.pointerId); - const samples = samplesRef.current; - const first = samples[0]; - const last = samples[samples.length - 1]; - const velocity = first && last ? -((last.value - first.value) / Math.max(1, last.time - first.time)) * physics.velocityScale : 0; - sessionRef.current = null; - setDragging(false); - if (session.dragged) event.preventDefault(); - if (Math.abs(overdragRef.current) > 0.1) springBack(velocity * physics.overdragScale); - else momentum(node, velocity); - }; - - const onClickCapture = (event: ReactMouseEvent) => { - if (!suppressClickRef.current) return; - suppressClickRef.current = false; - event.preventDefault(); - event.stopPropagation(); - }; - - const style = { "--mobile-carousel-overdrag": `${overdrag}px` } as CSSProperties; - const thumbStyle = { width: thumb.size, transform: `translateX(${thumb.offset}px)` }; - - return ( -
-
{children}
- {showScrollbar ? ( - - ); -} diff --git a/ui/src/mobile/Device.tsx b/ui/src/mobile/Device.tsx deleted file mode 100644 index aedf084..0000000 --- a/ui/src/mobile/Device.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { createContext, type PropsWithChildren, useContext, useMemo, useState } from "react"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons"; -import { mobileAssets } from "./assets"; -import { iphoneGeometry, pixelGeometry, type MobileDeviceGeometry } from "./geometry"; - -export type MobileDeviceId = "iphone" | "pixel-10"; - -type MobileDevicePreset = { - id: MobileDeviceId; - label: string; - platform: "ios" | "android"; - bezel: string; - bezelLayer: "above-screen" | "below-screen"; - geometry: MobileDeviceGeometry; - camera?: { - size: number; - top: number; - }; -}; - -export const mobileDevices: Record = { - iphone: { - id: "iphone", - label: "iPhone", - platform: "ios", - bezel: mobileAssets.iphoneBezel, - bezelLayer: "above-screen", - geometry: iphoneGeometry, - }, - "pixel-10": { - id: "pixel-10", - label: "Pixel 10", - platform: "android", - bezel: mobileAssets.pixel10Bezel, - bezelLayer: "below-screen", - geometry: pixelGeometry, - camera: { - size: 32, - top: 23, - }, - }, -}; - -type MobileDeviceContextValue = { - device: MobileDevicePreset; - deviceId: MobileDeviceId; - setDeviceId: (deviceId: MobileDeviceId) => void; -}; - -const MobileDeviceContext = createContext(null); - -export function MobileDeviceProvider({ children }: PropsWithChildren) { - const [deviceId, setDeviceId] = useState("iphone"); - const value = useMemo( - () => ({ device: mobileDevices[deviceId], deviceId, setDeviceId }), - [deviceId], - ); - - return {children}; -} - -export function useMobileDevice() { - const context = useContext(MobileDeviceContext); - - if (!context) { - throw new Error("useMobileDevice must be used inside MobileDeviceProvider"); - } - - return context; -} - -export function DevicePicker() { - const { device, deviceId, setDeviceId } = useMobileDevice(); - - return ( - -
- - - -
- - - setDeviceId(value as MobileDeviceId)} - > - {Object.values(mobileDevices).map((option) => ( - - {option.label} - - - - ))} - - - -
- ); -} diff --git a/ui/src/mobile/FlowStack.tsx b/ui/src/mobile/FlowStack.tsx deleted file mode 100644 index f98f257..0000000 --- a/ui/src/mobile/FlowStack.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { - createContext, - type CSSProperties, - type PropsWithChildren, - type ReactNode, - useCallback, - useContext, - useMemo, - useRef, - useState, -} from "react"; -import { AnimatePresence, motion } from "motion/react"; -import { useDrag } from "@use-gesture/react"; -import { useMobileDevice } from "./Device"; -import { useKeyboard, useKeyboardDismissDrag, useKeyboardInsets } from "./Keyboard"; - -export type FlowScreen = { - id: string; - title?: string; - header?: (flow: FlowControls) => ReactNode; - headerHeight?: number; - footer?: (flow: FlowControls) => ReactNode; - footerHeight?: number; - render: (flow: FlowControls) => ReactNode; -}; - -type FlowEntry = FlowScreen & { - key: string; -}; - -export type FlowControls = { - current: FlowEntry; - previous: FlowEntry | null; - stack: FlowEntry[]; - canGoBack: boolean; - push: (screen: FlowScreen) => void; - pop: () => void; - replace: (screen: FlowScreen) => void; -}; - -const FlowContext = createContext(null); - -export function useFlow() { - const context = useContext(FlowContext); - - if (!context) { - throw new Error("useFlow must be used inside FlowStack"); - } - - return context; -} - -function FlowProvider({ value, children }: PropsWithChildren<{ value: FlowControls }>) { - return {children}; -} - -export function FlowStack({ initial }: { initial: FlowScreen }) { - const { device } = useMobileDevice(); - const keyboard = useKeyboard(); - const { bottomInset, keyboardDragging } = useKeyboardInsets(); - const dismissKeyboardDrag = useKeyboardDismissDrag(); - const sequence = useRef(1); - const gestureStartedAtEdge = useRef(false); - const initialEntry = useRef({ ...initial, key: `${initial.id}-0` }); - const [stack, setStack] = useState(() => [initialEntry.current]); - const [direction, setDirection] = useState(1); - const [swipeX, setSwipeX] = useState(0); - - const toEntry = useCallback((screen: FlowScreen): FlowEntry => { - const next = sequence.current; - sequence.current += 1; - return { ...screen, key: `${screen.id}-${next}` }; - }, []); - - const pop = useCallback(() => { - keyboard.hide(); - setDirection(-1); - setStack((currentStack) => { - if (currentStack.length <= 1) return currentStack; - return currentStack.slice(0, -1); - }); - }, [keyboard]); - - const controls = useMemo(() => { - const current = stack[stack.length - 1]; - const previous = stack.length > 1 ? stack[stack.length - 2] : null; - - return { - current, - previous, - stack, - canGoBack: stack.length > 1, - push: (screen) => { - keyboard.hide(); - setDirection(1); - setSwipeX(0); - setStack((currentStack) => [...currentStack, toEntry(screen)]); - }, - pop, - replace: (screen) => { - keyboard.hide(); - setDirection(1); - setSwipeX(0); - setStack((currentStack) => { - const next = currentStack.slice(0, -1); - return [...next, toEntry(screen)]; - }); - }, - }; - }, [keyboard, pop, stack, toEntry]); - - const bindEdgeSwipe = useDrag( - (state) => { - if (!controls.canGoBack) return; - - if (state.first) { - const target = state.event.currentTarget as HTMLElement; - const bounds = target.getBoundingClientRect(); - gestureStartedAtEdge.current = state.initial[0] - bounds.left < 28; - } - - if (!gestureStartedAtEdge.current) return; - - const [movementX] = state.movement; - const [velocityX] = state.velocity; - const [directionX] = state.direction; - const nextX = Math.max(0, Math.min(movementX, device.geometry.screen.width)); - - if (!state.last) { - setSwipeX(nextX); - return; - } - - const shouldPop = nextX > 92 || (velocityX > 0.45 && directionX > 0); - setSwipeX(0); - gestureStartedAtEdge.current = false; - - if (shouldPop) { - controls.pop(); - } - }, - { - axis: "x", - filterTaps: true, - pointer: { touch: true }, - }, - ); - - const screenWidth = device.geometry.screen.width; - const topIndex = stack.length - 1; - const parkedX = -screenWidth * 0.28; - const header = controls.current.header?.(controls); - const headerHeight = controls.current.headerHeight ?? 0; - const headerSafeArea = header ? device.geometry.safeArea.top : 0; - const totalHeaderHeight = header ? headerSafeArea + headerHeight : 0; - const footer = controls.current.footer?.(controls); - const footerHeight = controls.current.footerHeight ?? 0; - - const screenVariants = { - enter: (animationDirection: number) => ({ - x: animationDirection > 0 ? screenWidth : parkedX, - scale: animationDirection > 0 ? 1 : 0.985, - }), - exit: (animationDirection: number) => ({ - x: animationDirection < 0 ? screenWidth : parkedX, - scale: animationDirection < 0 ? 1 : 0.985, - }), - }; - - return ( - -
- {header ? ( -
- {header} -
- ) : null} -
- - {stack.map((entry, index) => { - const isTop = index === topIndex; - const isVisible = index >= topIndex - 1; - - return ( - - {entry.render(controls)} - - ); - })} - -
- {footer ? ( -
- {footer} -
- ) : null} -
-
- ); -} diff --git a/ui/src/mobile/Keyboard.tsx b/ui/src/mobile/Keyboard.tsx deleted file mode 100644 index 54d8464..0000000 --- a/ui/src/mobile/Keyboard.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { - createContext, - type InputHTMLAttributes, - type PointerEvent as ReactPointerEvent, - type PropsWithChildren, - type Ref, - type TextareaHTMLAttributes, - useContext, - useMemo, - useRef, - useState, -} from "react"; -import { motion } from "motion/react"; -import { mobileAssets } from "./assets"; -import { useMobileDevice } from "./Device"; - -type KeyboardContextValue = { - visible: boolean; - height: number; - fullHeight: number; - progress: number; - dragOffset: number; - isDragging: boolean; - focusedElement: HTMLElement | null; - setDragOffset: (offset: number) => void; - setDragging: (dragging: boolean) => void; - show: (element?: HTMLElement | null) => void; - hide: () => void; -}; - -type KeyboardInputProps = InputHTMLAttributes & { - ref?: Ref; -}; - -const KeyboardContext = createContext(null); - -export function KeyboardProvider({ children }: PropsWithChildren) { - const { device } = useMobileDevice(); - const [visible, setVisible] = useState(false); - const [dragOffset, setRawDragOffset] = useState(0); - const [isDragging, setDragging] = useState(false); - const [focusedElement, setFocusedElement] = useState(null); - const fullHeight = device.geometry.keyboard.height; - const setDragOffset = (offset: number) => { - setRawDragOffset(Math.max(0, Math.min(fullHeight, offset))); - }; - - const value = useMemo( - () => ({ - visible, - height: visible ? Math.max(0, fullHeight - dragOffset) : 0, - fullHeight, - dragOffset, - isDragging, - progress: visible ? 1 : 0, - focusedElement, - setDragOffset, - setDragging, - show: (element) => { - setRawDragOffset(0); - setDragging(false); - setFocusedElement(element ?? null); - setVisible(true); - }, - hide: () => { - focusedElement?.blur(); - setDragging(false); - setFocusedElement(null); - setVisible(false); - }, - }), - [dragOffset, focusedElement, fullHeight, isDragging, visible], - ); - - return {children}; -} - -export function useKeyboard() { - const context = useContext(KeyboardContext); - - if (!context) { - throw new Error("useKeyboard must be used inside KeyboardProvider"); - } - - return context; -} - -export function useKeyboardInsets() { - const keyboard = useKeyboard(); - const { device } = useMobileDevice(); - const reservesAndroidNavigation = device.platform === "android" && !keyboard.visible; - - return { - keyboardHeight: keyboard.height, - keyboardFullHeight: keyboard.fullHeight, - keyboardDragging: keyboard.isDragging, - bottomInset: reservesAndroidNavigation - ? 0 - : device.platform === "android" - ? keyboard.height - : Math.max(device.geometry.safeArea.bottom, keyboard.height), - availableHeight: - device.geometry.screen.height - - keyboard.height - - (reservesAndroidNavigation ? device.geometry.safeArea.bottom : 0), - isKeyboardVisible: keyboard.visible, - }; -} - -export function useKeyboardDismissDrag() { - const keyboard = useKeyboard(); - const dragRef = useRef({ - pointerId: null as number | null, - startY: 0, - lastY: 0, - lastTime: 0, - velocityY: 0, - }); - - const endDismissDrag = (event: ReactPointerEvent) => { - const drag = dragRef.current; - if (drag.pointerId !== event.pointerId) return; - - try { - event.currentTarget.releasePointerCapture(event.pointerId); - } catch { - // Capture may already be gone after pointer cancel. - } - - const nextY = Math.max(0, event.clientY - drag.startY); - const shouldDismiss = nextY > 76 || drag.velocityY > 0.45; - drag.pointerId = null; - - if (shouldDismiss) { - keyboard.setDragOffset(nextY); - keyboard.hide(); - return; - } - - keyboard.setDragging(false); - keyboard.setDragOffset(0); - }; - - return { - onPointerDown: (event: ReactPointerEvent) => { - if (!keyboard.visible) return; - if (event.pointerType === "mouse" && event.button !== 0) return; - if ( - event.target instanceof Element && - event.target.closest('button, input, textarea, select, a, [role="button"], [contenteditable="true"]') - ) { - return; - } - - keyboard.setDragging(true); - dragRef.current = { - pointerId: event.pointerId, - startY: event.clientY, - lastY: event.clientY, - lastTime: performance.now(), - velocityY: 0, - }; - event.currentTarget.setPointerCapture(event.pointerId); - }, - onPointerMove: (event: ReactPointerEvent) => { - const drag = dragRef.current; - if (drag.pointerId !== event.pointerId) return; - - const now = performance.now(); - const elapsed = Math.max(1, now - drag.lastTime); - drag.velocityY = (event.clientY - drag.lastY) / elapsed; - drag.lastY = event.clientY; - drag.lastTime = now; - keyboard.setDragOffset(Math.max(0, event.clientY - drag.startY)); - }, - onPointerUp: endDismissDrag, - onPointerCancel: endDismissDrag, - }; -} - -export function KeyboardInput(props: KeyboardInputProps) { - const keyboard = useKeyboard(); - const { ref, ...inputProps } = props; - - return ( - { - keyboard.show(event.currentTarget); - inputProps.onFocus?.(event); - }} - /> - ); -} - -export function KeyboardTextarea(props: TextareaHTMLAttributes) { - const keyboard = useKeyboard(); - - return ( -