diff options
49 files changed, 6277 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3f63df --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +*.tsbuildinfo + +# Tooling caches +.npm-cache + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..42f57eb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# AGENTS.md + +Project-specific guidance for AI coding agents. + +<!-- ASTRYX:START --> +Astryx v0.7.2 · 173 components +CLI: run every command as `npx astryx <cmd>` (shown below as `astryx ...`). + +SETUP (once, in app entry e.g. main.tsx) — without these, components render unstyled: + import "@astryxdesign/core/reset.css"; + import "@astryxdesign/core/astryx.css"; + +WORKFLOW — start every page from a template, never from scratch: +1. `astryx build "<idea>"` — names the template to scaffold from and the parts it lacks. +2. `astryx template <name> <path>` — scaffold it; keep its frame, gap and padding; replace the content. +3. `astryx template <Block>` for parts it lacks; `astryx component <Name>` to read props before using one. +Changing an existing page? Keep it: skip step 2 and add blocks/components inside its sections. + +RULES: +- No <div> — components handle all layout and spacing. +- Read `astryx docs layout` before changing a template's frame. +- Dense data = rows (Table, List/Item), not Card-wrapped lists. Badge = counts only. +- Style with component props first, then tokens (var(--color-*|--spacing-*|--radius-*)). No raw hex/px. No StyleX/Tailwind compiler here — don't use xstyle/utility classes directly. Some templates use StyleX and need a compiler plugin; see `astryx docs styling-overview`. +- Palettes and custom colors go through the theme, never :root overrides. Run `astryx theme add <slug>` to import; `theme eject <slug>` only forks source. See `astryx docs theme`. +- SELF-CHECK: re-read the file; replace any raw <div>/<span>, imported .css/@apply, or hardcoded #hex/px with the component or a token. + +`astryx help` lists every command. Key ones beyond the workflow: + search "<query>" find any component / hook / doc / template + discover <words> integrations you could add + docs <topic> getting-started, principles, tokens, theme … (`astryx docs` lists all) + docs cli commands, API reference, integration authoring + upgrade --from <old version> --apply run after a dependency bump +<!-- ASTRYX:END --> diff --git a/README.md b/README.md new file mode 100644 index 0000000..5193384 --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +# Astryx Starter + +A Vite + React + React Router + Astryx starter with a working app shell, client-side routing, light/dark theming, a Chinese UI, an interactive component gallery, and an icon browser. + +## Stack + +| Tool | Version | +| --- | --- | +| [Vite](https://vite.dev) | 8.x | +| [React](https://react.dev) | 19.x | +| [React Router](https://reactrouter.com) | 7.x | +| [Astryx](https://astryx.atmeta.com) | 0.7.2 | +| TypeScript | 6.0 | +| Oxlint | 1.x | +| pnpm | 11.x | + +Astryx requires React 19 or later. It ships pre-built CSS, so this project does **not** need a StyleX, PostCSS, or Babel plugin. + +## Requirements + +- Node.js 20.19 or newer +- pnpm 11 or newer + +## Getting started + +```bash +pnpm install +pnpm dev +``` + +Vite prints the local URL. Open it in a browser and edit any file under `src/`; HMR updates the page immediately. + +## Scripts + +| Command | Description | +| --- | --- | +| `pnpm dev` | Start the Vite dev server | +| `pnpm build` | Type-check with TypeScript and build for production | +| `pnpm lint` | Run Oxlint | +| `pnpm preview` | Preview the production build locally | +| `pnpm astryx <command>` | Run the project-local Astryx CLI | + +Useful Astryx commands: + +```bash +pnpm astryx component Button # full props, examples, and theming +pnpm astryx component --list # list every component +pnpm astryx template --list # browse page and block templates +pnpm astryx docs theme # theme reference +``` + +## Project structure + +```text +app.config.ts router mode (browser or hash) +src/ + app-routes.tsx routes + sidebar labels/icons (single source of truth) + components/ + AppLayout.tsx app shell, top nav, side nav, route scroll reset + ComponentsRoute.tsx lazy route for the component gallery + IconsRoute.tsx lazy route for the icon browser + Page.tsx shared page frame and page header + data/ + astryxComponents.ts all 173 components and their groups + demos/ + ActionsDemo.tsx … interactive examples by category + pages/ + HomePage.tsx + ComponentsPage.tsx + IconsPage.tsx + AboutPage.tsx + NotFoundPage.tsx + theme/ + ThemeMode.tsx light/dark provider, persisted to localStorage + astryx-themes.ts generated theme registry (CLI-managed) + router.tsx React Router route table + App.tsx providers and router + main.tsx entry point +``` + +`src/app-routes.tsx` is the single source of truth for pages and navigation. Add a page there once; `src/router.tsx` derives the route tree from it and `src/components/AppLayout.tsx` derives the sidebar from the same list. Set `showInNav: false` for routes that should not appear in the sidebar. + +## Component gallery + +The components page renders 14 interactive scenes grouped into base, form, and overlay sections. The scenes cover buttons, typography, layout, data tables, feedback, navigation, chat, form inputs, selection controls, selectors, dialogs, menus, the command palette, and toasts. All 173 Astryx components are represented in the scene metadata, and the full component list is available in a collapsible section at the bottom. Use the search field or the group toggles to narrow the gallery. + +## Icons + +The icons page (`/icons`) browses 29 Astryx semantic icons and 1,903 Lucide icons. Click any icon to copy its usage, search by name, and page through the Lucide library. The Lucide catalog is lazy-loaded so it does not affect the initial route bundle. + +## Build and chunking + +- The component gallery (`/components`) and icon browser (`/icons`) are lazy-loaded routes. +- [vite.config.ts](./vite.config.ts) splits React and React Router into stable `react-vendor` and `router-vendor` chunks. +- In the current production build, the main `index` chunk is about 231 kB minified / 72 kB gzip, the lazy component gallery is about 385 kB / 118 kB gzip, and the lazy Lucide catalog is about 639 kB / 169 kB gzip. + +## Router mode + +The root [app.config.ts](./app.config.ts) controls which router React Router creates: + +```ts +export const appConfig = { + routerMode: 'browser', // or 'hash' +} +``` + +- `browser` uses normal URLs such as `/components`. The host must rewrite unknown paths to `index.html`. +- `hash` uses URLs such as `/#/components`. It works on static hosting without a rewrite. +- Restart the dev server after changing the mode. + +Astryx `LinkProvider` is wired to React Router's `Link`, so internal links, nav items, and buttons keep working in both modes. + +## Routing + +```tsx +const pageRoutes: RouteObject[] = appRoutes.map((route) => + route.path === '/' + ? { index: true, element: route.element } + : { path: route.path.slice(1), element: route.element }, +) + +const routes: RouteObject[] = [ + { + path: '/', + element: <AppLayout />, + children: [...pageRoutes, { path: '*', element: <NotFoundPage /> }], + }, +] + +export const router = + appConfig.routerMode === 'hash' + ? createHashRouter(routes) + : createBrowserRouter(routes) +``` + +`LinkProvider` is wired to React Router's `Link`, so Astryx links, buttons with `href`, nav items, and list rows all participate in client-side navigation. Absolute URLs are handled as external links by React Router. + +When using `browser` mode on static hosting, configure the host to rewrite unknown paths to `index.html` so deep links such as `/components` reach the client router. `hash` mode does not need a rewrite. + +## Theming + +`src/astryx-themes.ts` is generated by the Astryx CLI and imports the production theme CSS plus font CSS. `ThemeModeProvider` wraps the app in `<Theme mode={...}>` and exposes a toggle to the app shell. + +To add another installed theme: + +```bash +pnpm add @astryxdesign/theme-matcha +pnpm astryx theme add matcha +``` + +Then select it in `src/theme/ThemeMode.tsx`. The mode toggle supports light and dark, starts from the OS preference, and remembers the choice in `localStorage`. + +The base styles are imported in `src/index.css`: + +```css +@import '@astryxdesign/core/reset.css'; +@import '@astryxdesign/core/astryx.css'; +``` + +## Localization + +All app copy is Chinese. Astryx's built-in strings (for example the required-field label, skip link, and navigation controls) are localized through `InternationalizationProvider` in `src/main.tsx`, which loads the shipped `zh-CN` catalog. + +## Package manager policy + +The project uses pnpm 11 and pins the versions in `pnpm-lock.yaml`. Astryx `0.7.2` and `lucide-react` `1.61.0` were published inside pnpm's minimum release age window, so `pnpm-workspace.yaml` explicitly excludes those exact versions from the policy and allows the Astryx postinstall scripts. If you bump these packages, update the exclusions or wait until the new versions age past the policy window. + +## Notes + +- `AGENTS.md` contains the Astryx agent guide generated by `astryx init`. +- `theme.template.ts` is the editable template for authoring a custom theme. +- The `@astryxdesign/cli` package is a dev dependency; use the project-local `pnpm astryx` script so the versions stay in sync. diff --git a/app.config.ts b/app.config.ts new file mode 100644 index 0000000..63f3063 --- /dev/null +++ b/app.config.ts @@ -0,0 +1,20 @@ +/** + * 应用级根目录配置。 + * + * routerMode: + * - 'browser':使用普通的 History 路由,地址形如 /components。 + * 部署到静态服务器时需要把未知路径回退到 index.html。 + * - 'hash':使用 Hash 路由,地址形如 /#/components。 + * 不需要服务器做 SPA 回退,适合静态托管或直接打开构建产物。 + * + * 修改后重启开发服务器即可切换。 + */ +export type RouterMode = 'browser' | 'hash' + +export interface AppConfig { + routerMode: RouterMode +} + +export const appConfig: AppConfig = { + routerMode: 'hash', +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..33cef01 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ +<!doctype html> +<html lang="zh-CN"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta + name="description" + content="一个基于 Vite、React、React Router 与 Astryx 的起始项目。" + /> + <title>Astryx 起始模板</title> + </head> + <body> + <div id="root"></div> + <script type="module" src="/src/main.tsx"></script> + </body> +</html> diff --git a/package.json b/package.json new file mode 100644 index 0000000..362468a --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "template-react-astryx", + "private": true, + "version": "0.1.0", + "description": "A Vite + React + React Router + Astryx starter", + "type": "module", + "packageManager": "pnpm@11.24.0", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview", + "astryx": "node node_modules/@astryxdesign/cli/clients/cli/bin/astryx.mjs" + }, + "dependencies": { + "@astryxdesign/core": "^0.7.2", + "@astryxdesign/theme-neutral": "^0.7.2", + "@stylexjs/stylex": "^0.19.1", + "lucide-react": "^1.61.0", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "react-router-dom": "^7.18.4" + }, + "devDependencies": { + "@astryxdesign/cli": "^0.7.2", + "@types/node": "^24.19.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "oxlint": "^1.88.0", + "typescript": "~6.0.3", + "vite": "^8.3.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..2fbdd9a --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1888 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astryxdesign/core': + specifier: ^0.7.2 + version: 0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@astryxdesign/theme-neutral': + specifier: ^0.7.2 + version: 0.7.2(@astryxdesign/core@0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@types/react@19.3.0)(react@19.3.1) + '@stylexjs/stylex': + specifier: ^0.19.1 + version: 0.19.1 + lucide-react: + specifier: ^1.61.0 + version: 1.61.0(@types/react@19.3.0)(react@19.3.1) + react: + specifier: ^19.3.1 + version: 19.3.1 + react-dom: + specifier: ^19.3.1 + version: 19.3.1(react@19.3.1) + react-router-dom: + specifier: ^7.18.4 + version: 7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + devDependencies: + '@astryxdesign/cli': + specifier: ^0.7.2 + version: 0.7.2(@astryxdesign/core@0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@astryxdesign/theme-neutral@0.7.2(@astryxdesign/core@0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@types/react@19.3.0)(react@19.3.1)) + '@types/node': + specifier: ^24.19.2 + version: 24.19.2 + '@types/react': + specifier: ^19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.3.0 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: ^6.1.2 + version: 6.1.2(vite@8.3.4(@types/node@24.19.2)(jiti@2.7.0)) + oxlint: + specifier: ^1.88.0 + version: 1.89.0 + typescript: + specifier: ~6.0.3 + version: 6.0.3 + vite: + specifier: ^8.3.4 + version: 8.3.4(@types/node@24.19.2)(jiti@2.7.0) + +packages: + + '@astryxdesign/cli@0.7.2': + resolution: {integrity: sha512-pFTY8bm/kOkchF6z82lKqMd5CZBdgjBCvb6gEeR2om/DUbXOxC8FAtd1zevev86492twg7r6iiUeF5ZSv3JEcA==} + engines: {node: '>=22.13.0'} + hasBin: true + peerDependencies: + '@astryxdesign/charts': '*' + '@astryxdesign/core': '*' + '@astryxdesign/lab': '*' + '@astryxdesign/theme-neutral': '*' + peerDependenciesMeta: + '@astryxdesign/charts': + optional: true + '@astryxdesign/core': + optional: true + '@astryxdesign/lab': + optional: true + '@astryxdesign/theme-neutral': + optional: true + + '@astryxdesign/core@0.7.2': + resolution: {integrity: sha512-xckYAmyvAPM9Flk5fgRJOml4RVHAj//0P9norZEAxVZMz3J2G9ldIrtuBZzf7OoZMoicwJRVRaQOLwS1w0sImw==} + peerDependencies: + '@stylexjs/stylex': ^0.19.0 + react: '>=19.0.0' + react-dom: '>=19.0.0' + + '@astryxdesign/theme-neutral@0.7.2': + resolution: {integrity: sha512-C7rJwlsgslZpLMxvt3VgUNsFkdMuv69+BpeJDtArAumf53g8Z6pSgjOwLco9HgVzhSyheAXE1+7Q0Nn2434i4g==} + peerDependencies: + '@astryxdesign/core': 0.7.2 + react: '>=19' + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.10': + resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-flow@7.29.7': + resolution: {integrity: sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.29.7': + resolution: {integrity: sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.29.7': + resolution: {integrity: sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7': + resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.29.7': + resolution: {integrity: sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.29.7': + resolution: {integrity: sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-flow@7.29.7': + resolution: {integrity: sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.29.7': + resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/register@7.29.7': + resolution: {integrity: sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@formatjs/fast-memoize@3.1.7': + resolution: {integrity: sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==} + + '@formatjs/icu-messageformat-parser@3.5.18': + resolution: {integrity: sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA==} + + '@formatjs/icu-skeleton-parser@2.1.11': + resolution: {integrity: sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.154.0': + resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==} + + '@oxlint/binding-android-arm-eabi@1.89.0': + resolution: {integrity: sha512-Pflpba5F3gRAQ12sWglRxvWTHEdAusNDvmi4Y5N1T5SY1AGkM7AixOnET8qkrnLJ/F32qAlNokO6eCknT8tmnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.89.0': + resolution: {integrity: sha512-dYmyD1x7LaPlb79vZWUcpTShk8dI9z1I+IubtqrBxWNJaDhQdPHwcsvg7q5StTU4xWyd9AwPfLaTYFZY8NZgtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.89.0': + resolution: {integrity: sha512-Z/oxOPiIqiS7fItRdxaaOEC+6GD3OpGi0IxoR0dMefzQHolsKLzScw2DNcu/5gUPok3tv+G9mslvYB5bo3i0qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.89.0': + resolution: {integrity: sha512-eAjTkHSjAxrdCPJxO6y5sPQL5pYLKb2kvxZz9UU7tXnOZk9I/HLtkvx9S7GHY883k+AmL/eC7mqFt+w7WCwk0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.89.0': + resolution: {integrity: sha512-0lNCA9Tl1n1FKpqy2/9ckp0kIuSrtJ/l9bMRgaEhOtM7TsOHshFuwOcvW8oA9teXLMOJT61LjEKWiaHDFGxF/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + resolution: {integrity: sha512-OTFEvi9W+JczaZD1Caj17SGCoW8H7TE+fvx0D7ATEWa1Lohn91UMxFQ6r145SiGy/yCxeIwptMys/RAM7gSVSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + resolution: {integrity: sha512-fRxgNHvBLswj6MrhW2uBq5UDhPRQlCbHeq/Nf8yuG9zOxhDsXqX/mi2PX1axgsQxMm88el7HLiJAzglAnflYxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.89.0': + resolution: {integrity: sha512-v9ENzHvwrQoYSWTtviH1pOYc7R9UovDxVofnck50cNmPihRRTcrg5X0UlRbLOZ7IVtxPOlh2hb7kDO+FpE8SfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.89.0': + resolution: {integrity: sha512-vfffe8mQ484AzVadlUOFpgL+JDwhvs9hrzxTgQP1FVfi4ZEUUYeffXlzC9dFV7Dgnu5s/KrNKvfmG6HxBd/ISQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + resolution: {integrity: sha512-Buonlp4hVS7RGNP5M7bPpGNQrenyzA2C64JcO/oz9C1c6dGOXJ7VEAnaiAMY7voou0JYyCJdvDVlFQc5DZX3DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + resolution: {integrity: sha512-1eVwJJrJ3lYIPT+cm7BcBBZZXY6IQZP/oyonaruTor/MrVyWOpvnwt1oD5CLdsHg5aVBIxXodQSvt+1gK6ck5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.89.0': + resolution: {integrity: sha512-fRQQsKUGShdEL5Fl9o9uhj3grkZbeXKLv0icQ5tspUCNYSDy8wzzEcP1WC2bKdTQ0rSszbKlO3iiHORxV22DLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.89.0': + resolution: {integrity: sha512-dEtYjIy6+fHcjKklsmtmMkZRhwHr2UbzjmpvuPtQYSeN9awQ2hx5JLms5Wvri+8P/NZtD8v5hfgHALUql+/Ycg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.89.0': + resolution: {integrity: sha512-hqjwIj5KOYM8Wjef/p7IruNi1iByLhSzbMBk62y22P/E8q7Jj0Pp+DeUTKTRYyZYCDtU1wEGlnWOnttHbyNIww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.89.0': + resolution: {integrity: sha512-iAQN3e6o9UgUCcdGwqRx/jYcKbG3HK7ur3rH6lTqVcQeNGZ85AeWIg6cgn/edwFQ0ofEZZItMwkzyCVTahnvEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.89.0': + resolution: {integrity: sha512-MNxjEByz3N/7Q4qFQuPWvc14TpN9LoD3pECEDsJTZbP/wyB7U+hduUXEl7QAbbgVezFwsb9sWtNUO6XVBndOhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.89.0': + resolution: {integrity: sha512-xxe+i9wfuFso/VnoL4mfj0UlzE+cfCFiUB06vkXCMyQdgVVmItF5FfAF4tATAktSU6uHruWR+FjqvAT9cHZZzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.89.0': + resolution: {integrity: sha512-saFy07cO94E664glwgLmKLI40HgTL1PY86y4T6+ud4niX4H0Kdqjcmqmjps7mQNdBytV5guGbXDdyQtRfeLbLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.89.0': + resolution: {integrity: sha512-hLFJ/o0f/dTJh/EW0vmcbREYSQO9tYgfiAAlsOkiLFiJuRRWvYtFELUXN5o2ijpSo/tlO1n+YlWN58tZwPG18A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/binding-android-arm-eabi@1.2.14': + resolution: {integrity: sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.14': + resolution: {integrity: sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.14': + resolution: {integrity: sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.14': + resolution: {integrity: sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.14': + resolution: {integrity: sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.14': + resolution: {integrity: sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.14': + resolution: {integrity: sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.14': + resolution: {integrity: sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.14': + resolution: {integrity: sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.14': + resolution: {integrity: sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.14': + resolution: {integrity: sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.14': + resolution: {integrity: sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.14': + resolution: {integrity: sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.14': + resolution: {integrity: sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.14': + resolution: {integrity: sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@stylexjs/stylex@0.19.1': + resolution: {integrity: sha512-JwNFm0MOYPqi+M4Ax5Un4ZxfZp4HEAX2sAWetZ88KALFcv1eEiHNJs75oO3V3U7RVWnzfkiM8i9eBowC3hqgUw==} + + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@vitejs/plugin-react@6.1.2': + resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.152.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + + ast-types@0.16.3: + resolution: {integrity: sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==} + engines: {node: '>=4'} + + baseline-browser-mapping@2.11.30: + resolution: {integrity: sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.29.4: + resolution: {integrity: sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + caniuse-lite@1.0.30001817: + resolution: {integrity: sha512-uZ1JG80BwhtMalIAGPxPfmNhHyd/B/nTdr3hITWlRamSyAdUStP1Gop01e7SD5q6J90ixJvWDbvmiFVfDgn4bw==} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + css-mediaquery@0.1.2: + resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.462: + resolution: {integrity: sha512-9qpCBlXPy5v7n1Q7WTO5rW2StI4WNQhl96HpgXRZLPlNoXAD0LbsQUflovQYAO44Yia073ISusqohncvGSMbGA==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + find-cache-dir@2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + flow-estree@0.336.0: + resolution: {integrity: sha512-0VaGWHJourzh88otU5bVJ4Z/oIsD+R6yaeR0Q8t8dezsKQI/RLi5m3fNzkiLYLhJ+YwM3JRSKlOqGplOI5Yo3w==} + engines: {node: '>=18'} + + flow-parser@0.336.0: + resolution: {integrity: sha512-O8QvQf/B3rBF+eagcc9/WMAjfJoaKckdgSd8PT/qvMIIe4ZjVKrSqV5SDnRFAKY0VrE8r5C4ADYgvYhmwkWADA==} + engines: {node: '>=0.4.0'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + ignore@7.0.13: + resolution: {integrity: sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + intl-messageformat@11.2.15: + resolution: {integrity: sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jscodeshift@17.4.0: + resolution: {integrity: sha512-i3ESKiiTsGynxzTg5BhsZViD0ai72/6SsI1efDZxG6/5KCoElsmquxtyhXK5lpEgoO7MTNpYkjFEdEL97SkBNg==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + '@babel/preset-env': ^7.1.6 + peerDependenciesMeta: + '@babel/preset-env': + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.60.0: + resolution: {integrity: sha512-UqxOW0FgAfDyb9VmjKb6/BkOzW6yfhf/QNboKtS2NDsscnNRW2CDzlyhbMKBQ1ontmcxuIDqHJ2AVgFR3NI+GA==} + peerDependencies: + '@types/react': '*' + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + lucide-react@1.61.0: + resolution: {integrity: sha512-v2SQPPPNjnr+BlhL9Spuzmo5yR+hLkftfuE4E3jaxdNPgSH0hJKPcLJBP7yAta87jQNPsSERS59PJjCJ/CorBA==} + peerDependencies: + '@types/react': '*' + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-diff3@3.2.1: + resolution: {integrity: sha512-eKZcJ8RtMQ3cIaA15EgmtwG927fYnRlhtdA4Q9HAcCpAZPQhhU2XptnTH9GeAkMiX2bAXxlJSAFa9shFbztPgw==} + engines: {bun: '>=1.3.10'} + + node-releases@2.0.59: + resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==} + engines: {node: '>=18'} + + oxlint@1.89.0: + resolution: {integrity: sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2003' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + + postcss-selector-parser@7.1.7: + resolution: {integrity: sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==} + engines: {node: '>=4'} + + postcss@8.5.31: + resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react-router-dom@7.18.4: + resolution: {integrity: sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.4: + resolution: {integrity: sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + recast@0.23.21: + resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} + engines: {node: '>= 4'} + + rolldown@1.2.14: + resolution: {integrity: sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + styleq@0.2.1: + resolution: {integrity: sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.3.4: + resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + 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 + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + +snapshots: + + '@astryxdesign/cli@0.7.2(@astryxdesign/core@0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@astryxdesign/theme-neutral@0.7.2(@astryxdesign/core@0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@types/react@19.3.0)(react@19.3.1))': + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.8 + ast-types: 0.16.3 + commander: 12.1.0 + ignore: 7.0.13 + jiti: 2.7.0 + jscodeshift: 17.4.0 + node-diff3: 3.2.1 + postcss: 8.5.31 + postcss-selector-parser: 7.1.7 + recast: 0.23.21 + zod: 4.6.7 + optionalDependencies: + '@astryxdesign/core': 0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@astryxdesign/theme-neutral': 0.7.2(@astryxdesign/core@0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@types/react@19.3.0)(react@19.3.1) + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + + '@astryxdesign/core@0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@stylexjs/stylex': 0.19.1 + intl-messageformat: 11.2.15 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + '@astryxdesign/theme-neutral@0.7.2(@astryxdesign/core@0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@astryxdesign/core': 0.7.2(@stylexjs/stylex@0.19.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + lucide-react: 1.60.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + transitivePeerDependencies: + - '@types/react' + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.10 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.10 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.10': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-syntax-flow@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-flow-strip-types@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.7) + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/preset-flow@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.7) + + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/register@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + clone-deep: 4.0.1 + find-cache-dir: 2.1.0 + make-dir: 2.1.0 + pirates: 4.0.7 + source-map-support: 0.5.21 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.10': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@formatjs/fast-memoize@3.1.7': {} + + '@formatjs/icu-messageformat-parser@3.5.18': + dependencies: + '@formatjs/icu-skeleton-parser': 2.1.11 + + '@formatjs/icu-skeleton-parser@2.1.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@oxc-project/types@0.154.0': {} + + '@oxlint/binding-android-arm-eabi@1.89.0': + optional: true + + '@oxlint/binding-android-arm64@1.89.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.89.0': + optional: true + + '@oxlint/binding-darwin-x64@1.89.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.89.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.89.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.89.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.89.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.89.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.89.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.89.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.89.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.89.0': + optional: true + + '@rolldown/binding-android-arm-eabi@1.2.14': + optional: true + + '@rolldown/binding-android-arm64@1.2.14': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.14': + optional: true + + '@rolldown/binding-darwin-x64@1.2.14': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.14': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.14': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.14': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.14': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.14': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.14': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.14': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.14': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@stylexjs/stylex@0.19.1': + dependencies: + css-mediaquery: 0.1.2 + invariant: 2.2.4 + styleq: 0.2.1 + + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.1.2(vite@8.3.4(@types/node@24.19.2)(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.4(@types/node@24.19.2)(jiti@2.7.0) + + ast-types@0.16.3: + dependencies: + tslib: 2.8.1 + + baseline-browser-mapping@2.11.30: {} + + browserslist@4.29.4: + dependencies: + baseline-browser-mapping: 2.11.30 + caniuse-lite: 1.0.30001817 + electron-to-chromium: 1.5.462 + node-releases: 2.0.59 + update-browserslist-db: 1.3.4(browserslist@4.29.4) + + buffer-from@1.1.2: {} + + caniuse-lite@1.0.30001817: {} + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + commander@12.1.0: {} + + commondir@1.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + css-mediaquery@0.1.2: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.462: {} + + escalade@3.2.0: {} + + esprima@4.0.1: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + find-cache-dir@2.1.0: + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + flow-estree@0.336.0: {} + + flow-parser@0.336.0: + dependencies: + flow-estree: 0.336.0 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + graceful-fs@4.2.11: {} + + ignore@7.0.13: {} + + imurmurhash@0.1.4: {} + + intl-messageformat@11.2.15: + dependencies: + '@formatjs/fast-memoize': 3.1.7 + '@formatjs/icu-messageformat-parser': 3.5.18 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + isobject@3.0.1: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + jscodeshift@17.4.0: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) + '@babel/preset-flow': 7.29.7(@babel/core@7.29.7) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/register': 7.29.7(@babel/core@7.29.7) + flow-parser: 0.336.0 + graceful-fs: 4.2.11 + neo-async: 2.6.2 + picocolors: 1.1.1 + picomatch: 4.0.7 + recast: 0.23.21 + tmp: 0.2.7 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + kind-of@6.0.3: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.60.0(@types/react@19.3.0)(react@19.3.1): + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + lucide-react@1.61.0(@types/react@19.3.0)(react@19.3.1): + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + neo-async@2.6.2: {} + + node-diff3@3.2.1: {} + + node-releases@2.0.59: {} + + oxlint@1.89.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.89.0 + '@oxlint/binding-android-arm64': 1.89.0 + '@oxlint/binding-darwin-arm64': 1.89.0 + '@oxlint/binding-darwin-x64': 1.89.0 + '@oxlint/binding-freebsd-x64': 1.89.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.89.0 + '@oxlint/binding-linux-arm-musleabihf': 1.89.0 + '@oxlint/binding-linux-arm64-gnu': 1.89.0 + '@oxlint/binding-linux-arm64-musl': 1.89.0 + '@oxlint/binding-linux-ppc64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-musl': 1.89.0 + '@oxlint/binding-linux-s390x-gnu': 1.89.0 + '@oxlint/binding-linux-x64-gnu': 1.89.0 + '@oxlint/binding-linux-x64-musl': 1.89.0 + '@oxlint/binding-openharmony-arm64': 1.89.0 + '@oxlint/binding-win32-arm64-msvc': 1.89.0 + '@oxlint/binding-win32-ia32-msvc': 1.89.0 + '@oxlint/binding-win32-x64-msvc': 1.89.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + path-exists@3.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + pify@4.0.1: {} + + pirates@4.0.7: {} + + pkg-dir@3.0.0: + dependencies: + find-up: 3.0.0 + + postcss-selector-parser@7.1.7: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.31: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react-router-dom@7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + react-router: 7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + + react-router@7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + cookie: 1.1.1 + react: 19.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.3.1(react@19.3.1) + + react@19.3.1: {} + + recast@0.23.21: + dependencies: + ast-types: 0.16.3 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + rolldown@1.2.14: + dependencies: + '@oxc-project/types': 0.154.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.14 + '@rolldown/binding-android-arm64': 1.2.14 + '@rolldown/binding-darwin-arm64': 1.2.14 + '@rolldown/binding-darwin-x64': 1.2.14 + '@rolldown/binding-freebsd-x64': 1.2.14 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.14 + '@rolldown/binding-linux-arm64-gnu': 1.2.14 + '@rolldown/binding-linux-arm64-musl': 1.2.14 + '@rolldown/binding-linux-ppc64-gnu': 1.2.14 + '@rolldown/binding-linux-s390x-gnu': 1.2.14 + '@rolldown/binding-linux-x64-gnu': 1.2.14 + '@rolldown/binding-linux-x64-musl': 1.2.14 + '@rolldown/binding-openharmony-arm64': 1.2.14 + '@rolldown/binding-win32-arm64-msvc': 1.2.14 + '@rolldown/binding-win32-x64-msvc': 1.2.14 + + scheduler@0.28.0: {} + + semver@5.7.2: {} + + semver@6.3.1: {} + + set-cookie-parser@2.7.2: {} + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + signal-exit@4.1.0: {} + + source-map-js@1.2.2: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + styleq@0.2.1: {} + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tmp@0.2.7: {} + + tslib@2.8.1: {} + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + update-browserslist-db@1.3.4(browserslist@4.29.4): + dependencies: + browserslist: 4.29.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite@8.3.4(@types/node@24.19.2)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.31 + rolldown: 1.2.14 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.19.2 + fsevents: 2.3.3 + jiti: 2.7.0 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + yallist@3.1.1: {} + + zod@4.6.7: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..a563303 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,9 @@ +allowBuilds: + '@astryxdesign/cli': true + '@astryxdesign/core': true + +minimumReleaseAgeExclude: + - '@astryxdesign/cli@0.7.2' + - '@astryxdesign/core@0.7.2' + - '@astryxdesign/theme-neutral@0.7.2' + - lucide-react@1.61.0 diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..2c3d987 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> + <rect width="32" height="32" rx="8" fill="#111314" /> + <path + d="M16 5.5c.5 4.9 3.1 7.6 7.9 8.1v1.3c-4.8.5-7.4 3.2-7.9 8.1h-1.3c-.5-4.9-3.1-7.6-7.9-8.1v-1.3c4.8-.5 7.4-3.2 7.9-8.1H16Z" + fill="#f5f5f4" + /> + <circle cx="24.5" cy="24.5" r="3" fill="#f5f5f4" /> +</svg> diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..91e52c9 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,11 @@ +import { LinkProvider } from '@astryxdesign/core/Link' +import { Link as RouterLink, RouterProvider } from 'react-router-dom' +import { router } from './router' + +export function App() { + return ( + <LinkProvider component={RouterLink}> + <RouterProvider router={router} /> + </LinkProvider> + ) +} diff --git a/src/app-routes.tsx b/src/app-routes.tsx new file mode 100644 index 0000000..58747bb --- /dev/null +++ b/src/app-routes.tsx @@ -0,0 +1,56 @@ +import type { ReactNode } from 'react' +import type { LucideIcon } from 'lucide-react' +import { Blocks, House, Info, Shapes } from 'lucide-react' +import { ComponentsRoute } from './components/ComponentsRoute' +import { IconsRoute } from './components/IconsRoute' +import { AboutPage } from './pages/AboutPage' +import { HomePage } from './pages/HomePage' + +export interface AppRoute { + path: string + label: string + icon: LucideIcon + element: ReactNode + showInNav?: boolean +} + +/** + * 页面、路由和侧边栏的唯一数据源。 + * 新增页面时只在这里增加一项,路由与菜单会同步更新。 + */ +export const appRoutes: AppRoute[] = [ + { + path: '/', + label: '概览', + icon: House, + element: <HomePage />, + }, + { + path: '/components', + label: '组件', + icon: Blocks, + element: <ComponentsRoute />, + }, + { + path: '/icons', + label: '图标', + icon: Shapes, + element: <IconsRoute />, + }, + { + path: '/about', + label: '关于', + icon: Info, + element: <AboutPage />, + }, +] + +export const navRoutes = appRoutes.filter( + (route) => route.showInNav !== false, +) + +export function isRouteActive(routePath: string, pathname: string) { + return routePath === '/' + ? pathname === '/' + : pathname === routePath || pathname.startsWith(`${routePath}/`) +} diff --git a/src/astryx-themes.ts b/src/astryx-themes.ts new file mode 100644 index 0000000..9f253e2 --- /dev/null +++ b/src/astryx-themes.ts @@ -0,0 +1,15 @@ +/** + * @generated by `astryx theme` — do not edit manually. + * @astryx-theme-record {"themes":{"neutral":"@astryxdesign/theme-neutral"},"defaultThemeSlug":"neutral"} + */ + +import {neutralTheme as _astryxTheme0} from "@astryxdesign/theme-neutral/built"; +import "@astryxdesign/theme-neutral/theme.css"; +import "@astryxdesign/theme-neutral/fonts.css"; + +export const themes = { + "neutral": _astryxTheme0, +} as const; +export type ThemeSlug = keyof typeof themes; + +export const defaultThemeSlug: keyof typeof themes = "neutral"; diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx new file mode 100644 index 0000000..9c93302 --- /dev/null +++ b/src/components/AppLayout.tsx @@ -0,0 +1,79 @@ +import { useLayoutEffect, useState } from 'react' +import { Outlet, useLocation } from 'react-router-dom' +import { AppShell } from '@astryxdesign/core/AppShell' +import { Icon } from '@astryxdesign/core/Icon' +import { IconButton } from '@astryxdesign/core/IconButton' +import { NavIcon } from '@astryxdesign/core/NavIcon' +import { SideNav, SideNavItem, SideNavSection } from '@astryxdesign/core/SideNav' +import { TopNav, TopNavHeading } from '@astryxdesign/core/TopNav' +import { Moon, Sparkles, Sun } from 'lucide-react' +import { isRouteActive, navRoutes } from '../app-routes' +import { useThemeMode } from '../theme/useThemeMode' + +export function AppLayout() { + const { pathname } = useLocation() + const { mode, toggleMode } = useThemeMode() + const [isMobileNavOpen, setIsMobileNavOpen] = useState(false) + + useLayoutEffect(() => { + const main = document.getElementById('astryx-app-shell-main') + if (main) main.scrollTop = 0 + window.scrollTo(0, 0) + }, [pathname]) + + const nextMode = mode === 'light' ? 'dark' : 'light' + + return ( + <AppShell + height="fill" + contentPadding={0} + mobileNav={{ + isOpen: isMobileNavOpen, + onOpenChange: setIsMobileNavOpen, + }} + topNav={ + <TopNav + label="主导航" + heading={ + <TopNavHeading + heading="Astryx 起始模板" + headingHref="/" + logo={ + <NavIcon + icon={<Icon icon={Sparkles} size="sm" color="inherit" />} + /> + } + /> + } + endContent={ + <IconButton + label={`切换到${nextMode === 'dark' ? '深色' : '浅色'}主题`} + tooltip={`切换到${nextMode === 'dark' ? '深色' : '浅色'}主题`} + variant="ghost" + icon={<Icon icon={mode === 'light' ? Moon : Sun} />} + onClick={toggleMode} + /> + } + /> + } + sideNav={ + <SideNav collapsible> + <SideNavSection title="探索"> + {navRoutes.map(({ label, path, icon }) => ( + <SideNavItem + key={path} + label={label} + href={path} + icon={icon} + selectedIcon={icon} + isSelected={isRouteActive(path, pathname)} + onClick={() => setIsMobileNavOpen(false)} + /> + ))} + </SideNavSection> + </SideNav> + }> + <Outlet /> + </AppShell> + ) +} diff --git a/src/components/ComponentsRoute.tsx b/src/components/ComponentsRoute.tsx new file mode 100644 index 0000000..53db94a --- /dev/null +++ b/src/components/ComponentsRoute.tsx @@ -0,0 +1,16 @@ +import { lazy, Suspense } from 'react' +import { PageLoading } from './PageLoading' + +const ComponentsPage = lazy(() => + import('../pages/ComponentsPage').then((module) => ({ + default: module.ComponentsPage, + })), +) + +export function ComponentsRoute() { + return ( + <Suspense fallback={<PageLoading />}> + <ComponentsPage /> + </Suspense> + ) +} diff --git a/src/components/IconsRoute.tsx b/src/components/IconsRoute.tsx new file mode 100644 index 0000000..83c7690 --- /dev/null +++ b/src/components/IconsRoute.tsx @@ -0,0 +1,16 @@ +import { lazy, Suspense } from 'react' +import { PageLoading } from './PageLoading' + +const IconsPage = lazy(() => + import('../pages/IconsPage').then((module) => ({ + default: module.IconsPage, + })), +) + +export function IconsRoute() { + return ( + <Suspense fallback={<PageLoading />}> + <IconsPage /> + </Suspense> + ) +} diff --git a/src/components/Page.tsx b/src/components/Page.tsx new file mode 100644 index 0000000..4dd1248 --- /dev/null +++ b/src/components/Page.tsx @@ -0,0 +1,42 @@ +import type { ReactNode } from 'react' +import { Layout, LayoutContent } from '@astryxdesign/core/Layout' +import { Heading } from '@astryxdesign/core/Heading' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' + +export function Page({ children }: { children: ReactNode }) { + return ( + <Layout + height="auto" + padding={6} + contentWidth={1120} + content={<LayoutContent>{children}</LayoutContent>} + /> + ) +} + +interface PageHeaderProps { + eyebrow?: string + title: string + description?: string +} + +export function PageHeader({ eyebrow, title, description }: PageHeaderProps) { + return ( + <VStack gap={2} maxWidth={820}> + {eyebrow ? ( + <Text type="label" color="accent"> + {eyebrow} + </Text> + ) : null} + <Heading level={1} type="display-3" textWrap="balance"> + {title} + </Heading> + {description ? ( + <Text type="large" color="secondary" as="p" textWrap="pretty"> + {description} + </Text> + ) : null} + </VStack> + ) +} diff --git a/src/components/PageLoading.tsx b/src/components/PageLoading.tsx new file mode 100644 index 0000000..3e98fe8 --- /dev/null +++ b/src/components/PageLoading.tsx @@ -0,0 +1,13 @@ +import { Center } from '@astryxdesign/core/Center' +import { Spinner } from '@astryxdesign/core/Spinner' +import { Page } from './Page' + +export function PageLoading() { + return ( + <Page> + <Center minHeight={480}> + <Spinner size="lg" /> + </Center> + </Page> + ) +} diff --git a/src/data/astryxComponents.ts b/src/data/astryxComponents.ts new file mode 100644 index 0000000..352b20d --- /dev/null +++ b/src/data/astryxComponents.ts @@ -0,0 +1,1223 @@ +export type AstryxComponentGroup = 'base' | 'form' | 'overlay' + +export interface AstryxComponentEntry { + name: string + importPath: string + family: string + group: AstryxComponentGroup + docsUrl: string +} + +export const ASTRYX_COMPONENTS: AstryxComponentEntry[] = [ + { + name: 'AlertDialog', + importPath: '@astryxdesign/core/AlertDialog', + family: 'Dialog', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/AlertDialog', + }, + { + name: 'AppShell', + importPath: '@astryxdesign/core/AppShell', + family: 'AppShell', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/AppShell', + }, + { + name: 'AspectRatio', + importPath: '@astryxdesign/core/AspectRatio', + family: 'AspectRatio', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/AspectRatio', + }, + { + name: 'Avatar', + importPath: '@astryxdesign/core/Avatar', + family: 'Avatar', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Avatar', + }, + { + name: 'AvatarGroup', + importPath: '@astryxdesign/core/AvatarGroup', + family: 'Avatar', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/AvatarGroup', + }, + { + name: 'AvatarGroupOverflow', + importPath: '@astryxdesign/core/AvatarGroup', + family: 'Avatar', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/AvatarGroupOverflow', + }, + { + name: 'AvatarStatusDot', + importPath: '@astryxdesign/core/Avatar', + family: 'Avatar', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/AvatarStatusDot', + }, + { + name: 'Badge', + importPath: '@astryxdesign/core/Badge', + family: 'Badge', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Badge', + }, + { + name: 'Banner', + importPath: '@astryxdesign/core/Banner', + family: 'Banner', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Banner', + }, + { + name: 'BaseTypeahead', + importPath: '@astryxdesign/core/Typeahead', + family: 'Typeahead', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/BaseTypeahead', + }, + { + name: 'Blockquote', + importPath: '@astryxdesign/core/Blockquote', + family: 'Blockquote', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Blockquote', + }, + { + name: 'BottomSheet', + importPath: '@astryxdesign/core/BottomSheet', + family: 'BottomSheet', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/BottomSheet', + }, + { + name: 'BottomSheetSwitcher', + importPath: '@astryxdesign/core/BottomSheet', + family: 'BottomSheet', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/BottomSheetSwitcher', + }, + { + name: 'BreadcrumbItem', + importPath: '@astryxdesign/core/Breadcrumbs', + family: 'Breadcrumbs', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/BreadcrumbItem', + }, + { + name: 'Breadcrumbs', + importPath: '@astryxdesign/core/Breadcrumbs', + family: 'Breadcrumbs', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Breadcrumbs', + }, + { + name: 'Button', + importPath: '@astryxdesign/core/Button', + family: 'Button', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Button', + }, + { + name: 'ButtonGroup', + importPath: '@astryxdesign/core/ButtonGroup', + family: 'Button', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ButtonGroup', + }, + { + name: 'Calendar', + importPath: '@astryxdesign/core/Calendar', + family: 'Calendar', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/Calendar', + }, + { + name: 'Card', + importPath: '@astryxdesign/core/Card', + family: 'Card', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Card', + }, + { + name: 'Carousel', + importPath: '@astryxdesign/core/Carousel', + family: 'Carousel', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Carousel', + }, + { + name: 'Center', + importPath: '@astryxdesign/core/Center', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Center', + }, + { + name: 'ChatComposer', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatComposer', + }, + { + name: 'ChatComposerDrawer', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatComposerDrawer', + }, + { + name: 'ChatComposerInput', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatComposerInput', + }, + { + name: 'ChatDictationButton', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatDictationButton', + }, + { + name: 'ChatLayout', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatLayout', + }, + { + name: 'ChatLayoutScrollButton', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatLayoutScrollButton', + }, + { + name: 'ChatMessage', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatMessage', + }, + { + name: 'ChatMessageBubble', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatMessageBubble', + }, + { + name: 'ChatMessageList', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatMessageList', + }, + { + name: 'ChatMessageMetadata', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatMessageMetadata', + }, + { + name: 'ChatSendButton', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatSendButton', + }, + { + name: 'ChatSystemMessage', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatSystemMessage', + }, + { + name: 'ChatTokenizedText', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatTokenizedText', + }, + { + name: 'ChatToolCalls', + importPath: '@astryxdesign/core/Chat', + family: 'Chat', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ChatToolCalls', + }, + { + name: 'CheckboxInput', + importPath: '@astryxdesign/core/CheckboxInput', + family: 'Checkbox', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/CheckboxInput', + }, + { + name: 'CheckboxList', + importPath: '@astryxdesign/core/CheckboxList', + family: 'Checkbox', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/CheckboxList', + }, + { + name: 'CheckboxListItem', + importPath: '@astryxdesign/core/CheckboxList', + family: 'Checkbox', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/CheckboxListItem', + }, + { + name: 'Citation', + importPath: '@astryxdesign/core/Citation', + family: 'Citation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Citation', + }, + { + name: 'ClickableCard', + importPath: '@astryxdesign/core/ClickableCard', + family: 'Card', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ClickableCard', + }, + { + name: 'Code', + importPath: '@astryxdesign/core/Code', + family: 'Code', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Code', + }, + { + name: 'CodeBlock', + importPath: '@astryxdesign/core/CodeBlock', + family: 'CodeBlock', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/CodeBlock', + }, + { + name: 'Collapsible', + importPath: '@astryxdesign/core/Collapsible', + family: 'Collapsible', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Collapsible', + }, + { + name: 'CollapsibleGroup', + importPath: '@astryxdesign/core/Collapsible', + family: 'Collapsible', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/CollapsibleGroup', + }, + { + name: 'CommandPalette', + importPath: '@astryxdesign/core/CommandPalette', + family: 'CommandPalette', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/CommandPalette', + }, + { + name: 'CommandPaletteEmpty', + importPath: '@astryxdesign/core/CommandPalette', + family: 'CommandPalette', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteEmpty', + }, + { + name: 'CommandPaletteFooter', + importPath: '@astryxdesign/core/CommandPalette', + family: 'CommandPalette', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteFooter', + }, + { + name: 'CommandPaletteGroup', + importPath: '@astryxdesign/core/CommandPalette', + family: 'CommandPalette', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteGroup', + }, + { + name: 'CommandPaletteInput', + importPath: '@astryxdesign/core/CommandPalette', + family: 'CommandPalette', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteInput', + }, + { + name: 'CommandPaletteItem', + importPath: '@astryxdesign/core/CommandPalette', + family: 'CommandPalette', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteItem', + }, + { + name: 'CommandPaletteList', + importPath: '@astryxdesign/core/CommandPalette', + family: 'CommandPalette', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteList', + }, + { + name: 'ComplexSelector', + importPath: '@astryxdesign/core/ComplexSelector', + family: 'Selector', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/ComplexSelector', + }, + { + name: 'ContextMenu', + importPath: '@astryxdesign/core/ContextMenu', + family: 'ContextMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/ContextMenu', + }, + { + name: 'DateInput', + importPath: '@astryxdesign/core/DateInput', + family: 'DateInput', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/DateInput', + }, + { + name: 'DateRangeInput', + importPath: '@astryxdesign/core/DateRangeInput', + family: 'DateInput', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/DateRangeInput', + }, + { + name: 'DateTimeInput', + importPath: '@astryxdesign/core/DateTimeInput', + family: 'DateInput', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/DateTimeInput', + }, + { + name: 'Dialog', + importPath: '@astryxdesign/core/Dialog', + family: 'Dialog', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/Dialog', + }, + { + name: 'DialogHeader', + importPath: '@astryxdesign/core/Dialog', + family: 'Dialog', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DialogHeader', + }, + { + name: 'Divider', + importPath: '@astryxdesign/core/Divider', + family: 'Divider', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Divider', + }, + { + name: 'Drawer', + importPath: '@astryxdesign/core/Drawer', + family: 'Drawer', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/Drawer', + }, + { + name: 'DrawerHeader', + importPath: '@astryxdesign/core/Drawer', + family: 'Drawer', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DrawerHeader', + }, + { + name: 'DropdownMenu', + importPath: '@astryxdesign/core/DropdownMenu', + family: 'DropdownMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DropdownMenu', + }, + { + name: 'DropdownMenuCheckboxItem', + importPath: '@astryxdesign/core/DropdownMenu', + family: 'DropdownMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuCheckboxItem', + }, + { + name: 'DropdownMenuDivider', + importPath: '@astryxdesign/core/DropdownMenu', + family: 'DropdownMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuDivider', + }, + { + name: 'DropdownMenuGroup', + importPath: '@astryxdesign/core/DropdownMenu', + family: 'DropdownMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuGroup', + }, + { + name: 'DropdownMenuItem', + importPath: '@astryxdesign/core/DropdownMenu', + family: 'DropdownMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuItem', + }, + { + name: 'DropdownMenuRadioGroup', + importPath: '@astryxdesign/core/DropdownMenu', + family: 'DropdownMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuRadioGroup', + }, + { + name: 'DropdownMenuRadioItem', + importPath: '@astryxdesign/core/DropdownMenu', + family: 'DropdownMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuRadioItem', + }, + { + name: 'DropdownMenuSubMenu', + importPath: '@astryxdesign/core/DropdownMenu', + family: 'DropdownMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuSubMenu', + }, + { + name: 'EmptyState', + importPath: '@astryxdesign/core/EmptyState', + family: 'EmptyState', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/EmptyState', + }, + { + name: 'Field', + importPath: '@astryxdesign/core/Field', + family: 'Field', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/Field', + }, + { + name: 'FieldLabel', + importPath: '@astryxdesign/core/Field', + family: 'Field', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/FieldLabel', + }, + { + name: 'FieldStatus', + importPath: '@astryxdesign/core/FieldStatus', + family: 'Field', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/FieldStatus', + }, + { + name: 'FileInput', + importPath: '@astryxdesign/core/FileInput', + family: 'FileInput', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/FileInput', + }, + { + name: 'FormLayout', + importPath: '@astryxdesign/core/FormLayout', + family: 'Layout', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/FormLayout', + }, + { + name: 'Grid', + importPath: '@astryxdesign/core/Grid', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Grid', + }, + { + name: 'GridSpan', + importPath: '@astryxdesign/core/Grid', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/GridSpan', + }, + { + name: 'Heading', + importPath: '@astryxdesign/core/Heading', + family: 'Heading', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Heading', + }, + { + name: 'HoverCard', + importPath: '@astryxdesign/core/HoverCard', + family: 'HoverCard', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/HoverCard', + }, + { + name: 'HStack', + importPath: '@astryxdesign/core/HStack', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/HStack', + }, + { + name: 'Icon', + importPath: '@astryxdesign/core/Icon', + family: 'Icon', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Icon', + }, + { + name: 'IconButton', + importPath: '@astryxdesign/core/IconButton', + family: 'Button', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/IconButton', + }, + { + name: 'InputGroup', + importPath: '@astryxdesign/core/InputGroup', + family: 'Field', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/InputGroup', + }, + { + name: 'InputGroupText', + importPath: '@astryxdesign/core/InputGroup', + family: 'Field', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/InputGroupText', + }, + { + name: 'InternationalizationProvider', + importPath: '@astryxdesign/core/i18n', + family: 'Utilities', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/InternationalizationProvider', + }, + { + name: 'Item', + importPath: '@astryxdesign/core/Item', + family: 'Item', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Item', + }, + { + name: 'Kbd', + importPath: '@astryxdesign/core/Kbd', + family: 'Kbd', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Kbd', + }, + { + name: 'Layout', + importPath: '@astryxdesign/core/Layout', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Layout', + }, + { + name: 'LayoutContent', + importPath: '@astryxdesign/core/Layout', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/LayoutContent', + }, + { + name: 'LayoutFooter', + importPath: '@astryxdesign/core/Layout', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/LayoutFooter', + }, + { + name: 'LayoutHeader', + importPath: '@astryxdesign/core/Layout', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/LayoutHeader', + }, + { + name: 'LayoutPanel', + importPath: '@astryxdesign/core/Layout', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/LayoutPanel', + }, + { + name: 'Lightbox', + importPath: '@astryxdesign/core/Lightbox', + family: 'Lightbox', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/Lightbox', + }, + { + name: 'Link', + importPath: '@astryxdesign/core/Link', + family: 'Link', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Link', + }, + { + name: 'LinkProvider', + importPath: '@astryxdesign/core/Link', + family: 'Utilities', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/LinkProvider', + }, + { + name: 'List', + importPath: '@astryxdesign/core/List', + family: 'List', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/List', + }, + { + name: 'ListItem', + importPath: '@astryxdesign/core/List', + family: 'List', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ListItem', + }, + { + name: 'Markdown', + importPath: '@astryxdesign/core/Markdown', + family: 'Markdown', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Markdown', + }, + { + name: 'MediaTheme', + importPath: '@astryxdesign/core/theme', + family: 'Utilities', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/MediaTheme', + }, + { + name: 'MetadataList', + importPath: '@astryxdesign/core/MetadataList', + family: 'MetadataList', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/MetadataList', + }, + { + name: 'MetadataListItem', + importPath: '@astryxdesign/core/MetadataList', + family: 'MetadataList', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/MetadataListItem', + }, + { + name: 'MobileNav', + importPath: '@astryxdesign/core/MobileNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/MobileNav', + }, + { + name: 'MoreMenu', + importPath: '@astryxdesign/core/MoreMenu', + family: 'MoreMenu', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/MoreMenu', + }, + { + name: 'MultiSelector', + importPath: '@astryxdesign/core/MultiSelector', + family: 'Selector', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/MultiSelector', + }, + { + name: 'NavIcon', + importPath: '@astryxdesign/core/NavIcon', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/NavIcon', + }, + { + name: 'NumberInput', + importPath: '@astryxdesign/core/NumberInput', + family: 'NumberInput', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/NumberInput', + }, + { + name: 'Outline', + importPath: '@astryxdesign/core/Outline', + family: 'Outline', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Outline', + }, + { + name: 'OverflowList', + importPath: '@astryxdesign/core/OverflowList', + family: 'OverflowList', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/OverflowList', + }, + { + name: 'Overlay', + importPath: '@astryxdesign/core/Overlay', + family: 'Overlay', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/Overlay', + }, + { + name: 'Pagination', + importPath: '@astryxdesign/core/Pagination', + family: 'Pagination', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Pagination', + }, + { + name: 'Popover', + importPath: '@astryxdesign/core/Popover', + family: 'Popover', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/Popover', + }, + { + name: 'PowerSearch', + importPath: '@astryxdesign/core/PowerSearch', + family: 'PowerSearch', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/PowerSearch', + }, + { + name: 'ProgressBar', + importPath: '@astryxdesign/core/ProgressBar', + family: 'ProgressBar', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ProgressBar', + }, + { + name: 'RadioList', + importPath: '@astryxdesign/core/RadioList', + family: 'Radio', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/RadioList', + }, + { + name: 'RadioListItem', + importPath: '@astryxdesign/core/RadioList', + family: 'Radio', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/RadioListItem', + }, + { + name: 'ScrollableArea', + importPath: '@astryxdesign/core/ScrollableArea', + family: 'ScrollableArea', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/ScrollableArea', + }, + { + name: 'Section', + importPath: '@astryxdesign/core/Section', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Section', + }, + { + name: 'SegmentedControl', + importPath: '@astryxdesign/core/SegmentedControl', + family: 'SegmentedControl', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/SegmentedControl', + }, + { + name: 'SegmentedControlItem', + importPath: '@astryxdesign/core/SegmentedControl', + family: 'SegmentedControl', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/SegmentedControlItem', + }, + { + name: 'SelectableCard', + importPath: '@astryxdesign/core/SelectableCard', + family: 'Card', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/SelectableCard', + }, + { + name: 'Selector', + importPath: '@astryxdesign/core/Selector', + family: 'Selector', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/Selector', + }, + { + name: 'SelectorOption', + importPath: '@astryxdesign/core/Selector', + family: 'Selector', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/SelectorOption', + }, + { + name: 'SideNav', + importPath: '@astryxdesign/core/SideNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/SideNav', + }, + { + name: 'SideNavCollapseButton', + importPath: '@astryxdesign/core/SideNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/SideNavCollapseButton', + }, + { + name: 'SideNavHeading', + importPath: '@astryxdesign/core/SideNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/SideNavHeading', + }, + { + name: 'SideNavItem', + importPath: '@astryxdesign/core/SideNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/SideNavItem', + }, + { + name: 'SideNavSection', + importPath: '@astryxdesign/core/SideNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/SideNavSection', + }, + { + name: 'Skeleton', + importPath: '@astryxdesign/core/Skeleton', + family: 'Skeleton', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Skeleton', + }, + { + name: 'Slider', + importPath: '@astryxdesign/core/Slider', + family: 'Slider', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/Slider', + }, + { + name: 'Spinner', + importPath: '@astryxdesign/core/Spinner', + family: 'Spinner', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Spinner', + }, + { + name: 'Stack', + importPath: '@astryxdesign/core/Stack', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Stack', + }, + { + name: 'StatusDot', + importPath: '@astryxdesign/core/StatusDot', + family: 'StatusDot', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/StatusDot', + }, + { + name: 'Step', + importPath: '@astryxdesign/core/Stepper', + family: 'Stepper', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Step', + }, + { + name: 'Stepper', + importPath: '@astryxdesign/core/Stepper', + family: 'Stepper', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Stepper', + }, + { + name: 'Switch', + importPath: '@astryxdesign/core/Switch', + family: 'Switch', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/Switch', + }, + { + name: 'Tab', + importPath: '@astryxdesign/core/TabList', + family: 'Tabs', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Tab', + }, + { + name: 'Table', + importPath: '@astryxdesign/core/Table', + family: 'Table', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Table', + }, + { + name: 'TableBody', + importPath: '@astryxdesign/core/Table', + family: 'Table', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TableBody', + }, + { + name: 'TableCell', + importPath: '@astryxdesign/core/Table', + family: 'Table', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TableCell', + }, + { + name: 'TableFooter', + importPath: '@astryxdesign/core/Table', + family: 'Table', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TableFooter', + }, + { + name: 'TableHeader', + importPath: '@astryxdesign/core/Table', + family: 'Table', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TableHeader', + }, + { + name: 'TableHeaderCell', + importPath: '@astryxdesign/core/Table', + family: 'Table', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TableHeaderCell', + }, + { + name: 'TableRow', + importPath: '@astryxdesign/core/Table', + family: 'Table', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TableRow', + }, + { + name: 'TableSelectionToolbar', + importPath: '@astryxdesign/core/Table', + family: 'Table', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TableSelectionToolbar', + }, + { + name: 'TabList', + importPath: '@astryxdesign/core/TabList', + family: 'Tabs', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TabList', + }, + { + name: 'TabMenu', + importPath: '@astryxdesign/core/TabList', + family: 'Tabs', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/TabMenu', + }, + { + name: 'Text', + importPath: '@astryxdesign/core/Text', + family: 'Text', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Text', + }, + { + name: 'TextArea', + importPath: '@astryxdesign/core/TextArea', + family: 'TextArea', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/TextArea', + }, + { + name: 'TextInput', + importPath: '@astryxdesign/core/TextInput', + family: 'TextInput', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/TextInput', + }, + { + name: 'Theme', + importPath: '@astryxdesign/core/theme', + family: 'Utilities', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Theme', + }, + { + name: 'Thumbnail', + importPath: '@astryxdesign/core/Thumbnail', + family: 'Thumbnail', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Thumbnail', + }, + { + name: 'TimeInput', + importPath: '@astryxdesign/core/TimeInput', + family: 'TimeInput', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/TimeInput', + }, + { + name: 'Timer', + importPath: '@astryxdesign/core/Timer', + family: 'Timer', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Timer', + }, + { + name: 'Timestamp', + importPath: '@astryxdesign/core/Timestamp', + family: 'Timestamp', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Timestamp', + }, + { + name: 'Toast', + importPath: '@astryxdesign/core/Toast', + family: 'Toast', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/Toast', + }, + { + name: 'ToggleButton', + importPath: '@astryxdesign/core/ToggleButton', + family: 'Button', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/ToggleButton', + }, + { + name: 'ToggleButtonGroup', + importPath: '@astryxdesign/core/ToggleButton', + family: 'Button', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/ToggleButtonGroup', + }, + { + name: 'Token', + importPath: '@astryxdesign/core/Token', + family: 'Token', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Token', + }, + { + name: 'Tokenizer', + importPath: '@astryxdesign/core/Tokenizer', + family: 'Tokenizer', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/Tokenizer', + }, + { + name: 'Toolbar', + importPath: '@astryxdesign/core/Toolbar', + family: 'Toolbar', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/Toolbar', + }, + { + name: 'Tooltip', + importPath: '@astryxdesign/core/Tooltip', + family: 'Tooltip', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/Tooltip', + }, + { + name: 'TopNav', + importPath: '@astryxdesign/core/TopNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TopNav', + }, + { + name: 'TopNavHeading', + importPath: '@astryxdesign/core/TopNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TopNavHeading', + }, + { + name: 'TopNavItem', + importPath: '@astryxdesign/core/TopNav', + family: 'Navigation', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TopNavItem', + }, + { + name: 'TopNavMegaMenu', + importPath: '@astryxdesign/core/TopNav', + family: 'Navigation', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/TopNavMegaMenu', + }, + { + name: 'TopNavMegaMenuFeaturedCard', + importPath: '@astryxdesign/core/TopNav', + family: 'Navigation', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/TopNavMegaMenuFeaturedCard', + }, + { + name: 'TopNavMegaMenuItem', + importPath: '@astryxdesign/core/TopNav', + family: 'Navigation', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/TopNavMegaMenuItem', + }, + { + name: 'TopNavMenu', + importPath: '@astryxdesign/core/TopNav', + family: 'Navigation', + group: 'overlay', + docsUrl: 'https://astryx.atmeta.com/components/TopNavMenu', + }, + { + name: 'TreeList', + importPath: '@astryxdesign/core/TreeList', + family: 'TreeList', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/TreeList', + }, + { + name: 'Typeahead', + importPath: '@astryxdesign/core/Typeahead', + family: 'Typeahead', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/Typeahead', + }, + { + name: 'TypeaheadItem', + importPath: '@astryxdesign/core/Typeahead', + family: 'Typeahead', + group: 'form', + docsUrl: 'https://astryx.atmeta.com/components/TypeaheadItem', + }, + { + name: 'VisuallyHidden', + importPath: '@astryxdesign/core/VisuallyHidden', + family: 'VisuallyHidden', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/VisuallyHidden', + }, + { + name: 'VStack', + importPath: '@astryxdesign/core/VStack', + family: 'Layout', + group: 'base', + docsUrl: 'https://astryx.atmeta.com/components/VStack', + }, +] diff --git a/src/demos/ActionsDemo.tsx b/src/demos/ActionsDemo.tsx new file mode 100644 index 0000000..1f5f1ad --- /dev/null +++ b/src/demos/ActionsDemo.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react' +import { Button } from '@astryxdesign/core/Button' +import { HStack } from '@astryxdesign/core/HStack' +import { Icon } from '@astryxdesign/core/Icon' +import { IconButton } from '@astryxdesign/core/IconButton' +import { Kbd } from '@astryxdesign/core/Kbd' +import { Link } from '@astryxdesign/core/Link' +import { + SegmentedControl, + SegmentedControlItem, +} from '@astryxdesign/core/SegmentedControl' +import { Text } from '@astryxdesign/core/Text' +import { ToggleButton, ToggleButtonGroup } from '@astryxdesign/core/ToggleButton' +import { Toolbar } from '@astryxdesign/core/Toolbar' +import { VStack } from '@astryxdesign/core/VStack' +import { Heart, List, LayoutGrid } from 'lucide-react' + +export function ActionsDemo() { + const [layout, setLayout] = useState('网格') + const [view, setView] = useState('概览') + + return ( + <VStack gap={4}> + <Toolbar + label="常用操作" + startContent={ + <HStack gap={2} wrap="wrap"> + <Button label="主要操作" variant="primary" size="sm" /> + <Button label="次要操作" variant="secondary" size="sm" /> + <Button label="幽灵按钮" variant="ghost" size="sm" /> + </HStack> + } + endContent={ + <IconButton + label="收藏" + tooltip="收藏" + variant="ghost" + icon={<Icon icon={Heart} />} + /> + } + /> + + <HStack gap={4} wrap="wrap" vAlign="center"> + <ToggleButtonGroup + label="调整视图布局" + value={layout} + onChange={(value) => setLayout((value as string | null) ?? '网格')} + size="sm"> + <ToggleButton + label="网格" + value="网格" + icon={<Icon icon={LayoutGrid} size="sm" />} + /> + <ToggleButton + label="列表" + value="列表" + icon={<Icon icon={List} size="sm" />} + /> + </ToggleButtonGroup> + + <SegmentedControl + label="切换视图" + value={view} + onChange={setView} + size="sm"> + <SegmentedControlItem label="概览" value="概览" /> + <SegmentedControlItem label="活动" value="活动" /> + <SegmentedControlItem label="成员" value="成员" /> + </SegmentedControl> + </HStack> + + <HStack gap={3} vAlign="center" wrap="wrap"> + <Link href="/about" isStandalone> + 查看关于页 + </Link> + <Text type="supporting" size="sm"> + 快捷键 + </Text> + <Kbd keys="mod+k" /> + </HStack> + </VStack> + ) +} diff --git a/src/demos/ChatDemo.tsx b/src/demos/ChatDemo.tsx new file mode 100644 index 0000000..cf371f2 --- /dev/null +++ b/src/demos/ChatDemo.tsx @@ -0,0 +1,50 @@ +import { + ChatMessage, + ChatMessageBubble, + ChatMessageList, + ChatMessageMetadata, +} from '@astryxdesign/core/Chat' +import { Text } from '@astryxdesign/core/Text' +import { Timestamp } from '@astryxdesign/core/Timestamp' + +export function ChatDemo() { + return ( + <ChatMessageList> + <ChatMessage sender="user"> + <ChatMessageBubble group="first"> + 我把权限模块重构好了。 + </ChatMessageBubble> + <ChatMessageBubble + group="last" + metadata={ + <ChatMessageMetadata + timestamp={ + <Timestamp value="2026-04-28T14:30:00" format="time" /> + } + status="read" + /> + }> + 可以帮我检查一下令牌校验的改动吗? + </ChatMessageBubble> + </ChatMessage> + <ChatMessage sender="assistant"> + <ChatMessageBubble + variant="ghost" + metadata={ + <ChatMessageMetadata + timestamp={ + <Timestamp value="2026-04-28T14:31:00" format="time" /> + } + footer={ + <Text type="supporting" color="secondary"> + Astryx 助手 + </Text> + } + /> + }> + 看过了,刷新令牌轮换和错误处理都覆盖到了,可以发布。 + </ChatMessageBubble> + </ChatMessage> + </ChatMessageList> + ) +} diff --git a/src/demos/CommandPaletteDemo.tsx b/src/demos/CommandPaletteDemo.tsx new file mode 100644 index 0000000..e3c4119 --- /dev/null +++ b/src/demos/CommandPaletteDemo.tsx @@ -0,0 +1,46 @@ +import { useMemo, useState } from 'react' +import { Button } from '@astryxdesign/core/Button' +import { CommandPalette } from '@astryxdesign/core/CommandPalette' +import { HStack } from '@astryxdesign/core/HStack' +import { useToast } from '@astryxdesign/core/Toast' +import { createStaticSource } from '@astryxdesign/core/Typeahead' + +export function CommandPaletteDemo() { + const [open, setOpen] = useState(false) + const showToast = useToast() + const source = useMemo( + () => + createStaticSource([ + { id: 'home', label: '回到概览' }, + { id: 'components', label: '浏览组件' }, + { id: 'about', label: '查看关于' }, + { id: 'theme', label: '切换主题' }, + ]), + [], + ) + + return ( + <> + <HStack gap={2} wrap="wrap"> + <Button + label="打开命令面板" + variant="primary" + onClick={() => setOpen(true)} + /> + <Button + label="显示通知" + variant="secondary" + onClick={() => + showToast({ body: '设置已保存', type: 'info' }) + } + /> + </HStack> + <CommandPalette + isOpen={open} + onOpenChange={setOpen} + searchSource={source} + label="命令面板" + /> + </> + ) +} diff --git a/src/demos/DataDemo.tsx b/src/demos/DataDemo.tsx new file mode 100644 index 0000000..f2a5be7 --- /dev/null +++ b/src/demos/DataDemo.tsx @@ -0,0 +1,72 @@ +import { Avatar } from '@astryxdesign/core/Avatar' +import { Badge } from '@astryxdesign/core/Badge' +import { Divider } from '@astryxdesign/core/Divider' +import { HStack } from '@astryxdesign/core/HStack' +import { List, ListItem } from '@astryxdesign/core/List' +import { Text } from '@astryxdesign/core/Text' +import { Table, pixel, proportional } from '@astryxdesign/core/Table' +import type { TableColumn } from '@astryxdesign/core/Table' +import { VStack } from '@astryxdesign/core/VStack' + +interface Member extends Record<string, unknown> { + id: string + name: string + role: string + projects: number +} + +const MEMBERS: Member[] = [ + { id: '1', name: '林晓', role: '产品设计', projects: 4 }, + { id: '2', name: '陈默', role: '前端开发', projects: 7 }, + { id: '3', name: '周雨', role: '内容运营', projects: 3 }, +] + +const COLUMNS: TableColumn<Member>[] = [ + { key: 'name', header: '成员', width: proportional(1) }, + { key: 'role', header: '角色', width: proportional(1) }, + { key: 'projects', header: '进行中', width: pixel(90) }, +] + +export function DataDemo() { + return ( + <VStack gap={4}> + <List hasDividers> + <ListItem + label="林晓" + description="产品设计 · 4 个项目" + startContent={<Avatar name="林晓" size="sm" />} + endContent={<Badge variant="blue" label="3" />} + /> + <ListItem + label="陈默" + description="前端开发 · 7 个项目" + startContent={<Avatar name="陈默" size="sm" />} + endContent={<Badge variant="teal" label="8" />} + /> + <ListItem + label="周雨" + description="内容运营 · 3 个项目" + startContent={<Avatar name="周雨" size="sm" />} + endContent={<Badge variant="purple" label="2" />} + /> + </List> + + <Divider /> + + <VStack gap={2}> + <HStack gap={2} vAlign="center"> + <Text type="label">团队进度</Text> + <Text type="supporting" size="sm"> + 数据表格 + </Text> + </HStack> + <Table + data={MEMBERS} + columns={COLUMNS} + idKey="id" + density="compact" + /> + </VStack> + </VStack> + ) +} diff --git a/src/demos/DialogsDemo.tsx b/src/demos/DialogsDemo.tsx new file mode 100644 index 0000000..1b887df --- /dev/null +++ b/src/demos/DialogsDemo.tsx @@ -0,0 +1,95 @@ +import { useState } from 'react' +import { BottomSheet } from '@astryxdesign/core/BottomSheet' +import { Button } from '@astryxdesign/core/Button' +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog' +import { Drawer, DrawerHeader } from '@astryxdesign/core/Drawer' +import { HStack } from '@astryxdesign/core/HStack' +import { Layout, LayoutContent } from '@astryxdesign/core/Layout' +import { Section } from '@astryxdesign/core/Section' +import { Heading } from '@astryxdesign/core/Heading' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' + +type ActiveLayer = 'dialog' | 'drawer' | 'sheet' | null + +export function DialogsDemo() { + const [active, setActive] = useState<ActiveLayer>(null) + + return ( + <> + <HStack gap={2} wrap="wrap"> + <Button label="打开对话框" onClick={() => setActive('dialog')} /> + <Button + label="打开抽屉" + variant="secondary" + onClick={() => setActive('drawer')} + /> + <Button + label="打开底部面板" + variant="secondary" + onClick={() => setActive('sheet')} + /> + </HStack> + + <Dialog + isOpen={active === 'dialog'} + onOpenChange={(open) => setActive(open ? 'dialog' : null)}> + <Layout + header={ + <DialogHeader + title="确认发布" + onOpenChange={(open) => setActive(open ? 'dialog' : null)} + /> + } + content={ + <LayoutContent> + <Text type="body" as="p"> + 发布后,当前版本会立即对全部用户可见。你仍然可以稍后回滚。 + </Text> + </LayoutContent> + } + /> + </Dialog> + + <Drawer + isOpen={active === 'drawer'} + onOpenChange={(open) => setActive(open ? 'drawer' : null)} + label="项目详情" + width={360}> + <Layout + header={ + <DrawerHeader + title="项目详情" + onOpenChange={(open) => setActive(open ? 'drawer' : null)} + /> + } + content={ + <LayoutContent> + <VStack gap={2}> + <Heading level={3}>Astryx 起始模板</Heading> + <Text type="body" color="secondary" as="p"> + 抽屉适合承载不打断主流程的详细信息和次要操作。 + </Text> + </VStack> + </LayoutContent> + } + /> + </Drawer> + + <BottomSheet + isOpen={active === 'sheet'} + onOpenChange={(open) => setActive(open ? 'sheet' : null)} + label="筛选"> + <Section padding={4}> + <VStack gap={3}> + <Heading level={3}>筛选条件</Heading> + <Text type="body" color="secondary" as="p"> + 底部面板适合移动端上的快速筛选和短操作。 + </Text> + <Button label="应用筛选" onClick={() => setActive(null)} /> + </VStack> + </Section> + </BottomSheet> + </> + ) +} diff --git a/src/demos/FeedbackDemo.tsx b/src/demos/FeedbackDemo.tsx new file mode 100644 index 0000000..2909878 --- /dev/null +++ b/src/demos/FeedbackDemo.tsx @@ -0,0 +1,35 @@ +import { Banner } from '@astryxdesign/core/Banner' +import { HStack } from '@astryxdesign/core/HStack' +import { ProgressBar } from '@astryxdesign/core/ProgressBar' +import { Skeleton } from '@astryxdesign/core/Skeleton' +import { Spinner } from '@astryxdesign/core/Spinner' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' + +export function FeedbackDemo() { + return ( + <VStack gap={4}> + <Banner + status="info" + title="新版本已经准备好" + description="刷新页面后即可使用最新的组件与主题。" + /> + <VStack gap={2}> + <Text type="label">工作区初始化</Text> + <ProgressBar label="工作区初始化" value={68} hasValueLabel isLabelHidden /> + </VStack> + <HStack gap={4} vAlign="center" wrap="wrap"> + <HStack gap={2} vAlign="center"> + <Spinner size="sm" /> + <Text type="supporting" size="sm"> + 正在同步数据… + </Text> + </HStack> + <VStack gap={1} width={160}> + <Skeleton width="100%" height={14} index={0} /> + <Skeleton width="70%" height={14} index={1} /> + </VStack> + </HStack> + </VStack> + ) +} diff --git a/src/demos/InputsDemo.tsx b/src/demos/InputsDemo.tsx new file mode 100644 index 0000000..47c9724 --- /dev/null +++ b/src/demos/InputsDemo.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react' +import { Banner } from '@astryxdesign/core/Banner' +import { Button } from '@astryxdesign/core/Button' +import { HStack } from '@astryxdesign/core/HStack' +import { Switch } from '@astryxdesign/core/Switch' +import { TextArea } from '@astryxdesign/core/TextArea' +import { TextInput } from '@astryxdesign/core/TextInput' +import { VStack } from '@astryxdesign/core/VStack' + +export function InputsDemo() { + const [name, setName] = useState('模板项目') + const [description, setDescription] = useState( + '一个使用 Astryx 搭建的 React 应用。', + ) + const [updates, setUpdates] = useState(true) + const [saved, setSaved] = useState(false) + + return ( + <VStack gap={4}> + <TextInput + label="项目名称" + value={name} + onChange={setName} + isRequired + width="100%" + /> + <TextArea + label="项目描述" + value={description} + onChange={setDescription} + rows={3} + width="100%" + /> + <Switch + label="版本更新提醒" + description="有新版本时发送一封简短的邮件。" + value={updates} + onChange={setUpdates} + /> + <HStack gap={2} hAlign="end"> + <Button + label="保存设置" + variant="primary" + onClick={() => setSaved(true)} + /> + </HStack> + {saved ? ( + <Banner + status="success" + title="设置已保存" + description="表单值只保存在当前演示的内存中。" + isDismissable + onDismiss={() => setSaved(false)} + /> + ) : null} + </VStack> + ) +} diff --git a/src/demos/LayoutDemo.tsx b/src/demos/LayoutDemo.tsx new file mode 100644 index 0000000..53da1bd --- /dev/null +++ b/src/demos/LayoutDemo.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react' +import { Collapsible, CollapsibleGroup } from '@astryxdesign/core/Collapsible' +import { Grid } from '@astryxdesign/core/Grid' +import { Heading } from '@astryxdesign/core/Heading' +import { SelectableCard } from '@astryxdesign/core/SelectableCard' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' + +const PLANS = [ + { id: 'basic', name: '基础版', price: '¥9/月', desc: '适合个人项目' }, + { id: 'pro', name: '专业版', price: '¥29/月', desc: '适合小型团队' }, + { id: 'team', name: '团队版', price: '¥99/月', desc: '适合协作组织' }, +] + +export function LayoutDemo() { + const [selected, setSelected] = useState('pro') + + return ( + <VStack gap={4}> + <Grid columns={{ minWidth: 140, max: 3 }} gap={3}> + {PLANS.map((plan) => ( + <SelectableCard + key={plan.id} + label={plan.name} + isSelected={selected === plan.id} + onChange={() => setSelected(plan.id)}> + <VStack gap={1}> + <Heading level={4}>{plan.name}</Heading> + <Text type="large" weight="bold"> + {plan.price} + </Text> + <Text type="supporting" size="sm"> + {plan.desc} + </Text> + </VStack> + </SelectableCard> + ))} + </Grid> + + <CollapsibleGroup type="single" defaultValue="layout"> + <Collapsible + value="layout" + trigger={<Text type="label">这个布局是如何组织的?</Text>}> + <Text type="body" color="secondary" as="p"> + 外层使用 Grid 负责响应式列数,SelectableCard 负责可选中的卡片语义,Collapsible + 负责按需展开说明。所有间距都来自同一套 token。 + </Text> + </Collapsible> + </CollapsibleGroup> + </VStack> + ) +} diff --git a/src/demos/MenusDemo.tsx b/src/demos/MenusDemo.tsx new file mode 100644 index 0000000..c928e6f --- /dev/null +++ b/src/demos/MenusDemo.tsx @@ -0,0 +1,97 @@ +import { useState } from 'react' +import { Avatar } from '@astryxdesign/core/Avatar' +import { Button } from '@astryxdesign/core/Button' +import { Card } from '@astryxdesign/core/Card' +import { ContextMenu } from '@astryxdesign/core/ContextMenu' +import { DropdownMenu } from '@astryxdesign/core/DropdownMenu' +import { HStack } from '@astryxdesign/core/HStack' +import { Heading } from '@astryxdesign/core/Heading' +import { HoverCard } from '@astryxdesign/core/HoverCard' +import { MoreMenu } from '@astryxdesign/core/MoreMenu' +import { Popover } from '@astryxdesign/core/Popover' +import { Text } from '@astryxdesign/core/Text' +import { Tooltip } from '@astryxdesign/core/Tooltip' +import { VStack } from '@astryxdesign/core/VStack' + +export function MenusDemo() { + const [popoverOpen, setPopoverOpen] = useState(false) + + return ( + <VStack gap={4}> + <HStack gap={2} wrap="wrap" vAlign="center"> + <DropdownMenu + button={{ label: '操作菜单' }} + items={[ + { label: '编辑', onClick: () => {} }, + { label: '复制', onClick: () => {} }, + { label: '删除', onClick: () => {} }, + ]} + /> + <MoreMenu + items={[ + { label: '导出', onClick: () => {} }, + { label: '分享', onClick: () => {} }, + { label: '归档', onClick: () => {} }, + ]} + /> + <Tooltip content="这是一段提示文字" placement="above"> + <Button label="悬停查看提示" variant="secondary" /> + </Tooltip> + <Popover + isOpen={popoverOpen} + onOpenChange={setPopoverOpen} + hasAutoFocus={false} + placement="below" + label="设置" + width={260} + content={ + <VStack gap={2}> + <Heading level={4}>快捷设置</Heading> + <Text type="body" color="secondary" as="p"> + 弹层可以承载简短设置、筛选或上下文信息。 + </Text> + </VStack> + }> + <Button label="打开浮层" variant="secondary" /> + </Popover> + <HoverCard + placement="above" + content={ + <VStack gap={2} width={220}> + <HStack gap={2} vAlign="center"> + <Avatar name="林晓" size="md" /> + <VStack gap={0}> + <Heading level={5}>林晓</Heading> + <Text type="supporting" size="sm"> + 产品设计 + </Text> + </VStack> + </HStack> + <Text type="body" color="secondary" as="p"> + 专注于设计系统与复杂交互体验。 + </Text> + </VStack> + }> + <Button label="悬停查看资料" variant="ghost" /> + </HoverCard> + </HStack> + + <ContextMenu + presentation="adaptive" + items={[ + { label: '剪切', onClick: () => {} }, + { label: '复制', onClick: () => {} }, + { label: '粘贴', onClick: () => {} }, + ]}> + <Card padding={4}> + <VStack gap={1} hAlign="center"> + <Text type="label">右键或长按此区域</Text> + <Text type="supporting" size="sm"> + 打开上下文菜单 + </Text> + </VStack> + </Card> + </ContextMenu> + </VStack> + ) +} diff --git a/src/demos/NavigationDemo.tsx b/src/demos/NavigationDemo.tsx new file mode 100644 index 0000000..ebc75f2 --- /dev/null +++ b/src/demos/NavigationDemo.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import { BreadcrumbItem, Breadcrumbs } from '@astryxdesign/core/Breadcrumbs' +import { VStack } from '@astryxdesign/core/VStack' +import { Step, Stepper } from '@astryxdesign/core/Stepper' +import { Tab, TabList } from '@astryxdesign/core/TabList' + +export function NavigationDemo() { + const [tab, setTab] = useState('overview') + const [step, setStep] = useState(2) + + return ( + <VStack gap={5}> + <Breadcrumbs> + <BreadcrumbItem href="/">首页</BreadcrumbItem> + <BreadcrumbItem href="/components">组件</BreadcrumbItem> + <BreadcrumbItem isCurrent>导航示例</BreadcrumbItem> + </Breadcrumbs> + + <TabList value={tab} onChange={setTab} hasDivider> + <Tab value="overview" label="概览" /> + <Tab value="activity" label="活动" /> + <Tab value="settings" label="设置" /> + </TabList> + + <VStack width="100%"> + <Stepper activeStep={step} orientation="horizontal" onStepClick={setStep}> + <Step step={0} label="购物车" /> + <Step step={1} label="配送" /> + <Step step={2} label="支付" /> + <Step step={3} label="完成" /> + </Stepper> + </VStack> + </VStack> + ) +} diff --git a/src/demos/ProvidersDemo.tsx b/src/demos/ProvidersDemo.tsx new file mode 100644 index 0000000..48f5a9f --- /dev/null +++ b/src/demos/ProvidersDemo.tsx @@ -0,0 +1,29 @@ +import { Button } from '@astryxdesign/core/Button' +import { HStack } from '@astryxdesign/core/HStack' +import { Link } from '@astryxdesign/core/Link' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' +import { useThemeMode } from '../theme/useThemeMode' + +export function ProvidersDemo() { + const { mode, toggleMode } = useThemeMode() + const nextModeLabel = mode === 'light' ? '切换到深色主题' : '切换到浅色主题' + + return ( + <VStack gap={3}> + <Text type="body" color="secondary" as="p"> + 当前页面已经由 Theme、LinkProvider 和 InternationalizationProvider + 包裹。切换主题或点击站内链接即可看到实际效果。 + </Text> + <HStack gap={3} wrap="wrap" vAlign="center"> + <Button label={nextModeLabel} variant="primary" onClick={toggleMode} /> + <Link href="/about" isStandalone> + 通过 LinkProvider 打开的站内链接 + </Link> + </HStack> + <Text type="supporting" size="sm"> + Astryx 内置文案同样是中文,例如「必填」「关闭」「跳至内容」。 + </Text> + </VStack> + ) +} diff --git a/src/demos/SelectionDemo.tsx b/src/demos/SelectionDemo.tsx new file mode 100644 index 0000000..41ca918 --- /dev/null +++ b/src/demos/SelectionDemo.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react' +import { + CheckboxList, + CheckboxListItem, +} from '@astryxdesign/core/CheckboxList' +import { Grid } from '@astryxdesign/core/Grid' +import { RadioList, RadioListItem } from '@astryxdesign/core/RadioList' +import { Slider } from '@astryxdesign/core/Slider' + +export function SelectionDemo() { + const [notifications, setNotifications] = useState<string[]>(['email']) + const [plan, setPlan] = useState('team') + const [volume, setVolume] = useState(60) + + return ( + <Grid columns={{ minWidth: 220, max: 2 }} gap={5}> + <CheckboxList + label="通知方式" + description="可以选择多个渠道。" + value={notifications} + onChange={setNotifications} + hasDividers> + <CheckboxListItem + label="邮件" + value="email" + description="每周一发送摘要" + /> + <CheckboxListItem + label="推送" + value="push" + description="在设备上即时提醒" + /> + <CheckboxListItem + label="短信" + value="sms" + description="仅用于重要通知" + /> + </CheckboxList> + + <RadioList label="当前套餐" value={plan} onChange={setPlan}> + <RadioListItem label="个人版" value="personal" /> + <RadioListItem label="团队版" value="team" /> + <RadioListItem label="企业版" value="enterprise" /> + </RadioList> + + <Slider + label="提醒音量" + value={volume} + onChange={setVolume} + width="100%" + valueDisplay="text" + /> + </Grid> + ) +} diff --git a/src/demos/SelectorsDemo.tsx b/src/demos/SelectorsDemo.tsx new file mode 100644 index 0000000..5fd299f --- /dev/null +++ b/src/demos/SelectorsDemo.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react' +import { DateInput } from '@astryxdesign/core/DateInput' +import { Grid } from '@astryxdesign/core/Grid' +import { Selector } from '@astryxdesign/core/Selector' + +type DateString = + `${number}${number}${number}${number}-${number}${number}-${number}${number}` + +export function SelectorsDemo() { + const [fruit, setFruit] = useState<string | undefined>() + const [date, setDate] = useState<DateString | undefined>() + + return ( + <Grid columns={{ minWidth: 220, max: 2 }} gap={5}> + <Selector + label="最喜欢的水果" + options={['苹果', '香蕉', '橙子', '芒果', '菠萝']} + placeholder="请选择一种水果" + value={fruit} + onChange={setFruit} + width="100%" + /> + <DateInput + label="开始日期" + placeholder="选择日期" + value={date} + onChange={setDate} + hasClear + width="100%" + /> + </Grid> + ) +} diff --git a/src/demos/TypographyDemo.tsx b/src/demos/TypographyDemo.tsx new file mode 100644 index 0000000..75842d9 --- /dev/null +++ b/src/demos/TypographyDemo.tsx @@ -0,0 +1,36 @@ +import { Blockquote } from '@astryxdesign/core/Blockquote' +import { CodeBlock } from '@astryxdesign/core/CodeBlock' +import { Divider } from '@astryxdesign/core/Divider' +import { Heading } from '@astryxdesign/core/Heading' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' + +const SNIPPET = `const theme = defineTheme({ + name: 'ocean', + tokens: {'--color-accent': '#0077B6'}, +})` + +export function TypographyDemo() { + return ( + <VStack gap={3}> + <VStack gap={1}> + <Heading level={3}>清晰的内容层级</Heading> + <Text type="body" color="secondary" as="p"> + 标题、正文、辅助文字和代码共享同一套字体与节奏,让界面在中文和英文下都保持稳定。 + </Text> + </VStack> + <Blockquote cite="Astryx 设计原则"> + 把复杂性留在系统里,把清晰留给界面。 + </Blockquote> + <Divider /> + <CodeBlock + code={SNIPPET} + language="tsx" + title="theme.ts" + hasLineNumbers={false} + width="100%" + size="sm" + /> + </VStack> + ) +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..d589761 --- /dev/null +++ b/src/index.css @@ -0,0 +1,2 @@ +@import '@astryxdesign/core/reset.css'; +@import '@astryxdesign/core/astryx.css'; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..95ac71e --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,19 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import { InternationalizationProvider } from '@astryxdesign/core/i18n' +import zhCN from '@astryxdesign/core/locales/zh-CN.generated.js' +import { App } from './App' +import { ThemeModeProvider } from './theme/ThemeMode' + +createRoot(document.getElementById('root')!).render( + <StrictMode> + <InternationalizationProvider + locale="zh-CN" + messages={{ 'zh-CN': zhCN }}> + <ThemeModeProvider> + <App /> + </ThemeModeProvider> + </InternationalizationProvider> + </StrictMode>, +) diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx new file mode 100644 index 0000000..5f2aec2 --- /dev/null +++ b/src/pages/AboutPage.tsx @@ -0,0 +1,202 @@ +import { CodeBlock } from '@astryxdesign/core/CodeBlock' +import { Divider } from '@astryxdesign/core/Divider' +import { Grid } from '@astryxdesign/core/Grid' +import { Heading } from '@astryxdesign/core/Heading' +import { Icon } from '@astryxdesign/core/Icon' +import { List, ListItem } from '@astryxdesign/core/List' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' +import { + ArrowRight, + Atom, + Blocks, + BookOpen, + ExternalLink, + Palette, + Route, + Shapes, + Zap, +} from 'lucide-react' +import { Page, PageHeader } from '../components/Page' + +const COMMANDS = `npm run dev # 启动开发服务器 +npm run build # 类型检查并构建生产版本 +npm run lint # 运行 oxlint +npm run preview # 预览生产构建 + +npm run astryx -- component Button # 查看 Astryx 组件文档` + +const PROJECT_TREE = `src/ + components/ + AppLayout.tsx 应用外壳、顶部导航、侧边导航 + Page.tsx 共用页面框架与页头 + pages/ + HomePage.tsx + ComponentsPage.tsx + AboutPage.tsx + NotFoundPage.tsx + theme/ + ThemeMode.tsx 明暗主题提供者 + astryx-themes.ts 生成的主题注册表 + router.tsx React Router 路由表 + App.tsx 提供者与路由 + main.tsx 入口文件` + +const STACK = [ + { + name: 'React', + version: '19.3', + icon: Atom, + description: '界面运行时与 Hooks', + }, + { + name: 'Vite', + version: '8.3', + icon: Zap, + description: '开发服务器与生产构建工具', + }, + { + name: 'React Router', + version: '7.18', + icon: Route, + description: '基于数据路由的客户端路由,可在 hash 与 history 模式间切换', + }, + { + name: 'Astryx', + version: '0.7.2', + icon: Palette, + description: '设计系统、组件与 neutral 主题', + }, +] + +export function AboutPage() { + return ( + <Page> + <VStack gap={8}> + <PageHeader + eyebrow="关于" + title="是起点,不是框架。" + description="项目把每一部分都保持得小而可见:一张路由表、一套布局、一个主题提供者,以及普通的 React 组件。你可以直接替换需要改的部分,而不用和隐藏的约定较劲。" + /> + + <Divider /> + + <Grid columns={{ minWidth: 320, max: 2 }} gap={8}> + <VStack gap={4}> + <Heading level={2}>技术栈</Heading> + <List hasDividers> + {STACK.map(({ name, version, icon, description }) => ( + <ListItem + key={name} + label={name} + description={description} + startContent={ + <Icon icon={icon} size="sm" color="secondary" /> + } + endContent={ + <Text type="supporting" size="sm"> + {version} + </Text> + } + /> + ))} + </List> + </VStack> + + <VStack gap={4}> + <Heading level={2}>常用命令</Heading> + <CodeBlock + code={COMMANDS} + language="bash" + title="终端" + size="sm" + hasLanguageLabel={false} + width="100%" + /> + <Text type="supporting" size="sm"> + Vite 8 需要 Node 20.19+。 + </Text> + </VStack> + </Grid> + + <Divider /> + + <VStack gap={4} maxWidth={920}> + <Heading level={2}>项目结构</Heading> + <Text type="body" color="secondary" as="p"> + 在 <Text type="code">src/pages</Text> 下新建页面,然后在{' '} + <Text type="code">src/router.tsx</Text> 中注册路由,并在{' '} + <Text type="code">src/components/AppLayout.tsx</Text>{' '} + 中添加对应的导航项。 + </Text> + <CodeBlock + code={PROJECT_TREE} + language="plaintext" + title="项目结构" + width="100%" + /> + </VStack> + + <Divider /> + + <VStack gap={4}> + <Heading level={2}>继续探索</Heading> + <List hasDividers> + <ListItem + label="组件" + description="浏览全部 173 个组件的实时交互示例。" + href="/components" + startContent={<Icon icon={Blocks} size="sm" color="secondary" />} + endContent={<Icon icon={ArrowRight} size="sm" color="secondary" />} + /> + <ListItem + label="图标" + description="浏览 Astryx 语义图标和 Lucide 图标库。" + href="/icons" + startContent={<Icon icon={Shapes} size="sm" color="secondary" />} + endContent={<Icon icon={ArrowRight} size="sm" color="secondary" />} + /> + <ListItem + label="Astryx 文档" + description="组件接口、主题、布局指南和页面模板。" + href="https://astryx.atmeta.com" + target="_blank" + startContent={<Icon icon={BookOpen} size="sm" color="secondary" />} + endContent={ + <Icon icon={ExternalLink} size="sm" color="secondary" /> + } + /> + <ListItem + label="React Router 文档" + description="路由、数据加载、动作和导航。" + href="https://reactrouter.com" + target="_blank" + startContent={<Icon icon={Route} size="sm" color="secondary" />} + endContent={ + <Icon icon={ExternalLink} size="sm" color="secondary" /> + } + /> + </List> + </VStack> + + <Divider /> + + <VStack gap={2}> + <Heading level={3}>主题</Heading> + <Text type="body" color="secondary" as="p"> + 自动生成的 <Text type="code">src/astryx-themes.ts</Text>{' '} + 文件注册了 neutral 主题。可以运行{' '} + <Text type="code">npm run astryx -- theme add <slug></Text>{' '} + 添加其他主题包,然后在{' '} + <Text type="code">src/theme/ThemeMode.tsx</Text>{' '} + 中把它传给主题提供者。 + </Text> + <Text type="supporting" size="sm"> + Astryx 自带预构建 CSS,因此这个应用不需要 StyleX、PostCSS 或 Babel + 插件。 + </Text> + </VStack> + </VStack> + </Page> + ) +} diff --git a/src/pages/ComponentsPage.tsx b/src/pages/ComponentsPage.tsx new file mode 100644 index 0000000..8191acb --- /dev/null +++ b/src/pages/ComponentsPage.tsx @@ -0,0 +1,422 @@ +import { Fragment, useMemo, useState, type ReactNode } from 'react' +import { Card } from '@astryxdesign/core/Card' +import { Center } from '@astryxdesign/core/Center' +import { Collapsible, CollapsibleGroup } from '@astryxdesign/core/Collapsible' +import { Divider } from '@astryxdesign/core/Divider' +import { Grid } from '@astryxdesign/core/Grid' +import { Heading } from '@astryxdesign/core/Heading' +import { HStack } from '@astryxdesign/core/HStack' +import { Icon } from '@astryxdesign/core/Icon' +import { Section } from '@astryxdesign/core/Section' +import { Text } from '@astryxdesign/core/Text' +import { TextInput } from '@astryxdesign/core/TextInput' +import { ToggleButton, ToggleButtonGroup } from '@astryxdesign/core/ToggleButton' +import { VStack } from '@astryxdesign/core/VStack' +import { + Blocks, + Layers, + Search, + SlidersHorizontal, +} from 'lucide-react' +import { Page, PageHeader } from '../components/Page' +import { ASTRYX_COMPONENTS, type AstryxComponentGroup } from '../data/astryxComponents' +import { ActionsDemo } from '../demos/ActionsDemo' +import { ChatDemo } from '../demos/ChatDemo' +import { CommandPaletteDemo } from '../demos/CommandPaletteDemo' +import { DataDemo } from '../demos/DataDemo' +import { DialogsDemo } from '../demos/DialogsDemo' +import { FeedbackDemo } from '../demos/FeedbackDemo' +import { InputsDemo } from '../demos/InputsDemo' +import { LayoutDemo } from '../demos/LayoutDemo' +import { MenusDemo } from '../demos/MenusDemo' +import { NavigationDemo } from '../demos/NavigationDemo' +import { ProvidersDemo } from '../demos/ProvidersDemo' +import { SelectionDemo } from '../demos/SelectionDemo' +import { SelectorsDemo } from '../demos/SelectorsDemo' +import { TypographyDemo } from '../demos/TypographyDemo' + +interface DemoDefinition { + id: string + group: AstryxComponentGroup + title: string + description: string + preview: ReactNode + components: string[] +} + +const DEMOS: DemoDefinition[] = [ + { + id: 'actions', + group: 'base', + title: '操作与链接', + description: '按钮、图标按钮、工具栏、切换按钮和链接共享同一套交互状态。', + preview: <ActionsDemo />, + components: [ + 'Button', 'ButtonGroup', 'IconButton', 'ToggleButton', 'ToggleButtonGroup', + 'SegmentedControl', 'SegmentedControlItem', 'Link', 'Kbd', 'Toolbar', 'Icon', + ], + }, + { + id: 'typography', + group: 'base', + title: '文字与内容', + description: '标题、正文、引用、代码块和分隔线共同建立清晰的内容层级。', + preview: <TypographyDemo />, + components: [ + 'Text', 'Heading', 'Blockquote', 'Code', 'CodeBlock', 'Markdown', 'Citation', + 'Token', 'Divider', 'VisuallyHidden', 'Timer', 'Timestamp', + ], + }, + { + id: 'layout', + group: 'base', + title: '布局与卡片', + description: '响应式网格、可选卡片和折叠面板组成的真实布局结构。', + preview: <LayoutDemo />, + components: [ + 'Stack', 'HStack', 'VStack', 'Grid', 'GridSpan', 'Center', 'Section', 'Card', + 'ClickableCard', 'SelectableCard', 'Collapsible', 'CollapsibleGroup', + 'ScrollableArea', 'Layout', 'LayoutHeader', 'LayoutContent', 'LayoutFooter', + 'LayoutPanel', 'Outline', 'OverflowList', 'AspectRatio', 'Thumbnail', + 'Carousel', 'MediaTheme', + ], + }, + { + id: 'data', + group: 'base', + title: '列表、表格与头像', + description: '列表、表格、头像和计数徽标用于呈现结构化数据。', + preview: <DataDemo />, + components: [ + 'List', 'ListItem', 'Item', 'MetadataList', 'MetadataListItem', 'Table', + 'TableBody', 'TableCell', 'TableFooter', 'TableHeader', 'TableHeaderCell', + 'TableRow', 'TableSelectionToolbar', 'TreeList', 'EmptyState', 'Skeleton', + 'Spinner', 'Avatar', 'AvatarGroup', 'AvatarGroupOverflow', 'AvatarStatusDot', + 'Badge', 'StatusDot', 'NavIcon', + ], + }, + { + id: 'providers', + group: 'base', + title: '主题与全局能力', + description: '主题、链接和国际化 Provider 已经在页面中生效。', + preview: <ProvidersDemo />, + components: ['Theme', 'MediaTheme', 'LinkProvider', 'InternationalizationProvider'], + }, + { + id: 'feedback', + group: 'base', + title: '反馈与状态', + description: '用横幅、进度、加载指示和骨架屏表达系统状态。', + preview: <FeedbackDemo />, + components: ['Banner', 'ProgressBar', 'Spinner', 'Skeleton', 'EmptyState'], + }, + { + id: 'navigation', + group: 'base', + title: '导航与步骤', + description: '面包屑、标签页和步骤条帮助用户在层级和流程中定位。', + preview: <NavigationDemo />, + components: [ + 'AppShell', 'TopNav', 'TopNavHeading', 'TopNavItem', 'TopNavMenu', + 'TopNavMegaMenu', 'TopNavMegaMenuFeaturedCard', 'TopNavMegaMenuItem', + 'SideNav', 'SideNavCollapseButton', 'SideNavHeading', 'SideNavItem', + 'SideNavSection', 'MobileNav', 'Breadcrumbs', 'BreadcrumbItem', 'Pagination', + 'TabList', 'Tab', 'TabMenu', 'Stepper', 'Step', + ], + }, + { + id: 'chat', + group: 'base', + title: '聊天消息', + description: '消息列表、气泡、元数据和时间戳组成的对话界面。', + preview: <ChatDemo />, + components: [ + 'ChatComposer', 'ChatComposerDrawer', 'ChatComposerInput', 'ChatDictationButton', + 'ChatLayout', 'ChatLayoutScrollButton', 'ChatMessage', 'ChatMessageBubble', + 'ChatMessageList', 'ChatMessageMetadata', 'ChatSendButton', 'ChatSystemMessage', + 'ChatTokenizedText', 'ChatToolCalls', + ], + }, + { + id: 'inputs', + group: 'form', + title: '文本输入与校验', + description: '文本、长文本、开关和保存反馈组成的表单输入场景。', + preview: <InputsDemo />, + components: [ + 'TextInput', 'TextArea', 'NumberInput', 'InputGroup', 'InputGroupText', + 'Field', 'FieldLabel', 'FieldStatus', 'FormLayout', 'FileInput', 'PowerSearch', + ], + }, + { + id: 'selection', + group: 'form', + title: '选择控件', + description: '复选框、单选框和滑杆展示不同粒度的选择操作。', + preview: <SelectionDemo />, + components: [ + 'CheckboxInput', 'CheckboxList', 'CheckboxListItem', 'RadioList', + 'RadioListItem', 'Switch', 'Slider', + ], + }, + { + id: 'selectors', + group: 'form', + title: '选择器与日期', + description: '下拉选择、日期输入和更多选择模式集中在同一场景。', + preview: <SelectorsDemo />, + components: [ + 'Selector', 'SelectorOption', 'MultiSelector', 'ComplexSelector', 'Typeahead', + 'BaseTypeahead', 'TypeaheadItem', 'Tokenizer', 'Calendar', 'DateInput', + 'DateRangeInput', 'DateTimeInput', 'TimeInput', + ], + }, + { + id: 'dialogs', + group: 'overlay', + title: '对话框、抽屉与底部面板', + description: '点击按钮打开真实弹层,体验遮罩、焦点管理和关闭流程。', + preview: <DialogsDemo />, + components: [ + 'Dialog', 'AlertDialog', 'DialogHeader', 'Drawer', 'DrawerHeader', + 'BottomSheet', 'BottomSheetSwitcher', 'Overlay', + ], + }, + { + id: 'menus', + group: 'overlay', + title: '菜单与浮层', + description: '下拉菜单、上下文菜单、弹出框、提示和悬停卡片。', + preview: <MenusDemo />, + components: [ + 'DropdownMenu', 'DropdownMenuCheckboxItem', 'DropdownMenuDivider', + 'DropdownMenuGroup', 'DropdownMenuItem', 'DropdownMenuRadioGroup', + 'DropdownMenuRadioItem', 'DropdownMenuSubMenu', 'ContextMenu', 'MoreMenu', + 'Popover', 'Tooltip', 'HoverCard', + ], + }, + { + id: 'command', + group: 'overlay', + title: '命令面板与通知', + description: '命令面板把搜索、分组结果和键盘操作集中在一个浮层里。', + preview: <CommandPaletteDemo />, + components: [ + 'CommandPalette', 'CommandPaletteEmpty', 'CommandPaletteFooter', + 'CommandPaletteGroup', 'CommandPaletteInput', 'CommandPaletteItem', + 'CommandPaletteList', 'Toast', 'Lightbox', + ], + }, +] + +const GROUPS = [ + { + id: 'base', + label: '基础组件', + icon: Blocks, + description: '布局、排版、内容、操作、反馈、导航和数据展示等通用构建块。', + }, + { + id: 'form', + label: '表单组件', + icon: SlidersHorizontal, + description: '输入、选择、日期、校验和表单布局控件。', + }, + { + id: 'overlay', + label: '弹层组件', + icon: Layers, + description: '对话框、抽屉、菜单、浮层、提示和命令面板。', + }, +] as const + +type Filter = 'all' | AstryxComponentGroup + +const GROUP_COUNTS = GROUPS.reduce( + (counts, group) => { + counts[group.id] = ASTRYX_COMPONENTS.filter( + (component) => component.group === group.id, + ).length + return counts + }, + { base: 0, form: 0, overlay: 0 } as Record<AstryxComponentGroup, number>, +) + +function DemoCard({ demo }: { demo: DemoDefinition }) { + return ( + <Card padding={0}> + <Section variant="muted" padding={5}> + {demo.preview} + </Section> + <Section variant="transparent" padding={5}> + <VStack gap={2}> + <Heading level={3}>{demo.title}</Heading> + <Text type="body" color="secondary" as="p"> + {demo.description} + </Text> + <Text type="supporting" size="sm" maxLines={2}> + 覆盖组件:{demo.components.join('、')} + </Text> + </VStack> + </Section> + </Card> + ) +} + +export function ComponentsPage() { + const [filter, setFilter] = useState<Filter>('all') + const [query, setQuery] = useState('') + + const results = useMemo(() => { + const normalized = query.trim().toLowerCase() + + return DEMOS.filter((demo) => { + const matchesGroup = filter === 'all' || demo.group === filter + const matchesQuery = + normalized.length === 0 || + demo.title.toLowerCase().includes(normalized) || + demo.description.toLowerCase().includes(normalized) || + demo.components.some((name) => name.toLowerCase().includes(normalized)) + + return matchesGroup && matchesQuery + }) + }, [filter, query]) + + const sections = GROUPS.map((group) => ({ + ...group, + demos: results.filter((demo) => demo.group === group.id), + })).filter((group) => group.demos.length > 0) + + const coveredComponents = new Set(results.flatMap((demo) => demo.components)).size + const isFiltering = filter !== 'all' || query.trim().length > 0 + + return ( + <Page> + <VStack gap={8}> + <PageHeader + eyebrow="实时预览" + title="组件" + description={`通过可交互的组合场景展示 Astryx 组件的使用效果。全部 ${ASTRYX_COMPONENTS.length} 个组件按基础、表单和弹层分类整理。`} + /> + + <VStack gap={2}> + <Text type="supporting" size="sm"> + 组件名称与 API 保留 Astryx 原始命名;示例中的操作和内容均可直接体验。 + </Text> + <HStack gap={3} wrap="wrap"> + <Text type="supporting" size="sm"> + 基础组件 {GROUP_COUNTS.base} + </Text> + <Text type="supporting" size="sm"> + 表单组件 {GROUP_COUNTS.form} + </Text> + <Text type="supporting" size="sm"> + 弹层组件 {GROUP_COUNTS.overlay} + </Text> + </HStack> + </VStack> + + <Divider /> + + <VStack gap={4}> + <HStack gap={4} wrap="wrap" vAlign="end"> + <TextInput + label="搜索组件" + isLabelHidden + placeholder="搜索组件名或场景" + value={query} + onChange={setQuery} + startIcon={Search} + size="lg" + width={360} + /> + <ToggleButtonGroup + label="按组件分类筛选" + value={filter} + onChange={(value) => setFilter((value as Filter | null) ?? 'all')} + size="sm"> + <ToggleButton label="全部" value="all" /> + {GROUPS.map((group) => ( + <ToggleButton + key={group.id} + label={group.label} + value={group.id} + /> + ))} + </ToggleButtonGroup> + </HStack> + <Text type="supporting" size="sm"> + {isFiltering + ? `${results.length} 个场景,覆盖 ${coveredComponents} 个组件` + : `${DEMOS.length} 个场景,覆盖 ${coveredComponents} 个组件`} + </Text> + </VStack> + + {results.length === 0 ? ( + <Center minHeight={220}> + <VStack gap={2} hAlign="center"> + <Text type="label">没有找到匹配的场景。</Text> + <Text type="supporting" color="secondary"> + 试试更宽泛的关键词,或切换回「全部」。 + </Text> + </VStack> + </Center> + ) : ( + sections.map((group, index) => ( + <Fragment key={group.id}> + {index > 0 ? <Divider /> : null} + <VStack gap={4}> + <VStack gap={1}> + <HStack gap={2} vAlign="center" wrap="wrap"> + <Icon icon={group.icon} size="md" color="accent" /> + <Heading level={2}>{group.label}</Heading> + <Text type="supporting" size="sm"> + {group.demos.length} 个场景 + </Text> + </HStack> + <Text type="body" color="secondary" as="p"> + {group.description} + </Text> + </VStack> + <Grid columns={{ minWidth: 460, max: 2 }} gap={4} align="start"> + {group.demos.map((demo) => ( + <DemoCard key={demo.id} demo={demo} /> + ))} + </Grid> + </VStack> + </Fragment> + )) + )} + + <Divider /> + + <CollapsibleGroup type="single"> + <Collapsible + value="all-components" + trigger={<Text type="label">查看完整组件清单</Text>}> + <VStack gap={3}> + {GROUPS.map((group) => ( + <VStack key={group.id} gap={1}> + <Text type="label">{group.label}</Text> + <Text type="supporting" size="sm"> + {ASTRYX_COMPONENTS.filter( + (component) => component.group === group.id, + ) + .map((component) => component.name) + .join('、')} + </Text> + </VStack> + ))} + </VStack> + </Collapsible> + </CollapsibleGroup> + + <Text type="supporting" size="sm"> + 共收录 {ASTRYX_COMPONENTS.length} 个组件,数据来自 + @astryxdesign/core 0.7.2。 + </Text> + </VStack> + </Page> + ) +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 0000000..a1e7668 --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1,219 @@ +import { Button } from '@astryxdesign/core/Button' +import { Card } from '@astryxdesign/core/Card' +import { CodeBlock } from '@astryxdesign/core/CodeBlock' +import { Divider } from '@astryxdesign/core/Divider' +import { Grid } from '@astryxdesign/core/Grid' +import { Heading } from '@astryxdesign/core/Heading' +import { HStack } from '@astryxdesign/core/HStack' +import { Icon } from '@astryxdesign/core/Icon' +import { List, ListItem } from '@astryxdesign/core/List' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' +import { + ArrowRight, + Atom, + Check, + ExternalLink, + Layers, + Moon, + Palette, + Route, + Sparkles, + Terminal, + Zap, +} from 'lucide-react' +import { Page } from '../components/Page' + +const STACK = [ + { + name: 'Vite', + version: '8.x', + icon: Zap, + description: '极速开发服务器、快速热更新,以及经过优化的生产构建。', + }, + { + name: 'React', + version: '19.x', + icon: Atom, + description: '现代 Hooks 与全应用共享的组件模型。', + }, + { + name: 'React Router', + version: '7.x', + icon: Route, + description: '嵌套路由、持续存在的应用外壳,以及类型安全的导航。', + }, + { + name: 'Astryx', + version: '0.7', + icon: Palette, + description: '无障碍组件、设计令牌、主题与深色模式。', + }, +] + +const QUICK_START = `npm install +npm run dev` + +const BUILT_IN = [ + { + label: '应用外壳', + description: + '顶部导航、可折叠侧边栏和移动端抽屉都已替你处理好。', + icon: Layers, + }, + { + label: '客户端路由', + description: + '所有页面共用一套布局,可在根目录配置中切换 hash 与 history 路由。', + icon: Route, + }, + { + label: '明暗主题', + description: + 'Astryx neutral 主题可切换模式,并记住你的选择。', + icon: Moon, + }, + { + label: '完整组件目录', + description: + '全部 173 个 Astryx 组件,按基础、表单和弹层分类。', + icon: Sparkles, + }, +] + +export function HomePage() { + return ( + <Page> + <VStack gap={10}> + <VStack gap={5} maxWidth={860}> + <Text type="label" color="accent"> + Vite + React + React Router + Astryx + </Text> + <VStack gap={3}> + <Heading level={1} type="display-2" textWrap="balance"> + 为你的下一个 React 应用,准备更好的起点。 + </Heading> + <Text type="large" color="secondary" as="p" textWrap="pretty"> + Vite 让反馈循环保持飞快,React Router + 给应用真正的结构,Astryx 提供无障碍组件与主题。所有部分都已经连接好。 + </Text> + </VStack> + <HStack gap={3} wrap="wrap"> + <Button + label="浏览组件" + variant="primary" + href="/components" + endContent={<Icon icon={ArrowRight} size="sm" color="inherit" />} + /> + <Button + label="Astryx 文档" + variant="secondary" + href="https://astryx.atmeta.com" + target="_blank" + rel="noreferrer" + endContent={ + <Icon icon={ExternalLink} size="sm" color="inherit" /> + } + /> + </HStack> + </VStack> + + <Divider /> + + <VStack gap={5}> + <VStack gap={2} maxWidth={680}> + <Text type="label" color="accent"> + 技术栈 + </Text> + <Heading level={2}>四件工具,已经连接好。</Heading> + <Text type="body" color="secondary" as="p"> + 无需构建插件,也无需额外配置 CSS。Astryx + 直接提供预构建样式,项目里能看到的导入就是全部配置。 + </Text> + </VStack> + <Grid columns={{ minWidth: 230, max: 4 }} gap={6}> + {STACK.map(({ name, version, icon, description }) => ( + <HStack key={name} gap={3} vAlign="start"> + <Icon icon={icon} size="md" color="accent" /> + <VStack gap={1}> + <HStack gap={2} vAlign="center" wrap="wrap"> + <Text type="label">{name}</Text> + <Text type="supporting" size="sm"> + {version} + </Text> + </HStack> + <Text type="supporting" size="sm" as="p"> + {description} + </Text> + </VStack> + </HStack> + ))} + </Grid> + </VStack> + + <Divider /> + + <Grid columns={{ minWidth: 320, max: 2 }} gap={8}> + <VStack gap={4}> + <VStack gap={1}> + <Heading level={2}>已经接好的能力</Heading> + <Text type="body" color="secondary" as="p"> + 一些方便替换、能随应用一起成长的小默认值。 + </Text> + </VStack> + <List hasDividers> + {BUILT_IN.map(({ label, description, icon }) => ( + <ListItem + key={label} + label={label} + description={description} + startContent={ + <Icon icon={icon} size="sm" color="secondary" /> + } + endContent={ + <Icon icon={Check} size="sm" color="success" /> + } + /> + ))} + </List> + </VStack> + + <Card padding={6} elevation="low"> + <VStack gap={4}> + <VStack gap={1}> + <Heading level={2}>快速开始</Heading> + <Text type="body" color="secondary" as="p"> + 安装依赖、启动 Vite,然后编辑{' '} + <Text type="code">src/</Text> 下的任意文件。 + </Text> + </VStack> + <CodeBlock + code={QUICK_START} + language="bash" + title="终端" + hasLanguageLabel={false} + width="100%" + /> + <HStack gap={2} vAlign="center"> + <Icon icon={Terminal} size="sm" color="secondary" /> + <Text type="supporting" size="sm"> + 需要 Node 20.19+ 和 npm 10+。 + </Text> + </HStack> + </VStack> + </Card> + </Grid> + + <Divider /> + + <HStack gap={2} vAlign="center"> + <Icon icon={Sparkles} size="sm" color="accent" /> + <Text type="supporting" size="sm"> + 基于 Astryx v0.7.2 和 neutral 主题构建。可在 + src/astryx-themes.ts 中更改主题。 + </Text> + </HStack> + </VStack> + </Page> + ) +} diff --git a/src/pages/IconsPage.tsx b/src/pages/IconsPage.tsx new file mode 100644 index 0000000..6b836ca --- /dev/null +++ b/src/pages/IconsPage.tsx @@ -0,0 +1,334 @@ +import { useMemo, useState } from 'react' +import { Button } from '@astryxdesign/core/Button' +import { Center } from '@astryxdesign/core/Center' +import { ClickableCard } from '@astryxdesign/core/ClickableCard' +import { Divider } from '@astryxdesign/core/Divider' +import { Grid } from '@astryxdesign/core/Grid' +import { Heading } from '@astryxdesign/core/Heading' +import { HStack } from '@astryxdesign/core/HStack' +import { Icon } from '@astryxdesign/core/Icon' +import type { IconName } from '@astryxdesign/core/Icon' +import { Text } from '@astryxdesign/core/Text' +import { TextInput } from '@astryxdesign/core/TextInput' +import { useToast } from '@astryxdesign/core/Toast' +import { ToggleButton, ToggleButtonGroup } from '@astryxdesign/core/ToggleButton' +import { VStack } from '@astryxdesign/core/VStack' +import { + ChevronLeft, + ChevronRight, + icons, + Search, + type LucideIcon, +} from 'lucide-react' +import { Page, PageHeader } from '../components/Page' + +type IconSet = 'semantic' | 'lucide' + +interface SemanticIcon { + name: IconName + label: string +} + +const SEMANTIC_ICONS: SemanticIcon[] = [ + { name: 'close', label: '关闭' }, + { name: 'chevronDown', label: '向下展开' }, + { name: 'chevronLeft', label: '向左' }, + { name: 'chevronRight', label: '向右' }, + { name: 'chevronsLeft', label: '跳到开头' }, + { name: 'chevronsRight', label: '跳到末尾' }, + { name: 'check', label: '确认' }, + { name: 'success', label: '成功' }, + { name: 'error', label: '错误' }, + { name: 'warning', label: '警告' }, + { name: 'info', label: '信息' }, + { name: 'calendar', label: '日历' }, + { name: 'clock', label: '时间' }, + { name: 'externalLink', label: '外部链接' }, + { name: 'menu', label: '菜单' }, + { name: 'moreHorizontal', label: '更多' }, + { name: 'search', label: '搜索' }, + { name: 'upload', label: '上传' }, + { name: 'arrowUp', label: '向上' }, + { name: 'arrowDown', label: '向下' }, + { name: 'arrowsUpDown', label: '排序' }, + { name: 'funnel', label: '筛选' }, + { name: 'eyeSlash', label: '隐藏' }, + { name: 'viewColumns', label: '列设置' }, + { name: 'copy', label: '复制' }, + { name: 'checkDouble', label: '已复制' }, + { name: 'wrench', label: '设置' }, + { name: 'stop', label: '停止' }, + { name: 'microphone', label: '麦克风' }, +] + +interface LucideEntry { + name: string + Icon: LucideIcon +} + +const LUCIDE_ICONS: LucideEntry[] = Object.entries(icons) + .map(([name, Icon]) => ({ name, Icon })) + .sort((a, b) => a.name.localeCompare(b.name)) + +const LUCIDE_PAGE_SIZE = 120 + +export function IconsPage() { + const [activeSet, setActiveSet] = useState<IconSet>('semantic') + const [query, setQuery] = useState('') + const [page, setPage] = useState(1) + const showToast = useToast() + + const changeSet = (value: string | null) => { + setActiveSet(value === 'lucide' ? 'lucide' : 'semantic') + setPage(1) + } + + const changeQuery = (value: string) => { + setQuery(value) + setPage(1) + } + + const copyUsage = async (code: string, name: string) => { + try { + if (!navigator.clipboard) throw new Error('Clipboard unavailable') + await navigator.clipboard.writeText(code) + showToast({ body: `已复制 ${name} 的用法`, type: 'info' }) + } catch { + showToast({ body: '复制失败,请检查浏览器权限', type: 'error' }) + } + } + + const normalizedQuery = query.trim().toLowerCase() + + const filteredSemantic = useMemo( + () => + SEMANTIC_ICONS.filter( + (icon) => + normalizedQuery.length === 0 || + icon.name.toLowerCase().includes(normalizedQuery) || + icon.label.includes(query.trim()), + ), + [normalizedQuery, query], + ) + + const filteredLucide = useMemo( + () => + normalizedQuery.length === 0 + ? LUCIDE_ICONS + : LUCIDE_ICONS.filter((icon) => + icon.name.toLowerCase().includes(normalizedQuery), + ), + [normalizedQuery], + ) + + const totalPages = Math.max(1, Math.ceil(filteredLucide.length / LUCIDE_PAGE_SIZE)) + const currentPage = Math.min(page, totalPages) + const pageStart = (currentPage - 1) * LUCIDE_PAGE_SIZE + const pageItems = filteredLucide.slice( + pageStart, + pageStart + LUCIDE_PAGE_SIZE, + ) + + const isSemantic = activeSet === 'semantic' + const visibleCount = isSemantic ? filteredSemantic.length : filteredLucide.length + + return ( + <Page> + <VStack gap={8}> + <PageHeader + eyebrow="图标资源" + title="图标" + description={`浏览 ${SEMANTIC_ICONS.length} 个 Astryx 语义图标和 ${LUCIDE_ICONS.length} 个 Lucide 图标。点击任意图标即可复制它的用法。`} + /> + + <Divider /> + + <VStack gap={4}> + <HStack gap={4} wrap="wrap" vAlign="end"> + <TextInput + label="搜索图标" + isLabelHidden + placeholder="搜索图标名称" + value={query} + onChange={changeQuery} + startIcon={Search} + size="lg" + width={360} + /> + <ToggleButtonGroup + label="选择图标集" + value={activeSet} + onChange={changeSet} + size="sm"> + <ToggleButton + label={`语义图标 ${SEMANTIC_ICONS.length}`} + value="semantic" + /> + <ToggleButton + label={`Lucide ${LUCIDE_ICONS.length}`} + value="lucide" + /> + </ToggleButtonGroup> + </HStack> + <Text type="supporting" size="sm"> + {isSemantic + ? '语义图标通过名称使用,会自动适配当前主题。' + : 'Lucide 图标以组件形式传入 Icon,可与语义图标混用。'} + </Text> + </VStack> + + {visibleCount === 0 ? ( + <Center minHeight={220}> + <VStack gap={2} hAlign="center"> + <Icon icon={Search} size="lg" color="secondary" /> + <Text type="label">没有找到匹配的图标。</Text> + <Text type="supporting" color="secondary"> + 试试更短的关键词。 + </Text> + </VStack> + </Center> + ) : isSemantic ? ( + <> + <Text type="supporting" size="sm"> + 共 {filteredSemantic.length} 个语义图标 + </Text> + <Grid columns={{ minWidth: 160, max: 4 }} gap={3}> + {filteredSemantic.map((semanticIcon) => ( + <ClickableCard + key={semanticIcon.name} + padding={4} + onClick={() => + void copyUsage( + `<Icon icon="${semanticIcon.name}" size="lg" />`, + semanticIcon.name, + ) + } + label={`复制 ${semanticIcon.name} 图标用法`}> + <VStack gap={2} hAlign="center"> + <Icon + icon={semanticIcon.name} + size="lg" + color="accent" + /> + <Text + type="code" + size="sm" + display="block" + justify="center" + maxLines={1}> + {semanticIcon.name} + </Text> + <Text + type="supporting" + size="sm" + display="block" + justify="center"> + {semanticIcon.label} + </Text> + </VStack> + </ClickableCard> + ))} + </Grid> + </> + ) : ( + <> + <HStack gap={2} vAlign="center" hAlign="between" wrap="wrap"> + <Text type="supporting" size="sm"> + 显示 {pageStart + 1}–{Math.min(pageStart + LUCIDE_PAGE_SIZE, filteredLucide.length)} 个,共 {filteredLucide.length} 个图标 + </Text> + <HStack gap={2} vAlign="center"> + <Button + label="上一页" + variant="secondary" + size="sm" + isDisabled={currentPage === 1} + onClick={() => setPage((value) => Math.max(1, value - 1))} + icon={<Icon icon={ChevronLeft} size="sm" />} + /> + <Text type="supporting" size="sm"> + 第 {currentPage} / {totalPages} 页 + </Text> + <Button + label="下一页" + variant="secondary" + size="sm" + isDisabled={currentPage === totalPages} + onClick={() => + setPage((value) => Math.min(totalPages, value + 1)) + } + endContent={<Icon icon={ChevronRight} size="sm" />} + /> + </HStack> + </HStack> + + <Grid columns={{ minWidth: 160, max: 5 }} gap={3}> + {pageItems.map(({ name, Icon: LucideComponent }) => ( + <ClickableCard + key={name} + padding={3} + onClick={() => + void copyUsage( + `import { ${name} } from 'lucide-react'\n\n<Icon icon={${name}} size="lg" />`, + name, + ) + } + label={`复制 ${name} 图标用法`}> + <VStack gap={2} hAlign="center"> + <Icon icon={LucideComponent} size="lg" color="primary" /> + <Text + type="supporting" + size="xsm" + display="block" + justify="center" + maxLines={2} + wordBreak="break-all"> + {name} + </Text> + </VStack> + </ClickableCard> + ))} + </Grid> + + <HStack gap={2} vAlign="center" hAlign="between" wrap="wrap"> + <Text type="supporting" size="sm"> + 点击图标复制 import 与 Icon 用法 + </Text> + <HStack gap={2} vAlign="center"> + <Button + label="上一页" + variant="secondary" + size="sm" + isDisabled={currentPage === 1} + onClick={() => setPage((value) => Math.max(1, value - 1))} + icon={<Icon icon={ChevronLeft} size="sm" />} + /> + <Text type="supporting" size="sm"> + 第 {currentPage} / {totalPages} 页 + </Text> + <Button + label="下一页" + variant="secondary" + size="sm" + isDisabled={currentPage === totalPages} + onClick={() => + setPage((value) => Math.min(totalPages, value + 1)) + } + endContent={<Icon icon={ChevronRight} size="sm" />} + /> + </HStack> + </HStack> + </> + )} + + <Divider /> + + <VStack gap={1}> + <Heading level={2}>使用方式</Heading> + <Text type="body" color="secondary" as="p"> + 语义图标可以直接传入名称;Lucide 图标需要先导入组件,再交给 Icon 渲染。 + </Text> + </VStack> + </VStack> + </Page> + ) +} diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..04ae800 --- /dev/null +++ b/src/pages/NotFoundPage.tsx @@ -0,0 +1,27 @@ +import { Button } from '@astryxdesign/core/Button' +import { Center } from '@astryxdesign/core/Center' +import { Heading } from '@astryxdesign/core/Heading' +import { Text } from '@astryxdesign/core/Text' +import { VStack } from '@astryxdesign/core/VStack' +import { Page } from '../components/Page' + +export function NotFoundPage() { + return ( + <Page> + <Center minHeight={520}> + <VStack gap={4} hAlign="center" maxWidth={440}> + <Text type="label" color="accent"> + 404 + </Text> + <Heading level={1} type="display-3" justify="center"> + 页面未找到 + </Heading> + <Text type="body" color="secondary" as="p" justify="center"> + 这个模板里没有你访问的路由。回到概览页,再选择一个目的地吧。 + </Text> + <Button label="返回概览" variant="primary" href="/" /> + </VStack> + </Center> + </Page> + ) +} diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 0000000..a7a0419 --- /dev/null +++ b/src/router.tsx @@ -0,0 +1,28 @@ +import { + createBrowserRouter, + createHashRouter, + type RouteObject, +} from 'react-router-dom' +import { appConfig } from '../app.config' +import { appRoutes } from './app-routes' +import { AppLayout } from './components/AppLayout' +import { NotFoundPage } from './pages/NotFoundPage' + +const pageRoutes: RouteObject[] = appRoutes.map((route) => + route.path === '/' + ? { index: true, element: route.element } + : { path: route.path.slice(1), element: route.element }, +) + +const routes: RouteObject[] = [ + { + path: '/', + element: <AppLayout />, + children: [...pageRoutes, { path: '*', element: <NotFoundPage /> }], + }, +] + +export const router = + appConfig.routerMode === 'hash' + ? createHashRouter(routes) + : createBrowserRouter(routes) diff --git a/src/theme/ThemeMode.tsx b/src/theme/ThemeMode.tsx new file mode 100644 index 0000000..cd91d5d --- /dev/null +++ b/src/theme/ThemeMode.tsx @@ -0,0 +1,55 @@ +import { useEffect, useMemo, useState, type ReactNode } from 'react' +import { Theme } from '@astryxdesign/core/theme' +import { defaultThemeSlug, themes } from '../astryx-themes' +import { + ThemeModeContext, + type ColorMode, + type ThemeModeValue, +} from './themeModeContext' + +const STORAGE_KEY = 'astryx-starter-mode' + +function getInitialMode(): ColorMode { + if (typeof window === 'undefined') return 'light' + + try { + const stored = window.localStorage.getItem(STORAGE_KEY) + if (stored === 'light' || stored === 'dark') return stored + } catch { + // Storage can be unavailable in private browsing or embedded contexts. + } + + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' +} + +export function ThemeModeProvider({ children }: { children: ReactNode }) { + const [mode, setMode] = useState<ColorMode>(getInitialMode) + + useEffect(() => { + try { + window.localStorage.setItem(STORAGE_KEY, mode) + } catch { + // The theme still switches for this session if persistence is blocked. + } + }, [mode]) + + const value = useMemo<ThemeModeValue>( + () => ({ + mode, + toggleMode: () => { + setMode((current) => (current === 'light' ? 'dark' : 'light')) + }, + }), + [mode], + ) + + return ( + <Theme theme={themes[defaultThemeSlug]} mode={mode}> + <ThemeModeContext.Provider value={value}> + {children} + </ThemeModeContext.Provider> + </Theme> + ) +} diff --git a/src/theme/themeModeContext.ts b/src/theme/themeModeContext.ts new file mode 100644 index 0000000..5bbf8ba --- /dev/null +++ b/src/theme/themeModeContext.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react' + +export type ColorMode = 'light' | 'dark' + +export interface ThemeModeValue { + mode: ColorMode + toggleMode: () => void +} + +export const ThemeModeContext = createContext<ThemeModeValue | null>(null) diff --git a/src/theme/useThemeMode.ts b/src/theme/useThemeMode.ts new file mode 100644 index 0000000..9769684 --- /dev/null +++ b/src/theme/useThemeMode.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react' +import { ThemeModeContext, type ThemeModeValue } from './themeModeContext' + +export function useThemeMode(): ThemeModeValue { + const context = useContext(ThemeModeContext) + + if (context === null) { + throw new Error('useThemeMode must be used inside ThemeModeProvider') + } + + return context +} diff --git a/theme.template.ts b/theme.template.ts new file mode 100644 index 0000000..64b61a8 --- /dev/null +++ b/theme.template.ts @@ -0,0 +1,399 @@ +/** + * Astryx theme template — every `defineTheme` field, with a note on what it + * does and when to reach for it. + * + * Only `name` is required. Delete what you do not need: the values here are + * placeholders that reproduce the built-in defaults, so a field you leave + * untouched changes nothing. + * + * Each section names the CLI command that prints the authoritative reference + * for it. These comments are a map; the CLI is the territory — it reads the + * same source the components do. `astryx docs` lists every topic. + * + * ═══════════════════════════════════════════════════════════════════════ + * HOW A THEME IS CONSUMED + * ═══════════════════════════════════════════════════════════════════════ + * + * 1. WRAP THE APP. A theme does nothing until it is provided. + * + * import {Theme} from '@astryxdesign/core/theme'; + * import {myTheme} from './theme'; + * + * <Theme theme={myTheme} mode="system"> // 'system' | 'light' | 'dark' + * <App /> + * </Theme> + * + * `system` follows the OS. Nest a second `<Theme>` to give one region its + * own theme or mode — a dark sidebar inside a light page. + * + * 2. SHIP THE FONTS YOU NAME. Astryx sets the `--font-family-*` tokens; it + * never loads a font file, so use whatever typeface the design needs and + * load it yourself. Either add it to your app's <head>: + * + * <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> + * <link rel="stylesheet" + * href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" /> + * + * or self-host it in your global CSS: + * + * @font-face { + * font-family: 'Inter'; + * src: url('/fonts/Inter.woff2') format('woff2'); + * font-weight: 100 900; + * font-display: swap; + * } + * + * Always give `fallbacks` as well. A named family with no file loads + * nothing and warns about nothing — the fallback silently becomes your + * theme. + * + * 3. BUILD IT FOR PRODUCTION. + * + * astryx theme build src/theme.ts + * + * writes `<name>.css` (tokens, component overrides and prose styles in + * `@scope` rules), `<name>.js` (the theme with `__built: true` and + * pre-resolved values), `<name>.d.ts`, and `<name>.variants.d.ts` when the + * theme adds custom prop values. Import the module and the CSS together: + * + * import {myThemeTheme} from './theme/my-theme'; + * import './theme/my-theme.css'; + * + * Unbuilt, `<Theme>` injects a <style> tag at hydration instead — fine for + * dev and client-only apps, but under SSR the component overrides flash on + * hydration. Build for Next.js and Remix. Run the build after every edit: + * it also validates the theme and regenerates the types for custom + * variants. + * + * 4. LOOK AT IT IN BOTH COLOUR MODES before you ship, including the states you + * did not write. Full guide: `astryx docs theme`. + * + * SYNC: this template mirrors the theme system. When you change one of these, + * update it here — scripts/check-theme-template.test.mjs fails when they drift: + * - /packages/core/src/theme/defineTheme.ts (the fields) + * - /packages/core/src/theme/tokens.stylex.ts (the token families) + * - /packages/core/src/theme/expandColorScale.ts + * - /packages/core/src/theme/expandTypeScale.ts + * - /packages/core/src/theme/expandRadiusScale.ts + * - /packages/core/src/theme/expandMotionScale.ts + */ + +import {defineTheme} from '@astryxdesign/core/theme'; +// import {palette} from './palette.generated'; +// import {dracula} from '@astryxdesign/core/theme/syntax'; +// import {neutralTheme} from '@astryxdesign/theme-neutral'; + +export const myTheme = defineTheme({ + /** Required. Becomes the `data-astryx-theme` attribute and the registry key. */ + name: 'my-theme', + + /** + * Start from another theme instead of the defaults. Tokens are copied then + * overridden, `components` deep-merge, `icons` shallow-merge — but the scale + * configs below REPLACE the base's rather than merging, because they are + * inputs to a generator, not values. + * + * Reference: `astryx theme list` (themes you can install and extend). + */ + // extends: neutralTheme, + + // ─────────────────────────────────────────────────────────────────────── + // Scale configs — concise inputs for typography, radius, and motion. + // The color scale remains supported for existing themes, but it is not the + // recommended starting point for a new palette; see its note below. + // ─────────────────────────────────────────────────────────────────────── + + /** + * Supported compatibility path for existing themes. This expands one accent + * seed into the neutral ramp, surfaces, text, icons, borders, muted fills, + * hover and pressed overlays — light and dark both. It is not an accent-only + * edit. + * + * For a new complete palette, run: + * + * astryx theme palette generate palette.config.json --out palette.generated.ts + * + * Review the generated candidate, then commit the palette module and receipt. + * Uncomment the `palette` import above and reference its stops from semantic + * tokens below. Regenerating an accepted palette then updates every mapped + * token that keeps those references. Generation does not rewrite the theme. + * + * accent seed hex, or a [light, dark] pair to seed each scheme's + * palette from its own colour; omit to keep the default + * accent and re-tone only the neutrals + * neutralStyle 'warm' | 'cool' | 'neutral' — the temperature of the greys + * contrast 'standard' | 'high' — 'high' widens the text/surface tone + * gap, for dense data UI or bright and clinical screens + * + * Whatever seed you pass, generated text holds >= 4.5:1 against its surface + * and --color-border-emphasized >= 3:1. Two limits on that guarantee: + * + * - It covers what this config generates. Write one side of a pair by hand + * in `tokens` below — an accent without its --color-on-accent, a surface + * without its text — and you own the contrast for that pair. + * - Status colours (success, warning, error) and the categorical data hues + * are not derived from the accent. They keep their defaults, which are + * tuned for the default surfaces; check them against yours. + * + * Either way the check is the same: resolve the pair in BOTH modes and + * measure it. `useTheme()` and `resolveThemeTokens()` return the resolved + * values, and the rendered DOM is the final word. + * + * Reference: `astryx docs color` for what each semantic role means, + * `astryx docs tokens/tokens-color` for every colour token and its light/dark + * default. + */ + // color: {accent: '#0064E0', neutralStyle: 'cool', contrast: 'standard'}, + + /** + * Type scale and the three font roles. + * scale.base body size in px; scale.ratio step between sizes + * (1.125 tight/dense · 1.2 default · 1.333 dramatic) + * body / heading / code: {family, fallbacks, weight, weights} + * heading inherits family and fallbacks from body when omitted; `weights` + * sets per-level weight for headings 1–6. + * + * See §2 above — naming a family here does not load it. + * + * Reference: `astryx docs typography`. + */ + typography: { + scale: {base: 16, ratio: 1.2}, + body: {family: 'Inter', fallbacks: '-apple-system, system-ui, sans-serif'}, + heading: {weight: 'semibold', weights: {1: 'bold', 2: 'bold'}}, + code: { + family: 'Geist Mono', + fallbacks: '"SF Mono", ui-monospace, monospace', + }, + }, + + /** + * Radius scale. `base` is the unit in px; `multiplier` scales every step + * (0 = square and brutalist, 2 = very soft). --radius-none and --radius-full + * are fixed and never scaled. + * + * Reference: `astryx docs shape`. + */ + radius: {base: 4, multiplier: 1}, + + /** + * Duration scale in ms. Each of fast/medium/slow also gets a -min and -max + * sibling computed with `ratio` (min = base × ratio, max = base ÷ ratio), so + * a component can pick a longer or shorter variant of the same tempo. + * Snappy {fast: 100, medium: 250} · Default {fast: 175, medium: 410, slow: 975} + * Cinematic {fast: 200, medium: 500, slow: 1200} + * `easing` overrides --ease-standard. + * + * Reference: `astryx docs motion`. + */ + motion: {fast: 175, medium: 410, slow: 975, ratio: 0.75}, + + // ─────────────────────────────────────────────────────────────────────── + // tokens — explicit overrides, which beat anything the scales generated. + // A string applies to both colour modes; a [light, dark] tuple compiles to + // CSS light-dark(). List only what you want to change. + // + // `astryx docs tokens/tokens-color` PRINTS THE WHOLE TABLE — every colour token + // with its light and dark default (`astryx docs tokens` lists every category). Read it before hand-writing colours: most of what you + // want already has a semantic name, and the defaults tell you what you are + // moving away from. + // + // The families, so you know what exists: + // --color-* accent, on-accent · background-{body,surface,card,popover, + // muted,inverted} · text-* · icon-* · border, + // border-emphasized · overlay{,-hover,-pressed} · + // success/warning/error (+ -muted, on-*) · skeleton, track, + // shadow, tint-hover · categorical {blue,cyan,gray,green, + // orange,pink,purple,red,teal,yellow} × + // {background,border,icon,text} + // --spacing-* 0 · 0-5 · 1 … 12 + // --size-element-* sm · md · lg (control heights) + // --focus-outline-* width · style · color · offset + // --border-width hairline thickness shared by bordered surfaces + // --radius-* none · inner · element · container · page · chat · full + // --shadow-* low/med/high + inset-{hover,selected,success,warning,error} + // --duration-* {fast,medium,slow} × {-min, base, -max} + // --ease-standard + // --font-family-* body · heading · code + // --font-size-* 4xs … 5xl + // --font-weight-* normal · medium · semibold · bold + // --text-* {heading-1…6, body, large, label, code, supporting, + // display-1…3} × {-size, -weight, -leading} + // --color-syntax-* code highlighting · --color-data-* charts + // ─────────────────────────────────────────────────────────────────────── + tokens: { + // Keep generated roles connected to the accepted palette. Uncomment the + // import above, then map each reviewed semantic role this way. + // '--color-background-surface': [ + // palette.neutral.light[100], + // palette.neutral.dark[15], + // ], + '--color-accent': ['#0064E0', '#2694FE'], + // Changing an accent means owning its on-colour: this is the label that + // sits on top of the fill above, in each mode. + '--color-on-accent': ['#FFFFFF', '#FFFFFF'], + '--color-background-body': ['#F1F4F7', '#111112'], + '--focus-outline-color': 'var(--color-accent)', + '--shadow-low': '0 1px 2px light-dark(#0000001A, #00000066)', + }, + + /** Optional theme-family-local roles accept any valid CSS custom-property name; + * prefixes do not establish ownership. */ + // localTokens: {}, + + // ─────────────────────────────────────────────────────────────────────── + // components — per-component CSS, emitted inside + // `@scope ([data-astryx-theme="my-theme"])`, so it only touches your theme. + // + // Keys are the component's stable class minus the `astryx-` prefix + // (`astryx-button` → `button`, `astryx-side-nav-item` → `side-nav-item`), + // including inner parts like `progress-bar-fill`. + // + // `astryx component <Name>` IS THE REFERENCE HERE. It prints that + // component's theming targets, the visual props and states you can key on, + // its public CSS variables with defaults, and which standard CSS properties + // it expands. `astryx component --list` names every component, and + // `astryx docs styling` covers the escape hatches outside a theme. + // + // Style-key forms: + // 'base' every instance + // 'variant:ghost' one prop value (any visual prop, not just variant) + // 'variant:ghost+size:sm' intersection of two + // 'checked' a state the component reflects + // ':hover' / ':focus-visible' / ':disabled' — nested inside any of the above + // + // Write ordinary CSS properties: `borderRadius` and `padding` expand into + // the component's internal vars for you, concentric radius math included. + // Set a public CSS var directly when no standard property reaches it — take + // the name from `astryx component <Name>`, since a var no component defines + // compiles to CSS that never applies. Never set a private `--_`-prefixed + // var; the build rejects it. + // ─────────────────────────────────────────────────────────────────────── + components: { + button: { + base: { + borderRadius: 'var(--radius-full)', + fontWeight: 'var(--font-weight-semibold)', + // A public var from `astryx component Button` — no CSS-property + // equivalent exists for it. + '--button-focus-offset': '2px', + // Interaction states live inside the same block. + ':hover': {transform: 'translateY(-1px)'}, + ':active': {transform: 'translateY(0)'}, + }, + 'variant:ghost': {borderWidth: '1px', borderStyle: 'solid'}, + 'variant:destructive+size:sm': {letterSpacing: '0.02em'}, + // A value that is not built in becomes a NEW variant: `astryx theme + // build` writes the module augmentation, so <Button variant="quiet" /> + // typechecks wherever this theme is active. Add one only where the + // product will render it — an unused variant is invisible work. + 'variant:quiet': { + backgroundColor: 'var(--color-background-muted)', + color: 'var(--color-text-secondary)', + }, + }, + // Container components take `padding` too; it expands to their layout tokens. + card: { + base: { + borderRadius: 'var(--radius-container)', + padding: 'var(--spacing-6)', + }, + }, + // The same mechanism adds custom Text types: <Text type="hero" />. + text: {'type:hero': {fontSize: 'var(--font-size-4xl)', lineHeight: '1.05'}}, + }, + + // ─────────────────────────────────────────────────────────────────────── + // Everything below takes React values, so it needs a .tsx file or an + // imported module. Commented out for that reason. + // ─────────────────────────────────────────────────────────────────────── + + /** + * Swap the artwork behind semantic icon names — every component that asks + * for `check`, `close`, `chevron-down` gets yours. + * Reference: `astryx docs icons` lists the names. + */ + // icons: {check: <MyCheck />, close: <MyClose />}, + + /** + * Replace the small components that DRAW control state — the checkbox box, + * the radio dot, the mark on a chosen option — by indicator name rather than + * per call site. Replacing `check` re-skins every single-selection mark in + * the app at once. + * Reference: `astryx component Indicator`. + */ + // indicators: {check: RadioIndicator}, + + /** + * Code highlighting: sets the --color-syntax-* tokens. Presets live in + * `@astryxdesign/core/theme/syntax`. + */ + // syntax: dracula, + + /** + * Content sitting on an inverted surface — a dark toast in light mode, a + * light popover in dark mode. Same shape as the theme itself (tokens and + * components). Sensible values are generated if you say nothing, so override + * only where your palette needs it. + * Reference: `astryx component MediaTheme`, which reads these. + */ + // onDark: {tokens: {'--color-accent': '#90CAF9'}, components: {button: {'variant:ghost': {borderWidth: '1px'}}}}, + // onLight: {tokens: {'--color-accent': '#0B5FCC'}}, + + // ─────────────────────────────────────────────────────────────────────── + // Theme adaptations + // ─────────────────────────────────────────────────────────────────────── + + /** + * Opt-in values for reviewed environmental conditions. Width points use the + * fixed names `sm`, `md`, `lg`, `xl`, and `2xl`; overriding the map alone + * emits no CSS. Fields inside `when` are ANDed, and matching rules cascade in + * authored order so later writes win. + * + * `from` includes its point; `below` excludes it. The other supported axes + * are primary-pointer precision, contrast preference, and motion preference. + * Component writes in a rule are validated exactly like root `components`. + * The one addition: a custom value that is valid only because a theme + * enrolls it must be declared at the root first, since generated type + * augmentation is unconditional; a rule may then restyle it. + */ + // Adaptations are opt-in. Uncomment and edit the example below when the + // theme needs environmental values. + // adaptations: { + // widthBreakpoints: { + // sm: 640, + // md: 768, + // lg: 1024, + // xl: 1280, + // '2xl': 1536, + // }, + // rules: [ + // { + // // Narrow at every pointer precision. + // when: {width: {below: 'md'}}, + // value: {tokens: {'--spacing-4': '0.75rem'}}, + // }, + // { + // // Taller controls under a finger or stylus at every width. + // when: {pointer: 'coarse'}, + // value: { + // tokens: { + // '--size-element-sm': '36px', + // '--size-element-md': '40px', + // '--size-element-lg': '44px', + // }, + // }, + // }, + // // Combined conditions are peers, not nested syntax: + // // { + // // when: { + // // width: {from: 'lg', below: 'xl'}, + // // pointer: 'coarse', + // // contrast: 'more', + // // motion: 'reduce', + // // }, + // // value: {components: {card: {base: {borderWidth: '2px'}}}}, + // // }, + // ], + // }, +}); diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..c3797c7 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "app.config.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..d649999 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,28 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + build: { + rolldownOptions: { + output: { + codeSplitting: { + // Do not pull a group's dependencies into that group. React and the + // router must stay in their own cacheable chunks. + includeDependenciesRecursively: false, + groups: [ + { + name: 'react-vendor', + test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/, + }, + { + name: 'router-vendor', + test: /node_modules[\\/]react-router(-dom)?[\\/]/, + }, + ], + }, + }, + }, + }, +}) |
