Compare commits

...

48 Commits

Author SHA1 Message Date
Maofeng 774495912c style(docs): simplify article sidebar surface 2026-09-30 01:31:54 +08:00
Maofeng 57d006a39b docs(layout): document app layout composition 2026-09-30 01:30:56 +08:00
Maofeng ffda0b9099 feat(layout): add configurable application layout 2026-09-30 01:30:47 +08:00
Maofeng 8b880a19e2 feat(docs): colocate localized package sandboxes 2026-09-29 22:13:17 +08:00
Maofeng e34675a931 docs(appearance): document preference hooks 2026-09-29 22:12:56 +08:00
Maofeng 642cb71b93 refactor(appearance): organize components and hooks 2026-09-29 22:12:40 +08:00
Maofeng 352db2c4c0 docs(appearance): document control variants 2026-09-29 21:29:49 +08:00
Maofeng 983ebead02 feat(appearance): expand preference controls 2026-09-29 21:29:38 +08:00
Maofeng 9a6ec3a396 feat(docs): add sandbox fullscreen mode 2026-09-29 21:04:34 +08:00
Maofeng 4710a56242 docs(appearance): document display preferences 2026-09-29 20:58:46 +08:00
Maofeng aae02d1502 feat(appearance): add accessible display preferences 2026-09-29 20:58:35 +08:00
Maofeng 75d8cbbfc3 feat(appearance): add display preference models 2026-09-29 20:57:59 +08:00
Maofeng 68f142dfa3 feat(docs): group package catalog 2026-09-29 20:56:44 +08:00
Maofeng 24b32ea5cc feat(cropper): expand image export API 2026-09-29 17:46:16 +08:00
Maofeng f56397fb97 feat(docs): discover package metadata 2026-09-29 17:20:25 +08:00
Maofeng 844acad316 docs(cropper): add bilingual usage guides 2026-09-29 17:09:39 +08:00
Maofeng 23e0867027 feat(cropper): add image cropping primitives 2026-09-29 17:09:26 +08:00
Maofeng acbc62463e docs(ui): explain shadcn adaptations 2026-09-29 17:08:23 +08:00
Maofeng ffe180f482 refactor(workspace): consume revised color tokens 2026-09-29 17:08:16 +08:00
Maofeng 91e010cff7 refactor(appearance): adopt accent and highlight tokens 2026-09-29 17:08:08 +08:00
Maofeng d5cec8342d refactor(ui): clarify semantic color roles 2026-09-29 17:08:03 +08:00
Maofeng c39d131a86 docs: register overlays and page packages 2026-09-28 23:13:54 +08:00
Maofeng d23deca0e7 feat(page): add composable page primitives 2026-09-28 23:13:49 +08:00
Maofeng 0ec968c344 feat(overlays): add managed overlay package 2026-09-28 23:13:44 +08:00
Maofeng bca874fd30 feat(ui): expose typed overlay handles 2026-09-28 23:13:39 +08:00
Maofeng 1353dc3145 docs(record): add bilingual usage guides 2026-09-28 17:24:14 +08:00
Maofeng d3de7cc172 test(record): cover providers fields and iteration 2026-09-28 17:24:08 +08:00
Maofeng 3eb0246c40 feat(record): add generic record context APIs 2026-09-28 17:23:59 +08:00
Maofeng 93373adb9a feat(docs): enhance code presentation 2026-09-28 16:00:01 +08:00
Maofeng 91cfb3d068 refactor(ui): use relative internal imports 2026-09-21 17:32:06 +08:00
Maofeng 4c61e79d0d docs(ui): expand bilingual component guides 2026-09-21 17:27:45 +08:00
Maofeng fb985edb20 feat(docs): add appearance sandbox example 2026-09-21 17:09:54 +08:00
Maofeng a9d6856ab8 feat(docs): add MDX sandbox previews 2026-09-21 17:09:44 +08:00
Maofeng b26c8886cf chore(web): resolve source alias from ESM config 2026-09-21 16:18:33 +08:00
Maofeng 3e671cce47 test(web): cover select disclosure icon 2026-09-21 16:18:29 +08:00
Maofeng 4d3892a5ea fix(docs): stabilize shared layout rendering 2026-09-21 16:17:06 +08:00
Maofeng df5bbc739c refactor(docs): share site header and footer 2026-09-21 16:12:12 +08:00
Maofeng 6711401f5f docs(appearance): add bilingual package guides 2026-09-21 16:05:19 +08:00
Maofeng e2dbbcb884 feat(appearance): add composable theme package 2026-09-21 16:05:05 +08:00
Maofeng 71c416caab fix(ui): render select icon as child 2026-09-20 18:32:14 +08:00
Maofeng a6f5f0b649 docs: rewrite project readme 2026-09-20 16:49:28 +08:00
Maofeng 02df33ce6b style(docs): update homepage branding 2026-09-20 16:44:27 +08:00
Maofeng eebf29c3d5 docs(media): add standalone package guide 2026-09-20 16:44:22 +08:00
Maofeng c519d4e74b feat(media): extract workspace package 2026-09-20 16:44:15 +08:00
Maofeng 2a6284d1b0 chore(docs): add Cloudflare deployment 2026-09-20 16:29:08 +08:00
Maofeng 7d57c9e7ae chore(docs): remove redundant workspace files 2026-09-20 16:23:41 +08:00
Maofeng 9635de56e8 feat(docs): add static generation and search indexes 2026-09-20 16:23:32 +08:00
Maofeng a57fdbe7c6 fix(search): restore history after hydration 2026-09-20 16:22:21 +08:00
377 changed files with 18557 additions and 1862 deletions
+2
View File
@@ -10,7 +10,9 @@ lerna-debug.log*
node_modules
dist
dist-ssr
.ssr
.output
docs/icons
*.local
# env files (can opt-in for committing if needed)
+127 -10
View File
@@ -1,22 +1,139 @@
# Simple React App Kit
Simple React App Kit is a reusable React application kit built with TanStack
Start, shadcn/ui, and workspace packages.
Simple React App Kit is a production-oriented React workspace for building application interfaces without assembling the foundation from scratch. It includes a runnable TanStack Start application, shared shadcn/ui components, feature packages, internationalization, documentation, and tested integration patterns.
## Adding components
[Open the documentation](https://react-app-kit.go-slim.dev) · [English documentation](https://react-app-kit.go-slim.dev/en-US/)
To add components to your app, run the following command at the root of your `web` app:
## What you get
```bash
pnpm dlx shadcn@latest add button -c apps/web
- A TanStack Start application with routing, SSR, responsive layout, and light/dark themes.
- Shared shadcn/ui primitives that remain editable source code.
- Application-level navigation, notifications, chat, and appearance blocks.
- Adapter-based Media and Search packages that do not prescribe a backend.
- A composable Lexical editor package.
- Type-safe application preferences and an internationalization workflow.
- A bilingual MDX documentation site with SSG, search, and Cloudflare deployment.
## Quick start
Requirements:
- [Bun](https://bun.com/) 1.3.5 or newer
- Node.js 22.19 or newer
Install dependencies and start the example application:
```sh
bun install
bun run dev
```
This will place the ui components in the `packages/ui/src/components` directory.
The development server prints the local URL when it is ready.
## Using components
To run the documentation site separately:
To use the components in your app, import them from the `ui` package.
```sh
bun run --cwd docs dev
```
## Workspace packages
The packages are private workspace packages by default. Use them inside this repository with `workspace:*`, or adapt and publish them under your own scope.
| Package | Use it for |
| --- | --- |
| `@workspace/ui` | Shared components, hooks, icons, design tokens, and global styles |
| `@workspace/blocks` | Application shell, navigation, notifications, chat, and appearance controls |
| `@workspace/media` | Media browsing, uploads, folders, asset selection, and storage adapters |
| `@workspace/search` | Application search, grouped results, pagination, and search history |
| `@workspace/lexical` | Rich-text editing, actions, media nodes, and editor presets |
| `@workspace/i18n` | Locale runtime, catalogs, extraction, compilation, and development tools |
| `@workspace/preferences` | Typed external state, persistence callbacks, SSR snapshots, and effects |
The intended boundary is simple: your application owns APIs, persistence, routing decisions, and business rules; workspace packages own reusable presentation and interaction behavior.
## Using a package
Add a workspace dependency to the consuming application:
```json
{
"dependencies": {
"@workspace/media": "workspace:*"
}
}
```
Import the package stylesheet once when it provides one. This also lets Tailwind discover the utilities used by that package:
```css
@import "@workspace/ui/globals.css";
@import "@workspace/media/globals.css";
```
Then import only the public API you need:
```tsx
import { Button } from "@workspace/ui/components/button";
import {
MediaLibrary,
MediaProvider,
type MediaAdapter,
} from "@workspace/media"
export function MediaPage({ adapter }: { adapter: MediaAdapter }) {
return (
<MediaProvider adapter={adapter}>
<MediaLibrary />
</MediaProvider>
)
}
```
Package-specific setup, adapters, locale catalogs, and progressive examples are documented on the [documentation site](https://react-app-kit.go-slim.dev).
## Adding shadcn/ui components
Run the shadcn CLI from the repository root:
```sh
bunx shadcn@latest add button -c apps/web
```
The workspace aliases place generated components in `packages/ui/src/components`. Import them through the UI package:
```tsx
import { Button } from "@workspace/ui/components/button"
```
Generated components belong to the repository and can be changed to match the product instead of being treated as an opaque dependency.
## Common commands
| Command | Purpose |
| --- | --- |
| `bun run dev` | Start the example web application |
| `bun run build` | Build the web application |
| `bun run check` | Check formatting, lint, and all workspace types |
| `bun run format` | Format application and package source files |
| `bun run lint` | Run Oxlint across the repository |
| `bun run typecheck` | Type-check every workspace |
| `bun run --cwd packages/media test` | Run tests for one package |
| `bun run --cwd docs build` | Generate the static documentation site |
| `bun run --cwd docs deploy` | Build and deploy the docs to Cloudflare |
## Project structure
```text
apps/web/ Runnable TanStack Start application
docs/ Documentation application and SSG pipeline
packages/
blocks/ Application-level interface blocks
i18n/ Internationalization runtime and tooling
lexical/ Rich-text editor
media/ Media library and picker
preferences/ Typed preference state
search/ Application search
ui/ Shared shadcn/ui components
```
Documentation for each package lives beside its source under `packages/<name>/docs`, so examples and APIs evolve with the implementation.
+2 -1
View File
@@ -7,7 +7,7 @@
"@workspace/blocks/appearance/locales/{locale}",
"@workspace/blocks/chats/locales/{locale}",
"@workspace/blocks/layout/locales/{locale}",
"@workspace/blocks/media/locales/{locale}",
"@workspace/media/locales/{locale}",
"@workspace/blocks/navigation/locales/{locale}",
"@workspace/blocks/notifications/locales/{locale}",
"@workspace/search/locales/{locale}",
@@ -16,6 +16,7 @@
"include": [
"src",
"../../packages/blocks/src",
"../../packages/media/src",
"../../packages/search/src",
"../../packages/ui/src"
],
+1
View File
@@ -29,6 +29,7 @@
"@workspace/blocks": "workspace:*",
"@workspace/i18n": "workspace:*",
"@workspace/lexical": "workspace:*",
"@workspace/media": "workspace:*",
"@workspace/search": "workspace:*",
"@workspace/ui": "workspace:*",
"react": "^19.2.6",
+28
View File
@@ -0,0 +1,28 @@
import { cleanup, render, screen } from "@testing-library/react"
import { afterEach, describe, expect, it } from "vitest"
import {
Select,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
afterEach(cleanup)
describe("SelectTrigger", () => {
it("renders the disclosure icon without forwarding fallback children to its svg", () => {
render(
<Select items={[{ label: "System", value: "system" }]} value="system">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</Select>
)
const trigger = screen.getByRole("combobox")
const icon = trigger.querySelector("[aria-hidden='true'] > svg")
expect(icon).not.toBeNull()
expect(icon?.childNodes.length).toBeGreaterThan(0)
})
})
+1 -1
View File
@@ -183,7 +183,7 @@ function Field({
Component: FieldInputComponent
}) {
return (
<fieldset className="relative flex h-12 items-center rounded-lg border-2 border-border p-0 focus-within:border-primary focus-within:[&_label]:opacity-100">
<fieldset className="relative flex h-12 items-center rounded-lg border-2 border-border p-0 focus-within:border-accent focus-within:[&_label]:opacity-100">
<legend className="ml-2 flex h-0 px-0">
<label
className="h-fit -translate-y-1/2 px-1 text-sm opacity-50"
+1
View File
@@ -1,6 +1,7 @@
@import "@workspace/ui/globals.css";
@import "@workspace/blocks/globals.css";
@import "@workspace/lexical/globals.css";
@import "@workspace/media/globals.css";
@import "@workspace/search/globals.css";
:root {
+1 -1
View File
@@ -12,7 +12,7 @@ export default defineConfig({
resolve: {
tsconfigPaths: true,
alias: {
"@": path.resolve(__dirname, "./src"),
"@": path.resolve(import.meta.dirname, "./src"),
},
},
plugins: [
+388 -74
View File
@@ -27,6 +27,7 @@
"@workspace/blocks": "workspace:*",
"@workspace/i18n": "workspace:*",
"@workspace/lexical": "workspace:*",
"@workspace/media": "workspace:*",
"@workspace/search": "workspace:*",
"@workspace/ui": "workspace:*",
"react": "^19.2.6",
@@ -54,10 +55,17 @@
"version": "0.0.0",
"dependencies": {
"@icones/react": "^0.0.1",
"@shikijs/core": "^4.4.3",
"@shikijs/engine-javascript": "^4.4.3",
"@shikijs/langs": "^4.4.3",
"@shikijs/themes": "^4.4.3",
"@workspace/appearance": "workspace:*",
"@workspace/i18n": "workspace:*",
"@workspace/media": "workspace:*",
"@workspace/overlays": "workspace:*",
"@workspace/page": "workspace:*",
"@workspace/preferences": "workspace:*",
"@workspace/search": "workspace:*",
"@workspace/ui": "workspace:*",
"clsx": "^2.1.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
@@ -74,12 +82,34 @@
"@types/react-dom": "^19.2.7",
"@vitejs/plugin-react": "^6.1.1",
"babel-plugin-react-compiler": "^1.0.0",
"oxlint": "^1.81.0",
"satteri": "^0.10.5",
"satteri-expressive-code": "^0.1.24",
"tailwindcss": "^4.3.3",
"typescript": "~6.0.2",
"vite": "^8.3.0",
"vite-plugin-satteri": "^0.3.5",
"wrangler": "^4.135.0",
"yaml": "^2.9.1",
},
},
"packages/appearance": {
"name": "@workspace/appearance",
"version": "0.0.0",
"dependencies": {
"@tanstack/react-hotkeys": "^0.10.0",
"@workspace/i18n": "workspace:*",
"@workspace/preferences": "workspace:*",
"@workspace/ui": "workspace:*",
"react": "^19.2.6",
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"jsdom": "^30.0.1",
"react-dom": "^19.2.6",
"typescript": "~6",
"vitest": "^4.1.10",
},
},
"packages/blocks": {
@@ -109,6 +139,23 @@
"vitest": "^4.1.10",
},
},
"packages/cropper": {
"name": "@workspace/cropper",
"version": "0.0.0",
"dependencies": {
"@workspace/ui": "workspace:*",
"react": "^19.2.6",
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"jsdom": "^30.0.1",
"react-dom": "^19.2.6",
"typescript": "~6",
"vitest": "^4.1.10",
},
},
"packages/i18n": {
"name": "@workspace/i18n",
"version": "0.0.0",
@@ -139,6 +186,23 @@
"vitest": "^4",
},
},
"packages/layout": {
"name": "@workspace/layout",
"version": "0.0.0",
"dependencies": {
"@workspace/ui": "workspace:*",
"react": "^19.2.6",
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"jsdom": "^30.0.1",
"react-dom": "^19.2.6",
"typescript": "~6",
"vitest": "^4.1.10",
},
},
"packages/lexical": {
"name": "@workspace/lexical",
"version": "0.0.0",
@@ -166,6 +230,61 @@
"vitest": "^4.1.10",
},
},
"packages/media": {
"name": "@workspace/media",
"version": "0.0.0",
"dependencies": {
"@workspace/i18n": "workspace:*",
"@workspace/ui": "workspace:*",
"react": "^19.2.6",
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"jsdom": "^30.0.1",
"react-dom": "^19.2.6",
"typescript": "~6",
"vitest": "^4.1.10",
},
},
"packages/overlays": {
"name": "@workspace/overlays",
"version": "0.0.0",
"dependencies": {
"@base-ui/react": "^1.6.0",
"@workspace/i18n": "workspace:*",
"@workspace/ui": "workspace:*",
"react": "^19.2.6",
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"jsdom": "^30.0.1",
"react-dom": "^19.2.6",
"typescript": "~6",
"vitest": "^4.1.10",
},
},
"packages/page": {
"name": "@workspace/page",
"version": "0.0.0",
"dependencies": {
"@base-ui/react": "^1.6.0",
"@workspace/ui": "workspace:*",
"react": "^19.2.6",
"react-dom": "^19.2.6",
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"jsdom": "^30.0.1",
"typescript": "~6",
"vitest": "^4.1.10",
},
},
"packages/preferences": {
"name": "@workspace/preferences",
"version": "0.0.0",
@@ -182,6 +301,23 @@
"vitest": "^4.1.10",
},
},
"packages/record": {
"name": "@workspace/record",
"version": "0.0.0",
"dependencies": {
"es-toolkit": "^1.50.0",
"react": "^19.2.6",
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"jsdom": "^30.0.1",
"react-dom": "^19.2.6",
"typescript": "~6",
"vitest": "^4.1.10",
},
},
"packages/search": {
"name": "@workspace/search",
"version": "0.0.0",
@@ -330,15 +466,15 @@
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260908.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-t3juyCXFn12OklBL0S7UC98py3nLEmuioBOUOazeyeVsLUu8fv+pfJrR+XzwSH2Uh6rCXZNogRh+LtV0YpzMcQ=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260918.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-H5Em6Wd0jjxaloYh2rp+WLBl2eWbkk7nSP1svGt6K1RSv/rNVqmKdpjZPJTBSavOmeYGa88qvtOWwS+33OHTqQ=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260908.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I1nwA4qm/fUNSKhPSO76YA6JAhcA0KU63yFcYHN6AiDowGbDyfjDPH9KCVopT5rI1LY4GfbdAi5b9gODqZsAkQ=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260918.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CR9JRZEQo93fNgBVF4Df2H2/VYO4n7rxSseSwCVv3bJQEb0huADUSCj2FK4ipxar8ToOEB4wawyw0vJo5U/rQQ=="],
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260908.1", "", { "os": "linux", "cpu": "x64" }, "sha512-s/h5uSW1UC6dGeVKSqrPLpTu+vo0fKJZCNEokW1Ol1qcCB2oN6WCRHhoyzo4Y69oONAXRgLfLBYaHWe7z51Vnw=="],
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260918.1", "", { "os": "linux", "cpu": "x64" }, "sha512-UQ2nnY3qpXLzQ80frmWO+8HvtqyWaQILe8QYZwpemdjT+sqwCz4Dz+0/WVkFco0v/04kIIqikVeLTY/7gEhmkw=="],
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260908.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PP/nUKl0R6colwfocggL8dctO/dFMqMft12unzFUkoAJr0aXQeT+ia0JuNmOUWXe6EfvyCSmAbijEsTKQ5t/ew=="],
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260918.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4rib51MaLNWweUIUxM/Xj558M5QmyZoBSf0ffv+lYah5VTrvwnez3XGxX72pElnBKHROvAPOi5msQ5Ts8JSI0A=="],
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260908.1", "", { "os": "win32", "cpu": "x64" }, "sha512-jkaS5EKKTvzAdIlbMfOYrHoTucWVRwYBwIig+xRsjXQv5BXTLA2Llg+iM8djlKtk0CjkztZhXmuxuqoqWKZmFw=="],
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260918.1", "", { "os": "win32", "cpu": "x64" }, "sha512-sATrMx5ShYYgmgUGrcTmvsFSJBFuN95NEkX3xwb1qk8w1A6h5N11sea7yN2IeibwPyPmXKjWNjXOno0hZAM71Q=="],
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
@@ -356,6 +492,8 @@
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
"@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.1", "", {}, "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A=="],
"@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="],
"@devframes/hub": ["@devframes/hub@0.7.14", "", { "dependencies": { "birpc": "^4.0.0", "destr": "^2.0.5", "nostics": "^1.2.0", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "tinyexec": "^1.2.4", "zigpty": "^0.2.1" }, "peerDependencies": { "devframe": "0.7.14" } }, "sha512-Ym3YHkBnpdwhPw7y4YgURZYntQPShb9raRfo60D1Yk4jb1OlnL2GGHw6pt4iLZqHL4vp1P4T6YpBFPPVx4G38A=="],
@@ -432,6 +570,14 @@
"@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
"@expressive-code/core": ["@expressive-code/core@0.42.0", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw=="],
"@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0" } }, "sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA=="],
"@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0", "shiki": "^4.0.2" } }, "sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g=="],
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0" } }, "sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ=="],
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
@@ -462,57 +608,57 @@
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="],
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="],
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="],
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="],
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="],
"@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
@@ -774,6 +920,22 @@
"@shadcn/react": ["@shadcn/react@0.2.1", "", { "peerDependencies": { "@types/react": ">=19", "react": ">=19" }, "optionalPeers": ["@types/react", "react"] }, "sha512-5krgi3dRMKb5jH6a+qPzVJUy/54s0kKE4Rw4LjDfLqOdVQTWKUgxWf1kW8r912I0jX/Lzxqc+pgjkjWxUIK5BQ=="],
"@shikijs/core": ["@shikijs/core@4.4.3", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="],
"@shikijs/langs": ["@shikijs/langs@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="],
"@shikijs/primitive": ["@shikijs/primitive@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="],
"@shikijs/themes": ["@shikijs/themes@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="],
"@shikijs/types": ["@shikijs/types@4.4.3", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@sinclair/typebox": ["@sinclair/typebox@0.27.12", "", {}, "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g=="],
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
@@ -940,6 +1102,8 @@
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="],
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.7.1", "", { "peerDependencies": { "valibot": "^1.4.0" } }, "sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A=="],
"@vitejs/devtools": ["@vitejs/devtools@0.4.9", "", { "dependencies": { "@devframes/hub": "^0.7.14", "@devframes/plugin-inspect": "^0.7.14", "@devframes/plugin-messages": "^0.7.14", "@devframes/plugin-terminals": "^0.7.14", "@vitejs/devtools-kit": "0.4.9", "birpc": "^4.0.0", "cac": "^7.0.0", "devframe": "^0.7.14", "h3": "2.0.1-rc.22", "local-pkg": "^1.2.1", "mlly": "^1.8.2", "nostics": "^1.2.0", "obug": "^2.1.4", "pathe": "^2.0.3", "vue": "^3.5.40" }, "peerDependencies": { "@vitejs/devtools-oxc": "^0.4.9", "@vitejs/devtools-rolldown": "^0.4.9", "@vitejs/devtools-vite": "^0.4.9", "@vitejs/devtools-vitest": "^0.4.9", "vite": "*" }, "optionalPeers": ["@vitejs/devtools-oxc", "@vitejs/devtools-rolldown", "@vitejs/devtools-vite", "@vitejs/devtools-vitest"], "bin": { "vite-devtools": "./bin.js" } }, "sha512-Q9ju+eLFn/ge1nkgVUhAU0Yfu7lEITApK7f4r6z71sYRcTqIMqksMC/qimyAiz4/rQyL7WNj2hTFMIeYT7wolQ=="],
@@ -980,14 +1144,28 @@
"@vue/shared": ["@vue/shared@3.5.40", "", {}, "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg=="],
"@workspace/appearance": ["@workspace/appearance@workspace:packages/appearance"],
"@workspace/blocks": ["@workspace/blocks@workspace:packages/blocks"],
"@workspace/cropper": ["@workspace/cropper@workspace:packages/cropper"],
"@workspace/i18n": ["@workspace/i18n@workspace:packages/i18n"],
"@workspace/layout": ["@workspace/layout@workspace:packages/layout"],
"@workspace/lexical": ["@workspace/lexical@workspace:packages/lexical"],
"@workspace/media": ["@workspace/media@workspace:packages/media"],
"@workspace/overlays": ["@workspace/overlays@workspace:packages/overlays"],
"@workspace/page": ["@workspace/page@workspace:packages/page"],
"@workspace/preferences": ["@workspace/preferences@workspace:packages/preferences"],
"@workspace/record": ["@workspace/record@workspace:packages/record"],
"@workspace/search": ["@workspace/search@workspace:packages/search"],
"@workspace/ui": ["@workspace/ui@workspace:packages/ui"],
@@ -1032,6 +1210,8 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.4", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ=="],
"bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="],
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],
@@ -1040,6 +1220,8 @@
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
@@ -1062,10 +1244,16 @@
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
@@ -1088,6 +1276,8 @@
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"conf": ["conf@10.2.0", "", { "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "atomically": "^1.7.0", "debounce-fn": "^4.0.0", "dot-prop": "^6.0.1", "env-paths": "^2.2.1", "json-schema-typed": "^7.0.3", "onetime": "^5.1.2", "pkg-up": "^3.1.0", "semver": "^7.3.5" } }, "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg=="],
@@ -1116,6 +1306,8 @@
"crossws": ["crossws@0.4.10", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA=="],
"css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="],
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
@@ -1180,8 +1372,12 @@
"devframe": ["devframe@0.7.14", "", { "dependencies": { "@valibot/to-json-schema": "^1.7.1", "birpc": "^4.0.0", "crossws": "^0.4.10", "destr": "^2.0.5", "h3": "^2.0.1-rc.22", "mrmime": "^2.0.1", "nostics": "^1.2.0", "pathe": "^2.0.3", "ufo": "^1.6.4", "valibot": "^1.4.2" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "cac": "^7.0.0" }, "optionalPeers": ["@modelcontextprotocol/sdk", "cac"] }, "sha512-AtGfI3LKjZL11eBcGArZn6QMZTrmGZY7DCwJ9Wot8Vt6/jqaV4wv/Z/VsYneGc4Z/OmRmHucfMB0xlDrE6GYJw=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
"docs": ["docs@workspace:docs"],
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
@@ -1258,6 +1454,8 @@
"express-rate-limit": ["express-rate-limit@8.6.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA=="],
"expressive-code": ["expressive-code@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0", "@expressive-code/plugin-frames": "^0.42.0", "@expressive-code/plugin-shiki": "^0.42.0", "@expressive-code/plugin-text-markers": "^0.42.0" } }, "sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g=="],
"exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@@ -1322,12 +1520,32 @@
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="],
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
"hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="],
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"hono": ["hono@4.12.32", "", {}, "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg=="],
"hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"httpxy": ["httpxy@0.5.5", "", {}, "sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA=="],
@@ -1474,6 +1692,8 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="],
@@ -1484,6 +1704,16 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
@@ -1494,7 +1724,7 @@
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"miniflare": ["miniflare@5.20260908.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260908.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-BHIknb0u6vLIvb+R8PsUAzgoWWk3OlW85DrV2SG0EydfSpIba28YT0rH6xZThjnSwDxzNuW3FDRNSWvbiI/DVw=="],
"miniflare": ["miniflare@5.20260918.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.4", "undici": "7.29.0", "workerd": "1.20260918.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-vyIes7yW/OTzHtz4GhcBCTohZfqk2eQNiIkngZ07VRA5Qu24aNgW/TrFwlPkMLMjWDQ8R4JJBBHR/KX+liSDtg=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
@@ -1524,6 +1754,8 @@
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"nypm": ["nypm@0.6.9", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -1546,6 +1778,10 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"ora": ["ora@9.4.1", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
@@ -1598,6 +1834,8 @@
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="],
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
@@ -1610,6 +1848,8 @@
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"pseudolocale": ["pseudolocale@2.3.0", "", { "dependencies": { "commander": "^10.0.0" }, "bin": { "pseudolocale": "dist/cli.mjs" } }, "sha512-2RfZuwSSZ8sopelTIIZ2JhmO4GLnHflJQBmtMPF2APWEtmfKOsvqCIWsi8KQJ6EQ0D0+zVllPnLLGijUauSlqw=="],
@@ -1654,6 +1894,12 @@
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
@@ -1678,6 +1924,8 @@
"satteri": ["satteri@0.10.5", "", { "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.5", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.10.5", "@bruits/satteri-darwin-x64": "0.10.5", "@bruits/satteri-linux-arm64-gnu": "0.10.5", "@bruits/satteri-linux-arm64-musl": "0.10.5", "@bruits/satteri-linux-x64-gnu": "0.10.5", "@bruits/satteri-linux-x64-musl": "0.10.5", "@bruits/satteri-wasm32-wasi": "0.10.5", "@bruits/satteri-win32-arm64-msvc": "0.10.5", "@bruits/satteri-win32-x64-msvc": "0.10.5" } }, "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ=="],
"satteri-expressive-code": ["satteri-expressive-code@0.1.24", "", { "dependencies": { "expressive-code": "^0.42.0" }, "peerDependencies": { "satteri": ">=0.4.0" } }, "sha512-051Y0jA0qZlTL65e9M+xSxKSUO8msCPzRPASZ93FwBV9uYTil1+RNK6KwuIMidpzBFhxc4c8mgARaQOu1RcX/g=="],
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@@ -1696,12 +1944,14 @@
"shadcn": ["shadcn@4.15.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "undici": "^7.27.2", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-fFTpfOuRwqjpXGp/sKpAxJkjdgv1jf8bDrW1xi0cVn2k7WJ5ijV/gjkAlkAidGDjhEkgcUuxVdm4oRB9r8BivA=="],
"sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="],
"sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linux-riscv64": "1.3.3", "@img/sharp-libvips-linux-s390x": "1.3.3", "@img/sharp-libvips-linux-x64": "1.3.3", "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", "@img/sharp-libvips-linuxmusl-x64": "1.3.3", "@img/sharp-linux-arm": "0.35.4", "@img/sharp-linux-arm64": "0.35.4", "@img/sharp-linux-ppc64": "0.35.4", "@img/sharp-linux-riscv64": "0.35.4", "@img/sharp-linux-s390x": "0.35.4", "@img/sharp-linux-x64": "0.35.4", "@img/sharp-linuxmusl-arm64": "0.35.4", "@img/sharp-linuxmusl-x64": "0.35.4", "@img/sharp-webcontainers-wasm32": "0.35.4", "@img/sharp-win32-arm64": "0.35.4", "@img/sharp-win32-ia32": "0.35.4", "@img/sharp-win32-x64": "0.35.4" } }, "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="],
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
@@ -1724,6 +1974,8 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
@@ -1736,6 +1988,8 @@
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
@@ -1782,6 +2036,8 @@
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
"tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
@@ -1806,6 +2062,18 @@
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -1830,6 +2098,10 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
"vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="],
@@ -1858,9 +2130,9 @@
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"workerd": ["workerd@1.20260908.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260908.1", "@cloudflare/workerd-darwin-arm64": "1.20260908.1", "@cloudflare/workerd-linux-64": "1.20260908.1", "@cloudflare/workerd-linux-arm64": "1.20260908.1", "@cloudflare/workerd-windows-64": "1.20260908.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-rYhpW6NWHD++p34ej+VXWzi5Pdnmd7ncdbB/ux4s+i3mf7IeOpW3O4roqClQ+Z8ernvyMCknBLCb76z1rA+a7Q=="],
"workerd": ["workerd@1.20260918.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260918.1", "@cloudflare/workerd-darwin-arm64": "1.20260918.1", "@cloudflare/workerd-linux-64": "1.20260918.1", "@cloudflare/workerd-linux-arm64": "1.20260918.1", "@cloudflare/workerd-windows-64": "1.20260918.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-NsjfQlBNQ0iEniv/STOy4zbp8s5k60PzL1Ter02Eg44arbDhHtf6UOs03E31XDRmmBFxSIkAZuCyld3RKH1wqA=="],
"wrangler": ["wrangler@4.130.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260908.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260908.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260908.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-fzNjnTzyZl31PGJOFXbiLeZqEIToQ9KOzkkvGdQz4wlB+BoHnl3BRwCR5xg8AqYyzVunvvMHlMzvlbThQ7rrAA=="],
"wrangler": ["wrangler@4.135.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260918.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260918.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260918.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-WrNBQSfIG6YcILJcodYr5ty8vgkzGsV8YX+kfd+uZ5/Bd8cUW0GnUIRKAVfn5kYKqh1m/BPcji9h1d7mE8euFw=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
@@ -1894,6 +2166,8 @@
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
@@ -1958,12 +2232,12 @@
"data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
"docs/oxlint": ["oxlint@1.83.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.83.0", "@oxlint/binding-android-arm64": "1.83.0", "@oxlint/binding-darwin-arm64": "1.83.0", "@oxlint/binding-darwin-x64": "1.83.0", "@oxlint/binding-freebsd-x64": "1.83.0", "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", "@oxlint/binding-linux-arm-musleabihf": "1.83.0", "@oxlint/binding-linux-arm64-gnu": "1.83.0", "@oxlint/binding-linux-arm64-musl": "1.83.0", "@oxlint/binding-linux-ppc64-gnu": "1.83.0", "@oxlint/binding-linux-riscv64-gnu": "1.83.0", "@oxlint/binding-linux-riscv64-musl": "1.83.0", "@oxlint/binding-linux-s390x-gnu": "1.83.0", "@oxlint/binding-linux-x64-gnu": "1.83.0", "@oxlint/binding-linux-x64-musl": "1.83.0", "@oxlint/binding-openharmony-arm64": "1.83.0", "@oxlint/binding-win32-arm64-msvc": "1.83.0", "@oxlint/binding-win32-ia32-msvc": "1.83.0", "@oxlint/binding-win32-x64-msvc": "1.83.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ=="],
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"env-runner/miniflare": ["miniflare@5.20260908.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260908.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-BHIknb0u6vLIvb+R8PsUAzgoWWk3OlW85DrV2SG0EydfSpIba28YT0rH6xZThjnSwDxzNuW3FDRNSWvbiI/DVw=="],
"jest-validate/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
@@ -1980,6 +2254,8 @@
"ora/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="],
"pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
@@ -2090,43 +2366,11 @@
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"docs/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.83.0", "", { "os": "android", "cpu": "arm" }, "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA=="],
"env-runner/miniflare/sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="],
"docs/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.83.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg=="],
"env-runner/miniflare/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"docs/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.83.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ=="],
"docs/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.83.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA=="],
"docs/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.83.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w=="],
"docs/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.83.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw=="],
"docs/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.83.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA=="],
"docs/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.83.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg=="],
"docs/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.83.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew=="],
"docs/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.83.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g=="],
"docs/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.83.0", "", { "os": "linux", "cpu": "none" }, "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw=="],
"docs/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.83.0", "", { "os": "linux", "cpu": "none" }, "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw=="],
"docs/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.83.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ=="],
"docs/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.83.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw=="],
"docs/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.83.0", "", { "os": "linux", "cpu": "x64" }, "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ=="],
"docs/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.83.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA=="],
"docs/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.83.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg=="],
"docs/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.83.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA=="],
"docs/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.83.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q=="],
"env-runner/miniflare/workerd": ["workerd@1.20260908.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260908.1", "@cloudflare/workerd-darwin-arm64": "1.20260908.1", "@cloudflare/workerd-linux-64": "1.20260908.1", "@cloudflare/workerd-linux-arm64": "1.20260908.1", "@cloudflare/workerd-windows-64": "1.20260908.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-rYhpW6NWHD++p34ej+VXWzi5Pdnmd7ncdbB/ux4s+i3mf7IeOpW3O4roqClQ+Z8ernvyMCknBLCb76z1rA+a7Q=="],
"jest-validate/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -2178,6 +2422,68 @@
"vitest/vite/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
"env-runner/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
"env-runner/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="],
"env-runner/miniflare/sharp/@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="],
"env-runner/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="],
"env-runner/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="],
"env-runner/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="],
"env-runner/miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="],
"env-runner/miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="],
"env-runner/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="],
"env-runner/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="],
"env-runner/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="],
"env-runner/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="],
"env-runner/miniflare/sharp/@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="],
"env-runner/miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="],
"env-runner/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="],
"env-runner/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="],
"env-runner/miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"env-runner/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260908.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-t3juyCXFn12OklBL0S7UC98py3nLEmuioBOUOazeyeVsLUu8fv+pfJrR+XzwSH2Uh6rCXZNogRh+LtV0YpzMcQ=="],
"env-runner/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260908.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I1nwA4qm/fUNSKhPSO76YA6JAhcA0KU63yFcYHN6AiDowGbDyfjDPH9KCVopT5rI1LY4GfbdAi5b9gODqZsAkQ=="],
"env-runner/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260908.1", "", { "os": "linux", "cpu": "x64" }, "sha512-s/h5uSW1UC6dGeVKSqrPLpTu+vo0fKJZCNEokW1Ol1qcCB2oN6WCRHhoyzo4Y69oONAXRgLfLBYaHWe7z51Vnw=="],
"env-runner/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260908.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PP/nUKl0R6colwfocggL8dctO/dFMqMft12unzFUkoAJr0aXQeT+ia0JuNmOUWXe6EfvyCSmAbijEsTKQ5t/ew=="],
"env-runner/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260908.1", "", { "os": "win32", "cpu": "x64" }, "sha512-jkaS5EKKTvzAdIlbMfOYrHoTucWVRwYBwIig+xRsjXQv5BXTLA2Llg+iM8djlKtk0CjkztZhXmuxuqoqWKZmFw=="],
"shadcn/ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
"shadcn/ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
@@ -2205,5 +2511,13 @@
"vitest/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"vitest/vite/postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
"env-runner/miniflare/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
"env-runner/miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
"env-runner/miniflare/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"env-runner/miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
}
}
-24
View File
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
+45 -30
View File
@@ -1,35 +1,50 @@
# React + TypeScript + Vite
# Documentation site
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
文档站使用 Vite、React、Satteri 和 MDX。默认语言为 `zh-Hans`,英文页面位于 `/en-US`。
Currently, two official plugins are available:
## Development
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
Note: This will impact Vite dev & build performances.
You can also try [the experimental native React Compiler support in plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md#rust-react-compiler) by using `compiler: true` in the plugin options instead of using the Babel plugin.
## Expanding the Oxlint configuration
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
```json
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"options": {
"typeAware": true
},
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
```sh
bun run --cwd docs dev
```
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
开发服务器直接监听 `packages/*/docs/*/**/*.mdx`。新增或修改文档后,页面注册表与开发搜索索引会随 Vite 模块图更新。
## Static generation
```sh
bun run --cwd docs build
```
构建过程依次执行:
1. 类型检查并构建浏览器资源;
2. 构建仅供预渲染使用的 SSR 入口;
3. 枚举注册表中的首页、设置页和所有双语文章路由;
4. 将每个路由预渲染到 `dist/<route>/index.html`;
5. 生成 `dist/404.html` 和按语言拆分的搜索索引;
6. 删除临时 `.ssr` 目录。
生成的 `dist` 可以直接部署到静态文件服务。页面在浏览器中使用 hydration 恢复交互,后续站内导航仍由轻量客户端路由处理。
## Deployment
文档站通过 Cloudflare Workers Static Assets 部署到 <https://react-app-kit.go-slim.dev>:
```sh
bun run --cwd docs deploy
```
该命令会先执行完整的静态构建,再通过 Wrangler 发布 `dist`。首次部署前需要使用 `bunx wrangler login` 登录拥有 `go-slim.dev` Cloudflare Zone 的账号;自定义域名、静态路由和 `404.html` 行为均由 `wrangler.jsonc` 管理。
## Search data
搜索正文不会打进入口 JavaScript:
- 生产构建从 MDX 原始文本生成 `dist/search/zh-Hans.json` 和 `dist/search/en-US.json`;
- 用户第一次执行搜索时,浏览器只请求当前语言的索引,并在当前会话中缓存;
- 切换语言后才会按需请求另一份索引;
- MDX 页面组件按文章拆分为独立 chunk;
- 开发模式使用相同的索引生成逻辑,但从 Vite 监听中的 MDX 源文件即时构建。
新增符合 `packages/<package>/docs/<locale>/**` 目录约定的文档后,无需手工维护搜索列表。
-255
View File
@@ -1,255 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "docs",
"dependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8",
},
"devDependencies": {
"@babel/core": "^7.29.7",
"@rolldown/plugin-babel": "^0.2.3",
"@types/babel__core": "^7.20.5",
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
"@vitejs/plugin-react": "^6.1.1",
"babel-plugin-react-compiler": "^1.0.0",
"oxlint": "^1.81.0",
"typescript": "~6.0.2",
"vite": "^8.3.0",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.83.0", "", { "os": "android", "cpu": "arm" }, "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA=="],
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.83.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg=="],
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.83.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ=="],
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.83.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA=="],
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.83.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w=="],
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.83.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw=="],
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.83.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA=="],
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.83.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg=="],
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.83.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew=="],
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.83.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g=="],
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.83.0", "", { "os": "linux", "cpu": "none" }, "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw=="],
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.83.0", "", { "os": "linux", "cpu": "none" }, "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw=="],
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.83.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ=="],
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.83.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw=="],
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.83.0", "", { "os": "linux", "cpu": "x64" }, "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ=="],
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.83.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA=="],
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.83.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg=="],
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.83.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.83.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q=="],
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.8", "", { "os": "android", "cpu": "arm" }, "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.8", "", { "os": "android", "cpu": "arm64" }, "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.8", "", { "os": "linux", "cpu": "arm" }, "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.8", "", { "os": "none", "cpu": "arm64" }, "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng=="],
"@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.4", "", { "dependencies": { "picomatch": "^4.0.7" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-ygmm2j/mN+aCvstHo0x3IOu4uc6LqXT9SJ3q4kpfG/+Yhfz0B+E47FITcC0FljZADwUFARP6E2kiE+Lc26D8wg=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/node": ["@types/node@24.13.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug=="],
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
"@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.24", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg=="],
"browserslist": ["browserslist@4.29.0", "", { "dependencies": { "baseline-browser-mapping": "^2.11.23", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.427", "node-releases": "^2.0.55", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.430", "", {}, "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
"node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="],
"oxlint": ["oxlint@1.83.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.83.0", "@oxlint/binding-android-arm64": "1.83.0", "@oxlint/binding-darwin-arm64": "1.83.0", "@oxlint/binding-darwin-x64": "1.83.0", "@oxlint/binding-freebsd-x64": "1.83.0", "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", "@oxlint/binding-linux-arm-musleabihf": "1.83.0", "@oxlint/binding-linux-arm64-gnu": "1.83.0", "@oxlint/binding-linux-arm64-musl": "1.83.0", "@oxlint/binding-linux-ppc64-gnu": "1.83.0", "@oxlint/binding-linux-riscv64-gnu": "1.83.0", "@oxlint/binding-linux-riscv64-musl": "1.83.0", "@oxlint/binding-linux-s390x-gnu": "1.83.0", "@oxlint/binding-linux-x64-gnu": "1.83.0", "@oxlint/binding-linux-x64-musl": "1.83.0", "@oxlint/binding-openharmony-arm64": "1.83.0", "@oxlint/binding-win32-arm64-msvc": "1.83.0", "@oxlint/binding-win32-ia32-msvc": "1.83.0", "@oxlint/binding-win32-x64-msvc": "1.83.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
"react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
"rolldown": ["rolldown@1.2.8", "", { "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.8", "@rolldown/binding-android-arm64": "1.2.8", "@rolldown/binding-darwin-arm64": "1.2.8", "@rolldown/binding-darwin-x64": "1.2.8", "@rolldown/binding-freebsd-x64": "1.2.8", "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", "@rolldown/binding-linux-arm64-gnu": "1.2.8", "@rolldown/binding-linux-arm64-musl": "1.2.8", "@rolldown/binding-linux-ppc64-gnu": "1.2.8", "@rolldown/binding-linux-s390x-gnu": "1.2.8", "@rolldown/binding-linux-x64-gnu": "1.2.8", "@rolldown/binding-linux-x64-musl": "1.2.8", "@rolldown/binding-openharmony-arm64": "1.2.8", "@rolldown/binding-win32-arm64-msvc": "1.2.8", "@rolldown/binding-win32-x64-msvc": "1.2.8" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ=="],
"scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="],
"vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<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" />
<title>docs</title>
<title>Simple React App Kit Documentation</title>
</head>
<body>
<div id="root"></div>
+15 -6
View File
@@ -5,16 +5,23 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"build": "tsc -b && vite build && vite build --ssr src/entry-server.tsx --outDir .ssr && bun scripts/prerender.ts",
"deploy": "bun run build && wrangler deploy",
"preview": "vite preview",
"typecheck": "tsc -b"
},
"dependencies": {
"@icones/react": "^0.0.1",
"@shikijs/core": "^4.4.3",
"@shikijs/engine-javascript": "^4.4.3",
"@shikijs/langs": "^4.4.3",
"@shikijs/themes": "^4.4.3",
"@workspace/appearance": "workspace:*",
"@workspace/i18n": "workspace:*",
"@workspace/media": "workspace:*",
"@workspace/overlays": "workspace:*",
"@workspace/page": "workspace:*",
"@workspace/preferences": "workspace:*",
"@workspace/ui": "workspace:*",
"@workspace/search": "workspace:*",
"clsx": "^2.1.1",
"react": "^19.2.8",
@@ -32,11 +39,13 @@
"@types/react-dom": "^19.2.7",
"@vitejs/plugin-react": "^6.1.1",
"babel-plugin-react-compiler": "^1.0.0",
"oxlint": "^1.81.0",
"satteri": "^0.10.5",
"satteri-expressive-code": "^0.1.24",
"tailwindcss": "^4.3.3",
"typescript": "~6.0.2",
"vite": "^8.3.0",
"vite-plugin-satteri": "^0.3.5"
"vite-plugin-satteri": "^0.3.5",
"wrangler": "^4.135.0",
"yaml": "^2.9.1"
}
}
}
-24
View File
@@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

+88
View File
@@ -0,0 +1,88 @@
import { mkdir, readFile, rm, writeFile } from "node:fs/promises"
import { dirname, join } from "node:path"
import { pathToFileURL } from "node:url"
import { docLocales } from "../src/content/types.ts"
type StaticPage = {
html: string
locale: (typeof docLocales)[number]
metadata: { description: string; title: string }
}
type ServerEntry = {
getStaticDocRoutes: () => readonly string[]
getStaticSearchIndex: (locale: (typeof docLocales)[number]) => unknown
renderStaticPage: (pathname: string) => Promise<StaticPage>
}
const docsDirectory = dirname(dirname(import.meta.filename))
const outputDirectory = join(docsDirectory, "dist")
const serverDirectory = join(docsDirectory, ".ssr")
const serverEntryUrl = pathToFileURL(
join(serverDirectory, "entry-server.js")
).href
const server = (await import(serverEntryUrl)) as ServerEntry
const template = await readFile(join(outputDirectory, "index.html"), "utf8")
await Promise.all(
server.getStaticDocRoutes().map(async (route) => {
const page = await server.renderStaticPage(route)
const filename = getRouteFilename(route)
await mkdir(dirname(filename), { recursive: true })
await writeFile(filename, createHtml(template, page), "utf8")
})
)
const notFoundPage = await server.renderStaticPage("/404")
await writeFile(
join(outputDirectory, "404.html"),
createHtml(template, notFoundPage),
"utf8"
)
const searchDirectory = join(outputDirectory, "search")
await mkdir(searchDirectory, { recursive: true })
await Promise.all(
docLocales.map((locale) =>
writeFile(
join(searchDirectory, `${locale}.json`),
JSON.stringify(server.getStaticSearchIndex(locale)),
"utf8"
)
)
)
await rm(serverDirectory, { recursive: true })
function getRouteFilename(route: string) {
return route === "/"
? join(outputDirectory, "index.html")
: join(outputDirectory, route.slice(1), "index.html")
}
function createHtml(
source: string,
page: Awaited<ReturnType<typeof server.renderStaticPage>>
) {
const description = `<meta name="description" content="${escapeAttribute(page.metadata.description)}" />`
return source
.replace(/<html lang="[^"]*">/, `<html lang="${page.locale}">`)
.replace(
/<title>[^<]*<\/title>/,
`<title>${escapeText(page.metadata.title)}</title>\n ${description}`
)
.replace('<div id="root"></div>', `<div id="root">${page.html}</div>`)
}
function escapeText(value: string) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
}
function escapeAttribute(value: string) {
return escapeText(value).replaceAll('"', "&quot;")
}
+36 -18
View File
@@ -12,13 +12,15 @@ import { DocsPreferencesProvider } from "./components/preferences-provider"
import { DocsSearchProvider } from "./components/docs-search-provider"
import { resolveDocLocale } from "./content/registry"
import { useTranslate } from "./lib/locale"
import { PathnameProvider } from "./lib/router"
import type { Preferences } from "@workspace/preferences"
const Index = lazy(() => import("./pages/index"))
const Article = lazy(() => import("./pages/article"))
const Settings = lazy(() => import("./pages/settings"))
type RouteDefinition = {
Component: ComponentType
Component: ComponentType<{ pathname: string }>
matches: (pathname: string) => boolean
}
@@ -40,8 +42,15 @@ const routes: RouteDefinition[] = [
},
]
function App() {
const [currentUrl, setCurrentUrl] = useState(() => window.location.href)
export type AppProps = {
initialPreferences?: Preferences
initialUrl?: string
}
function App({ initialPreferences, initialUrl }: AppProps) {
const [currentUrl, setCurrentUrl] = useState(
() => initialUrl ?? window.location.href
)
useEffect(() => {
function updateRoute() {
@@ -99,26 +108,35 @@ function App() {
}
}, [])
const { pathname } = new URL(currentUrl)
const { pathname } = new URL(currentUrl, "https://docs.local")
const Route = routes.find((route) => route.matches(pathname))?.Component
const locale = resolveDocLocale(pathname)
return (
<DocsPreferencesProvider routeLocale={locale}>
<DocsPreferencesProvider
initialPreferences={initialPreferences}
routeLocale={locale}
>
<LocaleProvider locale={locale}>
<DocsSearchProvider>
<div style={rootCSSVariables}>
<Suspense
fallback={
<p className="p-6 text-slate-500 dark:text-slate-400">
Loading…
</p>
}
>
{Route ? <Route key={pathname} /> : <NotFound />}
</Suspense>
</div>
</DocsSearchProvider>
<PathnameProvider pathname={pathname}>
<DocsSearchProvider>
<div style={rootCSSVariables}>
<Suspense
fallback={
<p className="p-6 text-slate-500 dark:text-slate-400">
Loading…
</p>
}
>
{Route ? (
<Route key={pathname} pathname={pathname} />
) : (
<NotFound />
)}
</Suspense>
</div>
</DocsSearchProvider>
</PathnameProvider>
</LocaleProvider>
</DocsPreferencesProvider>
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File
+56 -67
View File
@@ -9,18 +9,8 @@ import { messages as searchMessagesEnUS } from "@workspace/search/locales/en-US"
import { messages as searchMessagesZhHans } from "@workspace/search/locales/zh-Hans"
import React from "react"
import {
getDocHref,
getDocPackages,
getDocSearchText,
getDocSectionLabel,
} from "../content/registry"
import type {
DocLocale,
DocPackage,
DocPage,
DocTocItem,
} from "../content/types"
import { loadDocSearchIndex } from "../content/search-index.client"
import type { DocLocale, DocSearchEntry } from "../content/types"
import { useLocale } from "../lib/locale"
const searchCatalogs = {
@@ -57,69 +47,73 @@ export function DocsSearchProvider({
}
function createDocsSearchAdapter(locale: DocLocale): SearchAdapter {
const docPackages = getDocPackages(locale)
return {
async search({ query, signal }) {
const normalizedQuery = normalize(query)
if (!normalizedQuery) return { groups: [] }
const index = await loadDocSearchIndex(locale)
if (signal.aborted) return { groups: [] }
const terms = normalizedQuery.split(/\s+/).filter(Boolean)
const groups = docPackages.flatMap((docPackage) => {
const items = docPackage.pages
.map((page) => ({
item: createResultItem(docPackage, page, locale),
score: getMatchScore(
normalizedQuery,
terms,
docPackage.label,
page
),
}))
.filter((result) => result.score >= 0)
.sort(
const groups = new Map<
string,
{
id: string
items: Array<{ item: SearchResultItem; score: number }>
label: string
total: number
}
>()
for (const entry of index) {
const score = getMatchScore(normalizedQuery, terms, entry)
if (score < 0) continue
const group = groups.get(entry.packageKey) ?? {
id: entry.packageKey,
items: [],
label: entry.packageLabel,
total: 0,
}
group.items.push({ item: createResultItem(entry), score })
group.total += 1
groups.set(entry.packageKey, group)
}
const results = Array.from(groups.values(), (group) => ({
id: group.id,
label: group.label,
total: group.total,
items: group.items
.toSorted(
(left, right) =>
right.score - left.score ||
left.item.title.localeCompare(right.item.title, locale)
)
.map((result) => result.item)
return items.length > 0
? [
{
id: docPackage.key,
label: docPackage.label,
items,
total: items.length,
},
]
: []
})
.map((result) => result.item),
}))
if (signal.aborted) return { groups: [] }
return { groups }
return { groups: results }
},
}
}
function createResultItem(
docPackage: DocPackage,
page: DocPage,
locale: DocLocale
): SearchResultItem {
function createResultItem(entry: DocSearchEntry): SearchResultItem {
return {
id: `${page.section}/${page.slug}`,
title: page.title,
description: page.description,
id: entry.id,
title: entry.title,
description: entry.description,
keywords: [
docPackage.label,
getDocSectionLabel(page.section, locale),
...page.toc.flatMap(flattenTocTitles),
getDocSearchText(page),
entry.packageLabel,
entry.sectionLabel,
...entry.toc,
entry.body,
],
meta: [getDocSectionLabel(page.section, locale)],
meta: [entry.sectionLabel],
payload: {
href: getDocHref(docPackage, page),
href: entry.href,
} satisfies DocsSearchPayload,
}
}
@@ -127,14 +121,13 @@ function createResultItem(
function getMatchScore(
query: string,
terms: readonly string[],
packageLabel: string,
page: DocPage
entry: DocSearchEntry
) {
const title = normalize(page.title)
const description = normalize(page.description)
const packageName = normalize(packageLabel)
const toc = normalize(page.toc.flatMap(flattenTocTitles).join(" "))
const body = normalize(getDocSearchText(page))
const title = normalize(entry.title)
const description = normalize(entry.description)
const packageName = normalize(entry.packageLabel)
const toc = normalize(entry.toc.join(" "))
const body = normalize(entry.body)
const searchableText = [packageName, title, description, toc, body].join(" ")
if (!terms.every((term) => searchableText.includes(term))) return -1
@@ -147,15 +140,11 @@ function getMatchScore(
else if (packageName.includes(query)) score += 20
if (description.includes(query)) score += 15
if (toc.includes(query)) score += 10
if (page.section === "overview") score += 2
if (entry.id === "overview/overview") score += 2
return score
}
function flattenTocTitles(item: DocTocItem): readonly string[] {
return [item.title, ...(item.items?.flatMap(flattenTocTitles) ?? [])]
}
function normalize(value: string) {
return value.normalize("NFKC").toLocaleLowerCase().trim()
}
-264
View File
@@ -1,264 +0,0 @@
import { Icon } from "@icones/react"
import { usePreference } from "@workspace/preferences"
import { useEffect, useId, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { LocaleFlag } from "./locale-flag"
import { getLocalizedHref } from "../content/registry"
import type { DocLocale } from "../content/types"
import { useLocale, useTranslate } from "../lib/locale"
import { useIsMobile } from "../lib/use-mobile"
type LocaleOption = {
label: string
locale: DocLocale
}
const localeOptions: readonly LocaleOption[] = [
{ label: "简体中文", locale: "zh-Hans" },
{ label: "English", locale: "en-US" },
]
export function LanguageSwitcher() {
const locale = useLocale()
const t = useTranslate()
const isMobile = useIsMobile()
const [, setLocaleMode] = usePreference("locale-mode")
const [, setPreferredLocale] = usePreference("locale")
const [open, setOpen] = useState(false)
const rootRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
const menuId = useId()
const selectedLocale = getLocaleOption(locale)
useEffect(() => {
if (!open) return
function isInsideSwitcher(target: EventTarget | null) {
return (
target instanceof Node &&
(rootRef.current?.contains(target) || menuRef.current?.contains(target))
)
}
function closeAndRestoreFocus() {
setOpen(false)
window.requestAnimationFrame(() => triggerRef.current?.focus())
}
function handlePointerDown(event: PointerEvent) {
if (isInsideSwitcher(event.target)) return
if (isMobile) {
closeAndRestoreFocus()
} else {
setOpen(false)
}
}
function handleFocusIn(event: FocusEvent) {
if (!isMobile && !isInsideSwitcher(event.target)) setOpen(false)
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
event.preventDefault()
closeAndRestoreFocus()
return
}
if (!isMobile || event.key !== "Tab") return
const items = getMenuItems(menuRef.current)
const firstItem = items[0]
const lastItem = items.at(-1)
if (!firstItem || !lastItem) return
if (event.shiftKey && document.activeElement === firstItem) {
event.preventDefault()
lastItem.focus()
} else if (!event.shiftKey && document.activeElement === lastItem) {
event.preventDefault()
firstItem.focus()
}
}
const previousOverflow = document.body.style.overflow
if (isMobile) {
document.body.style.overflow = "hidden"
window.requestAnimationFrame(() => {
menuRef.current
?.querySelector<HTMLElement>('[aria-checked="true"]')
?.focus()
})
}
document.addEventListener("pointerdown", handlePointerDown)
document.addEventListener("focusin", handleFocusIn)
document.addEventListener("keydown", handleKeyDown)
return () => {
document.removeEventListener("pointerdown", handlePointerDown)
document.removeEventListener("focusin", handleFocusIn)
document.removeEventListener("keydown", handleKeyDown)
if (isMobile) document.body.style.overflow = previousOverflow
}
}, [isMobile, open])
function focusMenuItem(position: "first" | "last") {
setOpen(true)
window.requestAnimationFrame(() => {
const items = getMenuItems(menuRef.current)
const item = position === "first" ? items[0] : items.at(-1)
item?.focus()
})
}
function selectLocale(nextLocale: DocLocale) {
setPreferredLocale(nextLocale)
setLocaleMode("manual")
setOpen(false)
window.requestAnimationFrame(() => triggerRef.current?.focus())
}
const menu = (
<div
ref={menuRef}
id={menuId}
role={isMobile ? "dialog" : undefined}
aria-label={t({ zh: "语言", us: "Language" })}
aria-modal={isMobile || undefined}
className="w-[min(80vw,20rem)] rounded-xl border border-slate-200 bg-white/95 p-4 shadow-2xl backdrop-blur-md md:w-auto md:min-w-40 md:p-1 dark:border-slate-800 dark:bg-slate-950/95 dark:shadow-black/60"
>
<p className="px-2 py-2 text-sm text-slate-500 md:text-xs dark:text-slate-400">
{t({ zh: "语言", us: "Language" })}
</p>
<ul
role="menu"
aria-label={t({ zh: "语言", us: "Languages" })}
onKeyDown={(event) => {
if (
event.key !== "ArrowDown" &&
event.key !== "ArrowUp" &&
event.key !== "Home" &&
event.key !== "End"
) {
return
}
event.preventDefault()
const items = getMenuItems(event.currentTarget)
const currentIndex = items.indexOf(
document.activeElement as HTMLButtonElement
)
let nextIndex = currentIndex
if (event.key === "Home") nextIndex = 0
if (event.key === "End") nextIndex = items.length - 1
if (event.key === "ArrowDown") {
nextIndex = (currentIndex + 1) % items.length
}
if (event.key === "ArrowUp") {
nextIndex = (currentIndex - 1 + items.length) % items.length
}
items[nextIndex]?.focus()
}}
>
{localeOptions.map((option) => {
const selected = option.locale === locale
return (
<li role="none" key={option.locale}>
<a
role="menuitemradio"
aria-checked={selected}
lang={option.locale}
href={getLocalizedHref(window.location.pathname, option.locale)}
className="flex h-11 w-full cursor-pointer items-center gap-2 rounded-lg px-2 text-left whitespace-nowrap transition-colors hover:bg-slate-100 focus:bg-slate-100 focus:outline-none aria-checked:text-indigo-600 dark:hover:bg-slate-900 dark:focus:bg-slate-900 dark:aria-checked:text-indigo-400"
onClick={() => selectLocale(option.locale)}
>
<LocaleFlag
locale={option.locale}
className="size-5 shrink-0"
/>
<span>{option.label}</span>
{selected && (
<Icon
name="tabler:check"
className="ml-auto size-4 shrink-0"
/>
)}
</a>
</li>
)
})}
</ul>
</div>
)
return (
<div ref={rootRef} className="flex items-center md:relative">
<button
ref={triggerRef}
type="button"
aria-controls={open ? menuId : undefined}
aria-expanded={open}
aria-haspopup={isMobile ? "dialog" : "menu"}
aria-label={t({ zh: "选择语言", us: "Choose language" })}
title={t({ zh: "选择语言", us: "Choose language" })}
className="inline-flex size-8 cursor-pointer items-center justify-center rounded-full transition-colors outline-none hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:hover:bg-slate-800 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-slate-950"
onClick={() => setOpen((current) => !current)}
onKeyDown={(event) => {
if (event.key === "ArrowDown") {
event.preventDefault()
focusMenuItem("first")
} else if (event.key === "ArrowUp") {
event.preventDefault()
focusMenuItem("last")
}
}}
>
<LocaleFlag locale={selectedLocale.locale} className="size-7" />
</button>
{open &&
(isMobile ? (
createPortal(
<div className="fixed inset-0 z-100 flex items-center justify-center p-4">
<button
type="button"
tabIndex={-1}
aria-label={t({
zh: "关闭语言菜单",
us: "Close language menu",
})}
className="absolute inset-0 cursor-default bg-black/60 backdrop-blur-sm"
onClick={() => {
setOpen(false)
window.requestAnimationFrame(() =>
triggerRef.current?.focus()
)
}}
/>
<div className="relative">{menu}</div>
</div>,
document.body
)
) : (
<div className="absolute top-full right-0 z-100 pt-2">{menu}</div>
))}
</div>
)
}
function getMenuItems(root: ParentNode | null) {
return Array.from(
root?.querySelectorAll<HTMLElement>('[role="menuitemradio"]') ?? []
)
}
function getLocaleOption(locale: DocLocale) {
return localeOptions.find((option) => option.locale === locale)!
}
+104 -6
View File
@@ -1,11 +1,11 @@
import { Icon } from "@icones/react"
import { cn } from "../lib/utils"
import { useTranslate } from "../lib/locale"
import { useLocale, useTranslate } from "../lib/locale"
import React from "react"
import { SearchTrigger } from "./search-trigger"
import { SettingsLink } from "./settings-link"
import { getDocsHomeHref } from "../content/registry"
// oxlint-disable-next-line react/only-export-components -- Shared layout tokens are consumed by App.
export const rootCSSVariables = {
"--cw": "60rem",
"--max-lw": "18rem",
@@ -38,7 +38,7 @@ export function HomeButton({
return (
<a
href={href}
className="flex items-center gap-2 py-1 transition-colors hover:text-indigo-500 dark:hover:text-indigo-400"
className="flex items-center gap-2 py-1 transition-colors hover:text-indigo-500 dark:hover:text-indigo-400 shrink-0"
>
<Icon name="tabler:smart-home" className="size-5" />
{label ?? t({ zh: "首页", us: "Home" })}
@@ -84,11 +84,109 @@ export function SlashLine({ className }: { className?: string }) {
)
}
export function Actions({ className }: { className?: string }) {
export function Actions() {
return (
<div className={cn("inline-flex items-center gap-2", className)}>
<>
<SearchTrigger />
<SettingsLink />
</div>
</>
)
}
export function SiteHeader({
banner,
bottom,
leading,
subleading,
actions,
locale,
...rest
}: React.ComponentProps<"header"> & {
banner?: React.ReactNode
bottom?: React.ReactNode
leading?: React.ReactNode
subleading?: React.ReactNode
actions?: React.ReactNode
locale?: string
}) {
return (
<header {...rest}>
{banner}
<HeaderInner
leading={leading}
subleading={subleading}
locale={locale}
actions={actions}
/>
{bottom}
</header>
)
}
function HeaderInner({
leading,
subleading,
actions,
locale,
}: {
leading?: React.ReactNode
subleading?: React.ReactNode
actions?: React.ReactNode
locale?: string
}) {
const resolvedLocale = useLocale() ?? locale
const hasLeading = React.Children.count(leading) > 0
const hasSubleading = React.Children.count(subleading) > 0
return (
<Container className="flex size-full items-center gap-4">
{hasLeading || hasSubleading ? (
<>
{hasLeading ? (
leading
) : (
<HomeButton href={getDocsHomeHref(resolvedLocale)} />
)}
{hasSubleading ? (
<>
<SlashLine />
{subleading}
</>
) : null}
</>
) : (
<h1 className="min-w-0 text-2xl font-semibold sm:text-3xl">
<span>S</span>
<span className="text-slate-300 dark:text-slate-400">imple</span>
<span>&nbsp;</span>
<span>R</span>
<span className="text-slate-300 dark:text-slate-400">eact</span>
<span>&nbsp;</span>
<span>A</span>
<span className="text-slate-300 dark:text-slate-400">pp</span>
<span>&nbsp;</span>
<span>K</span>
<span className="text-slate-300 dark:text-slate-400">it</span>
</h1>
)}
{actions != null ? (
<div className="-mr-2 ml-auto inline-flex items-center gap-2">
{actions}
</div>
) : null}
</Container>
)
}
export function SiteFooter({
className,
...rest
}: React.ComponentProps<"footer">) {
return (
<footer className={cn("py-8", className)} {...rest}>
<Container className="text-center text-slate-500 dark:text-slate-400">
<div>Copyright © 2026</div>
</Container>
</footer>
)
}
+3 -2
View File
@@ -1,6 +1,6 @@
import type { MdxComponents } from "../content/types"
import { Sandbox, SingleFileSandbox } from "./sandbox"
/* oxlint-disable react/only-export-components -- This module exports an MDX component registry. */
export const mdxComponents: MdxComponents = {
a: MdxAnchor,
blockquote: MdxBlockquote,
@@ -8,6 +8,8 @@ export const mdxComponents: MdxComponents = {
h2: MdxHeading2,
h3: MdxHeading3,
pre: MdxPre,
Sandbox,
SingleFileSandbox,
}
function MdxHeading2({ children, id, ...props }: React.ComponentProps<"h2">) {
@@ -90,4 +92,3 @@ function MdxPre({ className, ...props }: React.ComponentProps<"pre">) {
/>
)
}
/* oxlint-enable react/only-export-components */
+39 -3
View File
@@ -36,12 +36,17 @@ type PreferencesMessage =
export function DocsPreferencesProvider({
children,
initialPreferences,
routeLocale,
}: {
children: ReactNode
initialPreferences?: Preferences
routeLocale: DocLocale
}) {
const [initialPreferences] = useState(() => readDocsPreferences(routeLocale))
const hydrateStoredPreferences = initialPreferences !== undefined
const [providerInitialPreferences] = useState(
() => initialPreferences ?? readDocsPreferences(routeLocale)
)
const channelRef = useRef<BroadcastChannel>(null)
const applyingExternalUpdateRef = useRef(false)
@@ -58,12 +63,13 @@ export function DocsPreferencesProvider({
return (
<PreferencesProvider
effects={docsPreferenceEffects}
initialPreferences={initialPreferences}
initialPreferences={providerInitialPreferences}
onPreferenceChange={handlePreferenceChange}
>
<PreferencesRuntime
applyingExternalUpdateRef={applyingExternalUpdateRef}
channelRef={channelRef}
hydrateStoredPreferences={hydrateStoredPreferences}
routeLocale={routeLocale}
/>
{children}
@@ -74,10 +80,12 @@ export function DocsPreferencesProvider({
function PreferencesRuntime({
applyingExternalUpdateRef,
channelRef,
hydrateStoredPreferences,
routeLocale,
}: {
applyingExternalUpdateRef: MutableRefObject<boolean>
channelRef: MutableRefObject<BroadcastChannel | null>
hydrateStoredPreferences: boolean
routeLocale: DocLocale
}) {
const preferences = usePreferences()
@@ -89,6 +97,32 @@ function PreferencesRuntime({
const [, setLocale] = usePreference("locale")
const [, setThemeMode] = usePreference("theme-mode")
useEffect(() => {
if (!hydrateStoredPreferences) return
const storedPreferences = readDocsPreferences(routeLocale)
applyingExternalUpdateRef.current = true
try {
setAccentColor(storedPreferences["accent-color"])
setBackgroundShade(storedPreferences["background-shade"])
setForegroundShade(storedPreferences["foreground-shade"])
setLocaleMode(storedPreferences["locale-mode"])
setLocale(storedPreferences.locale)
setThemeMode(storedPreferences["theme-mode"])
} finally {
applyingExternalUpdateRef.current = false
}
}, [
applyingExternalUpdateRef,
hydrateStoredPreferences,
routeLocale,
setAccentColor,
setBackgroundShade,
setForegroundShade,
setLocale,
setLocaleMode,
setThemeMode,
])
useLayoutEffect(() => {
preferencesRef.current = preferences
}, [preferences])
@@ -148,10 +182,12 @@ function PreferencesRuntime({
if (!message || typeof message !== "object") return
if (message.type === "request") {
/* oxlint-disable unicorn/require-post-message-target-origin -- BroadcastChannel has no targetOrigin argument. */
channel.postMessage({
type: "snapshot",
preferences: preferencesRef.current,
} satisfies PreferencesMessage)
/* oxlint-enable unicorn/require-post-message-target-origin */
return
}
@@ -175,6 +211,7 @@ function PreferencesRuntime({
}
channel.addEventListener("message", handleMessage)
// oxlint-disable-next-line unicorn/require-post-message-target-origin -- BroadcastChannel has no targetOrigin argument.
channel.postMessage({ type: "request" } satisfies PreferencesMessage)
return () => {
@@ -191,7 +228,6 @@ function PreferencesRuntime({
setLocaleMode,
setThemeMode,
])
useEffect(() => {
if (preferences["locale-mode"] !== "auto") return
@@ -0,0 +1,141 @@
import type { HighlighterCore } from "@shikijs/core"
export type HighlightedToken = {
content: string
dark: string
light: string
offset: number
}
export type HighlightedLine = {
offset: number
tokens: HighlightedToken[]
}
export type HighlightedSource = HighlightedLine[]
const languageLoaders = {
bash: () => import("@shikijs/langs/bash"),
css: () => import("@shikijs/langs/css"),
html: () => import("@shikijs/langs/html"),
javascript: () => import("@shikijs/langs/javascript"),
json: () => import("@shikijs/langs/json"),
jsx: () => import("@shikijs/langs/jsx"),
markdown: () => import("@shikijs/langs/markdown"),
mdx: () => import("@shikijs/langs/mdx"),
tsx: () => import("@shikijs/langs/tsx"),
typescript: () => import("@shikijs/langs/typescript"),
yaml: () => import("@shikijs/langs/yaml"),
} as const
type SyntaxLanguage = keyof typeof languageLoaders
const languageByExtension: Readonly<Record<string, SyntaxLanguage>> = {
bash: "bash",
cjs: "javascript",
css: "css",
html: "html",
js: "javascript",
json: "json",
jsx: "jsx",
md: "markdown",
mdx: "mdx",
mjs: "javascript",
sh: "bash",
ts: "typescript",
tsx: "tsx",
yaml: "yaml",
yml: "yaml",
zsh: "bash",
}
const highlighterPromises = new Map<SyntaxLanguage, Promise<HighlighterCore>>()
export async function highlightSource(
source: string,
filename: string
): Promise<HighlightedSource> {
const extension = filename.split(".").pop()?.toLowerCase() ?? ""
const language = languageByExtension[extension]
if (!language) return createPlainSource(source)
const highlighter = await getHighlighter(language)
const lines = highlighter.codeToTokensWithThemes(source, {
lang: language,
themes: {
dark: "github-dark",
light: "github-light",
},
})
let lineOffset = 0
return lines.map((line) => {
const highlightedLine: HighlightedLine = {
offset: lineOffset,
tokens: line.map((token) => ({
content: token.content,
dark: token.variants.dark?.color ?? "currentColor",
light: token.variants.light?.color ?? "currentColor",
offset: token.offset,
})),
}
lineOffset += line.reduce(
(length, token) => length + token.content.length,
0
)
lineOffset += 1
return highlightedLine
})
}
function getHighlighter(language: SyntaxLanguage) {
const cached = highlighterPromises.get(language)
if (cached) return cached
const highlighter = Promise.all([
import("@shikijs/core"),
import("@shikijs/engine-javascript"),
import("@shikijs/themes/github-dark"),
import("@shikijs/themes/github-light"),
languageLoaders[language](),
]).then(
([
{ createHighlighterCore },
{ createJavaScriptRegexEngine },
{ default: githubDark },
{ default: githubLight },
{ default: grammar },
]) =>
createHighlighterCore({
engine: createJavaScriptRegexEngine(),
langs: grammar,
themes: [githubDark, githubLight],
})
)
highlighterPromises.set(language, highlighter)
return highlighter
}
function createPlainSource(source: string): HighlightedSource {
let offset = 0
return source.split("\n").map((content) => {
const line: HighlightedLine = {
offset,
tokens: [
{
content,
dark: "inherit",
light: "inherit",
offset,
},
],
}
offset += content.length + 1
return line
})
}
+15
View File
@@ -0,0 +1,15 @@
export {
Sandbox,
SingleFileSandbox,
type SandboxProps,
type SingleFileSandboxProps,
} from "./sandbox"
export {
defineSandbox,
type SandboxDefinition,
type SandboxLocalizedText,
type SandboxPreviewProps,
type SandboxPreviewModule,
type SandboxSource,
type SandboxSourceModule,
} from "./types"
+27
View File
@@ -0,0 +1,27 @@
import type { SandboxDefinition } from "./types"
type SandboxManifestModule = {
default: SandboxDefinition
}
const manifestModules = import.meta.glob<SandboxManifestModule>(
[
"../../sandboxes/*/*/sandbox.ts",
"../../../../packages/*/docs/sandboxes/*/sandbox.ts",
],
{ eager: true }
)
const sandboxDefinitions = new Map<string, SandboxDefinition>()
for (const module of Object.values(manifestModules)) {
const definition = module.default
if (sandboxDefinitions.has(definition.id)) {
throw new Error(`Duplicate sandbox id: ${definition.id}`)
}
sandboxDefinitions.set(definition.id, definition)
}
export function getSandboxDefinition(id: string) {
return sandboxDefinitions.get(id)
}
+4
View File
@@ -0,0 +1,4 @@
declare module "*?raw" {
const source: string
export default source
}
@@ -0,0 +1,27 @@
import React from "react"
type SandboxErrorBoundaryProps = {
children: React.ReactNode
fallback: (error: Error) => React.ReactNode
}
type SandboxErrorBoundaryState = {
error: Error | null
}
export class SandboxErrorBoundary extends React.Component<
SandboxErrorBoundaryProps,
SandboxErrorBoundaryState
> {
state: SandboxErrorBoundaryState = { error: null }
static getDerivedStateFromError(error: Error) {
return { error }
}
render() {
return this.state.error
? this.props.fallback(this.state.error)
: this.props.children
}
}
@@ -0,0 +1,111 @@
import React from "react"
import { createPortal } from "react-dom"
import type { DocLocale } from "../../content/types"
const minimumFrameHeight = 288
export function SandboxFrame({
children,
fill = false,
locale,
reloadKey,
title,
}: {
children: React.ReactNode
fill?: boolean
locale: DocLocale
reloadKey: number
title: string
}) {
const frameRef = React.useRef<HTMLIFrameElement>(null)
const [mountNode, setMountNode] = React.useState<HTMLElement | null>(null)
const [height, setHeight] = React.useState(minimumFrameHeight)
React.useLayoutEffect(() => {
const frame = frameRef.current
const frameDocument = frame?.contentDocument
if (!frame || !frameDocument) return
frameDocument.open()
frameDocument.write(
'<!doctype html><html><head></head><body><div id="sandbox-root"></div></body></html>'
)
frameDocument.close()
for (const node of document.head.querySelectorAll(
'link[rel="stylesheet"], style'
)) {
frameDocument.head.append(node.cloneNode(true))
}
const frameOverrides = frameDocument.createElement("style")
frameOverrides.textContent = `
html, body, #sandbox-root {
min-height: 100%;
min-width: 0 !important;
}
body {
box-sizing: border-box;
margin: 0;
padding: 1.5rem;
}
`
frameDocument.head.append(frameOverrides)
const syncDocumentTheme = () => {
frameDocument.documentElement.className =
document.documentElement.className
frameDocument.documentElement.style.colorScheme =
document.documentElement.style.colorScheme
}
syncDocumentTheme()
frameDocument.documentElement.lang = locale
const themeObserver = new MutationObserver(syncDocumentTheme)
themeObserver.observe(document.documentElement, {
attributeFilter: ["class", "style"],
attributes: true,
})
const nextMountNode = frameDocument.getElementById("sandbox-root")
setMountNode(nextMountNode)
return () => {
themeObserver.disconnect()
setMountNode(null)
}
}, [locale, reloadKey])
React.useEffect(() => {
if (!mountNode || fill) return
const frameDocument = mountNode.ownerDocument
const updateHeight = () => {
setHeight(
Math.max(
minimumFrameHeight,
Math.ceil(frameDocument.documentElement.scrollHeight)
)
)
}
const resizeObserver = new ResizeObserver(updateHeight)
resizeObserver.observe(frameDocument.body)
updateHeight()
return () => resizeObserver.disconnect()
}, [fill, mountNode])
return (
<>
<iframe
ref={frameRef}
className={`block w-full border-0 bg-white dark:bg-slate-950 ${fill ? "min-h-0 flex-1" : ""}`}
style={{ height: fill ? "100%" : height }}
sandbox="allow-same-origin"
title={title}
/>
{mountNode ? createPortal(children, mountNode) : null}
</>
)
}
+519
View File
@@ -0,0 +1,519 @@
import React from "react"
import { Icon } from "@icones/react"
import { useLocale, useTranslate } from "../../lib/locale"
import { getSandboxDefinition } from "./registry"
import { SandboxErrorBoundary } from "./sandbox-error-boundary"
import { SandboxFrame } from "./sandbox-frame"
import { highlightSource, type HighlightedSource } from "./highlight-source"
import type {
SandboxDefinition,
SandboxLocalizedText,
SandboxPreviewProps,
SandboxPreviewModule,
SandboxSource,
} from "./types"
type SandboxView = "code" | "preview"
const codeFontSizes = [12, 13, 14, 16] as const
export type SandboxProps = {
className?: string
example: string
title?: SandboxLocalizedText
}
export function Sandbox({ className, example, title }: SandboxProps) {
const definition = getSandboxDefinition(example)
const t = useTranslate()
if (!definition) {
return (
<SandboxMessage className={className} tone="error">
{t({
zh: `找不到 Sandbox 示例:${example}`,
us: `Sandbox example not found: ${example}`,
})}
</SandboxMessage>
)
}
return (
<SandboxViewer
className={className}
definition={definition}
title={title}
/>
)
}
export type SingleFileSandboxProps = {
className?: string
component: React.ComponentType
filename?: string
source: string
title?: SandboxLocalizedText
}
export function SingleFileSandbox({
className,
component,
filename = "App.tsx",
source,
title,
}: SingleFileSandboxProps) {
const definition = React.useMemo<SandboxDefinition>(
() => ({
defaultFile: filename,
entry: async () => ({
default: component as React.ComponentType<SandboxPreviewProps>,
}),
files: { [filename]: source },
id: `single-file:${filename}`,
title,
}),
[component, filename, source, title]
)
return <SandboxViewer className={className} definition={definition} />
}
function SandboxViewer({
className,
definition,
title,
}: {
className?: string
definition: SandboxDefinition
title?: SandboxLocalizedText
}) {
const rootRef = React.useRef<HTMLElement>(null)
const locale = useLocale()
const t = useTranslate()
const filenames = React.useMemo(
() => Object.keys(definition.files),
[definition.files]
)
const [view, setView] = React.useState<SandboxView>("preview")
const [selectedFile, setSelectedFile] = React.useState(
definition.defaultFile ?? filenames[0] ?? ""
)
const [previewModule, setPreviewModule] =
React.useState<SandboxPreviewModule | null>(null)
const [previewError, setPreviewError] = React.useState<Error | null>(null)
const [source, setSource] = React.useState<string | null>(null)
const [sourceError, setSourceError] = React.useState<Error | null>(null)
const [highlightedSource, setHighlightedSource] =
React.useState<HighlightedSource | null>(null)
const [codeFontSizeIndex, setCodeFontSizeIndex] = React.useState(1)
const [reloadKey, setReloadKey] = React.useState(0)
const [copied, setCopied] = React.useState(false)
const [fullscreen, setFullscreen] = React.useState(false)
const resolvedTitle = resolveSandboxText(
title ?? definition.title,
locale,
definition.id
)
React.useEffect(() => {
const root = rootRef.current
if (!root) return
const ownerDocument = root.ownerDocument
const syncFullscreen = () => {
setFullscreen(ownerDocument.fullscreenElement === root)
}
syncFullscreen()
ownerDocument.addEventListener("fullscreenchange", syncFullscreen)
return () => {
ownerDocument.removeEventListener("fullscreenchange", syncFullscreen)
}
}, [])
React.useEffect(() => {
let active = true
setPreviewModule(null)
setPreviewError(null)
definition.entry().then(
(module) => {
if (active) setPreviewModule(module)
},
(error: unknown) => {
if (active) setPreviewError(toError(error))
}
)
return () => {
active = false
}
}, [definition])
React.useEffect(() => {
if (view !== "code") return
const file = definition.files[selectedFile]
if (!file) {
setSource(null)
setSourceError(new Error(`Source file not found: ${selectedFile}`))
return
}
let active = true
setSource(null)
setSourceError(null)
setHighlightedSource(null)
resolveSource(file).then(
(value) => {
if (active) setSource(value)
},
(error: unknown) => {
if (active) setSourceError(toError(error))
}
)
return () => {
active = false
}
}, [definition.files, selectedFile, view])
React.useEffect(() => {
if (!source || view !== "code") return
let active = true
setHighlightedSource(null)
highlightSource(source, selectedFile).then(
(value) => {
if (active) setHighlightedSource(value)
},
() => {
if (active) setHighlightedSource(null)
}
)
return () => {
active = false
}
}, [selectedFile, source, view])
React.useEffect(() => {
setSelectedFile(definition.defaultFile ?? filenames[0] ?? "")
}, [definition.defaultFile, filenames])
const copySource = async () => {
if (!source) return
await navigator.clipboard.writeText(source)
setCopied(true)
window.setTimeout(() => setCopied(false), 1500)
}
const toggleFullscreen = async () => {
const root = rootRef.current
if (!root) return
const ownerDocument = root.ownerDocument
if (ownerDocument.fullscreenElement === root) {
await ownerDocument.exitFullscreen()
return
}
if (ownerDocument.fullscreenElement) {
await ownerDocument.exitFullscreen()
}
await root.requestFullscreen()
}
return (
<section
ref={rootRef}
className={`not-prose overflow-hidden border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950 ${fullscreen ? "m-0 flex h-dvh w-dvw flex-col rounded-none border-0" : "my-6 rounded-xl"} ${className ?? ""}`}
aria-label={resolvedTitle}
>
<div className="flex min-h-11 flex-wrap items-center gap-2 border-b border-slate-200 bg-slate-50 px-2 py-1.5 dark:border-slate-800 dark:bg-slate-900/70">
<div className="inline-flex rounded-lg bg-slate-200/70 p-0.5 dark:bg-slate-800">
<SandboxTab
active={view === "preview"}
onClick={() => setView("preview")}
>
<Icon name="lucide:play" className="size-3.5" />
{t({ zh: "预览", us: "Preview" })}
</SandboxTab>
<SandboxTab active={view === "code"} onClick={() => setView("code")}>
<Icon name="lucide:code-2" className="size-3.5" />
{t({ zh: "代码", us: "Code" })}
</SandboxTab>
</div>
<span className="min-w-0 truncate px-1 text-xs text-slate-500 dark:text-slate-400">
{resolvedTitle}
</span>
<div className="ml-auto flex items-center gap-1">
{view === "code" ? (
<>
<CodeFontSizeControl
decreaseLabel={t({ zh: "减小字号", us: "Decrease font size" })}
increaseLabel={t({ zh: "增大字号", us: "Increase font size" })}
index={codeFontSizeIndex}
onIndexChange={setCodeFontSizeIndex}
/>
<SandboxAction
disabled={!source}
label={t({ zh: "复制源码", us: "Copy source" })}
onClick={() => void copySource()}
>
<Icon
name={copied ? "lucide:check" : "lucide:copy"}
className="size-4"
/>
</SandboxAction>
</>
) : (
<SandboxAction
label={t({ zh: "重置预览", us: "Reset preview" })}
onClick={() => setReloadKey((value) => value + 1)}
>
<Icon name="lucide:rotate-ccw" className="size-4" />
</SandboxAction>
)}
<SandboxAction
label={t({
zh: fullscreen ? "退出全屏" : "全屏显示",
us: fullscreen ? "Exit fullscreen" : "Enter fullscreen",
})}
pressed={fullscreen}
onClick={() => void toggleFullscreen()}
>
<Icon
name={fullscreen ? "lucide:minimize-2" : "lucide:maximize-2"}
className="size-4"
/>
</SandboxAction>
</div>
</div>
{view === "preview" ? (
<SandboxFrame
fill={fullscreen}
locale={locale}
reloadKey={reloadKey}
title={resolvedTitle}
>
<SandboxErrorBoundary key={reloadKey} fallback={renderSandboxError}>
{previewError ? (
<SandboxMessage tone="error">
{previewError.message}
</SandboxMessage>
) : previewModule ? (
React.createElement(previewModule.default, {
key: reloadKey,
locale,
})
) : (
<SandboxMessage>
{t({ zh: "正在加载预览…", us: "Loading preview…" })}
</SandboxMessage>
)}
</SandboxErrorBoundary>
</SandboxFrame>
) : (
<div
className={`grid min-h-72 ${fullscreen ? "min-h-0 flex-1 overflow-hidden" : ""} ${filenames.length > 1 ? "md:grid-cols-[12rem_minmax(0,1fr)]" : ""}`}
>
{filenames.length > 1 ? (
<nav
className="flex overflow-x-auto border-b border-slate-200 bg-slate-50 p-2 md:flex-col md:border-r md:border-b-0 dark:border-slate-800 dark:bg-slate-900/40"
aria-label={t({ zh: "示例文件", us: "Example files" })}
>
{filenames.map((filename) => (
<button
key={filename}
type="button"
className="rounded-md px-2 py-1.5 text-left font-mono text-xs whitespace-nowrap text-slate-500 hover:bg-slate-200/70 hover:text-slate-950 aria-pressed:bg-indigo-100 aria-pressed:text-indigo-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100 dark:aria-pressed:bg-indigo-950 dark:aria-pressed:text-indigo-300"
aria-pressed={selectedFile === filename}
onClick={() => setSelectedFile(filename)}
>
{filename}
</button>
))}
</nav>
) : null}
<pre
className="m-0 min-w-0 overflow-auto bg-slate-50 p-4 font-mono text-slate-800 dark:bg-slate-950 dark:text-slate-100"
style={{
fontSize: `${codeFontSizes[codeFontSizeIndex]}px`,
lineHeight: 1.55,
}}
>
<code className="block min-w-max">
{sourceError ? (
sourceError.message
) : highlightedSource ? (
<HighlightedCode source={highlightedSource} />
) : (
(source ?? t({ zh: "正在加载源码…", us: "Loading source…" }))
)}
</code>
</pre>
</div>
)}
</section>
)
}
function resolveSandboxText(
value: SandboxLocalizedText | undefined,
locale: "en-US" | "zh-Hans",
fallback: string
) {
if (typeof value === "string") return value
return value?.[locale] ?? fallback
}
function CodeFontSizeControl({
decreaseLabel,
increaseLabel,
index,
onIndexChange,
}: {
decreaseLabel: string
increaseLabel: string
index: number
onIndexChange: React.Dispatch<React.SetStateAction<number>>
}) {
return (
<div
className="inline-flex h-8 items-center rounded-md border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"
aria-label={`${codeFontSizes[index]}px`}
>
<SandboxAction
disabled={index === 0}
label={decreaseLabel}
onClick={() => onIndexChange((value) => Math.max(0, value - 1))}
>
<Icon name="lucide:minus" className="size-3.5" />
</SandboxAction>
<span className="w-9 text-center font-mono text-[11px] text-slate-500 tabular-nums dark:text-slate-400">
{codeFontSizes[index]}
</span>
<SandboxAction
disabled={index === codeFontSizes.length - 1}
label={increaseLabel}
onClick={() =>
onIndexChange((value) =>
Math.min(codeFontSizes.length - 1, value + 1)
)
}
>
<Icon name="lucide:plus" className="size-3.5" />
</SandboxAction>
</div>
)
}
function HighlightedCode({ source }: { source: HighlightedSource }) {
return source.map((line, lineIndex) => (
<React.Fragment key={line.offset}>
<span className="sandbox-code-line">
{line.tokens.map((token) => (
<span
key={token.offset}
className="sandbox-code-token"
style={
{
"--sandbox-token-dark": token.dark,
"--sandbox-token-light": token.light,
} as React.CSSProperties
}
>
{token.content}
</span>
))}
</span>
{lineIndex < source.length - 1 ? "\n" : null}
</React.Fragment>
))
}
function SandboxTab({
active,
children,
onClick,
}: {
active: boolean
children: React.ReactNode
onClick: VoidFunction
}) {
return (
<button
type="button"
className="inline-flex h-7 items-center gap-1.5 rounded-md px-2 text-xs font-medium text-slate-500 transition-colors hover:text-slate-950 aria-pressed:bg-white aria-pressed:text-slate-950 aria-pressed:shadow-sm dark:text-slate-400 dark:hover:text-slate-100 dark:aria-pressed:bg-slate-700 dark:aria-pressed:text-slate-100"
aria-pressed={active}
onClick={onClick}
>
{children}
</button>
)
}
function SandboxAction({
children,
disabled,
label,
onClick,
pressed,
}: {
children: React.ReactNode
disabled?: boolean
label: string
onClick: VoidFunction
pressed?: boolean
}) {
return (
<button
type="button"
className="inline-flex size-8 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-200 hover:text-slate-950 disabled:opacity-40 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
aria-label={label}
aria-pressed={pressed}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
)
}
function SandboxMessage({
children,
className,
tone = "neutral",
}: {
children: React.ReactNode
className?: string
tone?: "error" | "neutral"
}) {
return (
<div
className={`flex min-h-72 items-center justify-center p-6 text-sm ${tone === "error" ? "text-red-600 dark:text-red-400" : "text-slate-500 dark:text-slate-400"} ${className ?? ""}`}
role={tone === "error" ? "alert" : undefined}
>
{children}
</div>
)
}
async function resolveSource(source: SandboxSource) {
if (typeof source === "string") return source
const loaded = await source()
return typeof loaded === "string" ? loaded : loaded.default
}
function toError(value: unknown) {
return value instanceof Error ? value : new Error(String(value))
}
function renderSandboxError(error: Error) {
return <SandboxMessage tone="error">{error.message}</SandboxMessage>
}
+35
View File
@@ -0,0 +1,35 @@
import type { ComponentType } from "react"
import type { DocLocale } from "../../content/types"
export type SandboxLocalizedText = string | Readonly<Record<DocLocale, string>>
export type SandboxPreviewProps = {
locale: DocLocale
}
export type SandboxPreviewModule = {
default: ComponentType<SandboxPreviewProps>
}
export type SandboxSourceModule = {
default: string
}
export type SandboxSource =
| string
| (() => Promise<string | SandboxSourceModule>)
export type SandboxDefinition = {
defaultFile?: string
entry: () => Promise<SandboxPreviewModule>
files: Readonly<Record<string, SandboxSource>>
id: string
title?: SandboxLocalizedText
}
export function defineSandbox<const Definition extends SandboxDefinition>(
definition: Definition
) {
return definition
}
-55
View File
@@ -1,55 +0,0 @@
import { Icon } from "@icones/react"
import { usePreference } from "@workspace/preferences"
import { useResolvedTheme } from "../lib/appearance"
import { useTranslate } from "../lib/locale"
export function ThemeSwitch({ className }: { className?: string }) {
const t = useTranslate()
const [, setThemeMode] = usePreference("theme-mode")
const resolvedTheme = useResolvedTheme()
const dark = resolvedTheme === "dark"
const label = dark
? t({ zh: "切换到浅色主题", us: "Switch to light theme" })
: t({ zh: "切换到深色主题", us: "Switch to dark theme" })
return (
<button
type="button"
role="switch"
aria-checked={dark}
aria-label={label}
title={label}
className={[
"relative inline-flex h-7 w-13 shrink-0 cursor-pointer items-center rounded-full border transition-colors duration-200",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 motion-reduce:transition-none",
dark
? "border-zinc-700 bg-zinc-900 shadow-inner hover:border-zinc-600"
: "border-slate-300 bg-slate-100 shadow-inner hover:border-slate-400",
className,
]
.filter(Boolean)
.join(" ")}
onClick={() => {
setThemeMode(dark ? "light" : "dark")
}}
>
<span
aria-hidden="true"
className={[
"absolute top-px left-px inline-flex size-6 items-center justify-center rounded-full ring-1 shadow-sm transition-transform duration-200 ease-out motion-reduce:transition-none",
dark
? "translate-x-6 bg-black text-zinc-100 ring-zinc-700"
: "translate-x-0 bg-white text-slate-500 ring-slate-200",
].join(" ")}
>
<Icon
name="tabler:sun-high"
altName="tabler:moon"
showAlt={dark}
className="z-10 size-4"
/>
</span>
</button>
)
}
+17
View File
@@ -0,0 +1,17 @@
- key: foundations
order: 10
label:
zh-Hans: 基础能力
en-US: Foundations
- key: application
order: 20
label:
zh-Hans: 应用构建
en-US: Application
- key: content
order: 30
label:
zh-Hans: 内容与数据
en-US: Content and data
+215 -77
View File
@@ -1,3 +1,5 @@
import { lazy } from "react"
import {
defaultDocLocale,
docLocales,
@@ -5,6 +7,7 @@ import {
type DocLocale,
type DocLocation,
type DocPackage,
type DocPackageGroup,
type DocPage,
type DocSection,
type DocTocItem,
@@ -12,87 +15,77 @@ import {
} from "./types"
import { trans, type Translation } from "../lib/locale"
type DocModule = {
type DocContentModule = {
default: React.ComponentType<{ components?: MdxComponents }>
frontmatter: Record<string, unknown>
}
type LocalizedText = Translation<string>
type PackageDefinition = {
group: string
key: string
label: string
description: LocalizedText
description: Record<DocLocale, string>
order: number
}
type ParsedDocPage = DocPage & {
packageKey: string
searchText: string
type PackageGroupDefinition = {
key: string
label: Record<DocLocale, string>
order: number
}
const packageDefinitions: readonly PackageDefinition[] = [
{
key: "blocks",
label: "Blocks",
description: {
zh: "构建应用外壳,并接入导航、通知、聊天、媒体和外观控制。",
us: "Build an application shell and add navigation, notifications, chat, media, and appearance controls.",
},
},
{
key: "i18n",
label: "I18n",
description: {
zh: "接入应用自有词典、本地化运行时、开发工具和完整翻译工作流。",
us: "Add application-owned catalogs, localized runtime rendering, development tools, and translation workflows.",
},
},
{
key: "lexical",
label: "Lexical",
description: {
zh: "从根节点和内容区域开始,逐步构建支持自定义操作与媒体的富文本编辑器。",
us: "Build a rich-text editor progressively from its root and content surface to custom actions and media.",
},
},
{
key: "preferences",
label: "Preferences",
description: {
zh: "使用 SSR 快照、持久化回调和可组合副作用管理类型安全且可扩展的应用偏好。",
us: "Manage typed, extensible application preferences with SSR snapshots, persistence callbacks, and composable effects.",
},
},
{
key: "search",
label: "Search",
description: {
zh: "通过可替换的 Adapter、分组结果、分页和搜索历史构建应用级搜索体验。",
us: "Build application search with a replaceable adapter, grouped results, pagination, and search history.",
},
},
{
key: "ui",
label: "UI",
description: {
zh: "配置共享 UI 层,并学习组合组件、Hooks、图标和本地化词典。",
us: "Set up the shared UI layer and learn how to compose its components, hooks, icons, and locale catalogs.",
},
},
]
type ParsedDocPage = DocPage & { packageKey: string }
const docModules = import.meta.glob<DocModule>(
"../../../packages/*/docs/*/**/*.mdx",
{ eager: true }
const packageGroupModules = import.meta.glob<unknown>("./package-groups.yaml", {
eager: true,
import: "default",
})
const packageGroupDefinitions = Object.values(packageGroupModules)
.flatMap(parsePackageGroupDefinitions)
.toSorted(
(left, right) =>
left.order - right.order || left.key.localeCompare(right.key)
)
const packageGroupsByKey = new Map(
packageGroupDefinitions.map((group) => [group.key, group])
)
const docSources = import.meta.glob<string>(
"../../../packages/*/docs/*/**/*.mdx",
{ eager: true, import: "default", query: "?docs-search-raw" }
const packageMetaModules = import.meta.glob<unknown>(
"../../../packages/*/docs/meta.yaml",
{ eager: true, import: "default" }
)
const parsedPages = Object.entries(docModules).map(([path, module]) =>
parseDocPage(path, module, docSources[path] ?? "")
const packageDefinitions = Object.entries(packageMetaModules)
.map(([path, metadata]) => parsePackageDefinition(path, metadata))
.toSorted((left, right) => {
const leftGroup = getPackageGroupDefinition(left.group)
const rightGroup = getPackageGroupDefinition(right.group)
return (
leftGroup.order - rightGroup.order ||
left.order - right.order ||
left.key.localeCompare(right.key)
)
})
const docModuleLoaders = import.meta.glob<DocContentModule>(
"../../../packages/*/docs/*/**/*.mdx",
{ eager: false }
)
const docFrontmatter = import.meta.glob<Record<string, unknown>>(
"../../../packages/*/docs/*/**/*.mdx",
{ eager: true, import: "frontmatter" }
)
const parsedPages = Object.entries(docFrontmatter).map(
([path, frontmatter]) => {
const load = docModuleLoaders[path]
if (!load) throw new Error(`Missing documentation module for "${path}".`)
return parseDocPage(path, frontmatter, lazy(load))
}
)
const packagesByLocale: Record<DocLocale, readonly DocPackage[]> = {
@@ -104,12 +97,21 @@ const packagesByLocale: Record<DocLocale, readonly DocPackage[]> = {
),
}
const packageGroupsByLocale: Record<DocLocale, readonly DocPackageGroup[]> = {
"zh-Hans": createDocPackageGroups("zh-Hans"),
"en-US": createDocPackageGroups("en-US"),
}
validateLocalizedPagePairs(packagesByLocale)
export function getDocPackages(locale: DocLocale = defaultDocLocale) {
return packagesByLocale[locale]
}
export function getDocPackageGroups(locale: DocLocale = defaultDocLocale) {
return packageGroupsByLocale[locale]
}
export function getDocHref(docPackage: DocPackage, page?: DocPage) {
const prefix = getLocalePrefix(docPackage.locale)
const target = page ?? getOverviewPage(docPackage)
@@ -147,7 +149,7 @@ export function getLocalizedHref(pathname: string, locale: DocLocale) {
}
export function resolveDocLocale(pathname: string): DocLocale {
const firstSegment = pathname.split("/").filter(Boolean)[0]
const firstSegment = pathname.split("/").find(Boolean)
return isDocLocale(firstSegment) ? firstSegment : defaultDocLocale
}
@@ -193,8 +195,17 @@ export function getDocSectionLabel(section: DocSection, locale: DocLocale) {
return trans(locale, labels[section])
}
export function getDocSearchText(page: DocPage) {
return (page as ParsedDocPage).searchText
export function getStaticDocRoutes() {
return docLocales.flatMap((locale) => {
const home = getDocsHomeHref(locale)
return [
home,
`${home === "/" ? "" : home}/settings`,
...getDocPackages(locale).flatMap((docPackage) =>
docPackage.pages.map((page) => getDocHref(docPackage, page))
),
]
})
}
function createDocPackage(
@@ -205,7 +216,7 @@ function createDocPackage(
.filter(
(page) => page.packageKey === definition.key && page.locale === locale
)
.sort(
.toSorted(
(left, right) =>
docSections.indexOf(left.section) -
docSections.indexOf(right.section) ||
@@ -231,18 +242,123 @@ function createDocPackage(
}
return {
group: definition.group,
key: definition.key,
label: definition.label,
description: trans(locale, definition.description),
description: definition.description[locale],
locale,
pages,
}
}
function createDocPackageGroups(locale: DocLocale): readonly DocPackageGroup[] {
const packages = packagesByLocale[locale]
return packageGroupDefinitions.flatMap((definition) => {
const groupPackages = packages.filter(
(docPackage) => docPackage.group === definition.key
)
if (groupPackages.length === 0) return []
return {
key: definition.key,
label: definition.label[locale],
packages: groupPackages,
}
})
}
function parsePackageGroupDefinitions(
value: unknown
): PackageGroupDefinition[] {
if (!Array.isArray(value)) {
throw new Error("Package documentation groups must be an array.")
}
const groups = value.map((entry, index) => {
const path = `package-groups.yaml[${index}]`
if (!isRecord(entry)) {
throw new Error(
`Package documentation group must be an object: "${path}".`
)
}
const label = entry.label
if (!isRecord(label)) {
throw new Error(
`Package documentation group requires a localized "label" object: "${path}".`
)
}
return {
key: readMetaString(entry, "key", path),
label: {
"zh-Hans": readMetaString(label, "zh-Hans", path),
"en-US": readMetaString(label, "en-US", path),
},
order: readMetaNumber(entry, "order", path),
}
})
const duplicate = groups.find(
(group, index) =>
groups.findIndex((item) => item.key === group.key) !== index
)
if (duplicate) {
throw new Error(`Duplicate package documentation group "${duplicate.key}".`)
}
return groups
}
function getPackageGroupDefinition(key: string) {
const group = packageGroupsByKey.get(key)
if (!group) {
throw new Error(`Unknown package documentation group "${key}".`)
}
return group
}
function parsePackageDefinition(
path: string,
value: unknown
): PackageDefinition {
const match = path.match(/\/packages\/([^/]+)\/docs\/meta\.yaml$/)
if (!match) {
throw new Error(
`Invalid package documentation metadata path "${path}". Expected packages/<package>/docs/meta.yaml.`
)
}
if (!isRecord(value)) {
throw new Error(
`Package documentation metadata must be an object: "${path}".`
)
}
const description = value.description
if (!isRecord(description)) {
throw new Error(
`Package documentation metadata requires a localized "description" object: "${path}".`
)
}
return {
group: readMetaString(value, "group", path),
key: match[1]!,
label: readMetaString(value, "label", path),
description: {
"zh-Hans": readMetaString(description, "zh-Hans", path),
"en-US": readMetaString(description, "en-US", path),
},
order: readMetaNumber(value, "order", path),
}
}
function parseDocPage(
path: string,
module: DocModule,
source: string
frontmatter: Record<string, unknown>,
Content: DocPage["Content"]
): ParsedDocPage {
const match = path.match(
/\/packages\/([^/]+)\/docs\/([^/]+)\/(overview|guide|advanced|examples)(?:\/([^/]+))?\.mdx$/
@@ -269,14 +385,12 @@ function parseDocPage(
throw new Error(`Documentation file is in the wrong directory: "${path}".`)
}
const frontmatter = module.frontmatter
return {
Content: module.default,
Content,
description: readString(frontmatter, "description"),
locale: localeValue,
order: readNumber(frontmatter, "order"),
packageKey,
searchText: source,
section: sectionValue,
slug: nestedSlug ?? "overview",
title: readString(frontmatter, "title"),
@@ -331,6 +445,30 @@ function readNumber(frontmatter: Record<string, unknown>, key: string) {
throw new Error(`Documentation frontmatter requires a number "${key}".`)
}
function readMetaString(
metadata: Record<string, unknown>,
key: string,
path: string
) {
const value = metadata[key]
if (typeof value === "string" && value.trim()) return value
throw new Error(
`Package documentation metadata requires a string "${key}": "${path}".`
)
}
function readMetaNumber(
metadata: Record<string, unknown>,
key: string,
path: string
) {
const value = metadata[key]
if (typeof value === "number" && Number.isFinite(value)) return value
throw new Error(
`Package documentation metadata requires a number "${key}": "${path}".`
)
}
function readToc(value: unknown): readonly DocTocItem[] {
if (value === undefined) return []
if (!Array.isArray(value)) {
@@ -351,5 +489,5 @@ function readToc(value: unknown): readonly DocTocItem[] {
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
return typeof value === "object" && value !== null && !Array.isArray(value)
}
+31
View File
@@ -0,0 +1,31 @@
import type { DocLocale, DocSearchEntry } from "./types"
const indexes = new Map<DocLocale, Promise<readonly DocSearchEntry[]>>()
export function loadDocSearchIndex(locale: DocLocale) {
const existing = indexes.get(locale)
if (existing) return existing
const index = (
import.meta.env.DEV
? import("./search-index.source").then((module) =>
module.getDocSearchIndex(locale)
)
: fetch(
`${import.meta.env.BASE_URL}search/${encodeURIComponent(locale)}.json`
).then(async (response) => {
if (!response.ok) {
throw new Error(
`Unable to load the ${locale} documentation search index.`
)
}
return (await response.json()) as readonly DocSearchEntry[]
})
).catch((error: unknown) => {
indexes.delete(locale)
throw error
})
indexes.set(locale, index)
return index
}
+11
View File
@@ -0,0 +1,11 @@
import { createDocSearchIndex } from "./search-index"
import type { DocLocale } from "./types"
const docSources = import.meta.glob<string>(
"../../../packages/*/docs/*/**/*.mdx",
{ eager: true, import: "default", query: "?docs-search-raw" }
)
export function getDocSearchIndex(locale: DocLocale) {
return createDocSearchIndex(locale, docSources)
}
+49
View File
@@ -0,0 +1,49 @@
import { getDocHref, getDocPackages, getDocSectionLabel } from "./registry"
import type { DocLocale, DocSearchEntry, DocTocItem } from "./types"
export function createDocSearchIndex(
locale: DocLocale,
sources: Readonly<Record<string, string>>
): readonly DocSearchEntry[] {
const sourcesByPage = new Map(
Object.entries(sources).map(([path, source]) => [
getSourcePageKey(path),
stripFrontmatter(source),
])
)
return getDocPackages(locale).flatMap((docPackage) =>
docPackage.pages.map((page) => ({
body:
sourcesByPage.get(
`${docPackage.key}/${locale}/${page.section}/${page.slug}`
) ?? "",
description: page.description,
href: getDocHref(docPackage, page),
id: `${page.section}/${page.slug}`,
packageKey: docPackage.key,
packageLabel: docPackage.label,
sectionLabel: getDocSectionLabel(page.section, locale),
title: page.title,
toc: page.toc.flatMap(flattenTocTitles),
}))
)
}
function getSourcePageKey(path: string) {
const match = path.match(
/\/packages\/([^/]+)\/docs\/([^/]+)\/(overview|guide|advanced|examples)(?:\/([^/]+))?\.mdx$/
)
if (!match) throw new Error(`Invalid documentation search source: "${path}".`)
const [, packageKey, locale, section, nestedSlug] = match
return `${packageKey}/${locale}/${section}/${nestedSlug ?? "overview"}`
}
function flattenTocTitles(item: DocTocItem): readonly string[] {
return [item.title, ...(item.items?.flatMap(flattenTocTitles) ?? [])]
}
function stripFrontmatter(source: string) {
return source.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, "")
}
+19
View File
@@ -30,7 +30,20 @@ export type DocPage = {
toc: readonly DocTocItem[]
}
export type DocSearchEntry = {
body: string
description: string
href: string
id: string
packageKey: string
packageLabel: string
sectionLabel: string
title: string
toc: readonly string[]
}
export type DocPackage = {
group: string
key: string
label: string
description: string
@@ -38,6 +51,12 @@ export type DocPackage = {
pages: readonly DocPage[]
}
export type DocPackageGroup = {
key: string
label: string
packages: readonly DocPackage[]
}
export type DocLocation = {
locale: DocLocale
package: DocPackage
+68
View File
@@ -0,0 +1,68 @@
import { StrictMode } from "react"
import { prerender } from "react-dom/static"
import App from "./App"
import {
getStaticDocRoutes,
resolveDocLocale,
resolveDocLocation,
} from "./content/registry"
import { getDocSearchIndex } from "./content/search-index.source"
import type { DocLocale } from "./content/types"
import { createDefaultDocsPreferences } from "./lib/preferences"
export { getStaticDocRoutes, getDocSearchIndex as getStaticSearchIndex }
export async function renderStaticPage(pathname: string) {
const locale = resolveDocLocale(pathname)
const { prelude } = await prerender(
<StrictMode>
<App
initialPreferences={createDefaultDocsPreferences(locale)}
initialUrl={new URL(pathname, "https://docs.local").href}
/>
</StrictMode>
)
return {
html: await new Response(prelude).text(),
locale,
metadata: getPageMetadata(pathname, locale),
}
}
function getPageMetadata(pathname: string, locale: DocLocale) {
const location = resolveDocLocation(pathname)
if (location) {
return {
description: location.page.description,
title: `${location.page.title} · ${location.package.label}`,
}
}
const english = locale === "en-US"
if (/\/settings\/?$/.test(pathname)) {
return {
description: english
? "Configure the documentation theme, colors, and language."
: "配置文档站的主题、颜色和语言。",
title: english ? "Settings · Documentation" : "设置 · 文档",
}
}
if (pathname === "/" || pathname === "/en-US") {
return {
description: english
? "Documentation for the my-shadcn-ui workspace packages."
: "my-shadcn-ui 工作区各个 package 的使用文档。",
title: english ? "my-shadcn-ui Documentation" : "my-shadcn-ui 文档",
}
}
return {
description: english
? "The requested documentation page does not exist."
: "请求的文档页面不存在。",
title: english ? "Page not found · Documentation" : "页面不存在 · 文档",
}
}
+28 -9
View File
@@ -1,6 +1,12 @@
@import "@workspace/ui/globals.css";
@import "@workspace/appearance/globals.css";
@import "@workspace/media/globals.css";
@import "@workspace/overlays/globals.css";
@import "@workspace/page/globals.css";
@import "@workspace/search/globals.css";
@source "../../packages/*/docs/**/*.{mdx,ts,tsx}";
@custom-variant dark (&:where(.dark, .dark *));
@layer base {
@@ -29,8 +35,8 @@
--card-foreground: var(--docs-foreground-light, var(--color-slate-950));
--popover: var(--docs-background-light, var(--color-white));
--popover-foreground: var(--docs-foreground-light, var(--color-slate-950));
--primary: var(--color-indigo-600);
--primary-foreground: var(--color-white);
--accent: var(--color-indigo-600);
--accent-foreground: var(--color-white);
--secondary: var(--docs-surface-light, var(--color-slate-100));
--secondary-foreground: var(
--docs-foreground-light,
@@ -38,8 +44,11 @@
);
--muted: var(--docs-surface-light, var(--color-slate-100));
--muted-foreground: var(--docs-muted-light, var(--color-slate-600));
--accent: var(--docs-surface-light, var(--color-slate-100));
--accent-foreground: var(--docs-foreground-light, var(--color-slate-950));
--highlight: var(--docs-surface-light, var(--color-slate-100));
--highlight-foreground: var(
--docs-foreground-light,
var(--color-slate-950)
);
--border: var(--color-slate-200);
--input: var(--color-slate-200);
--ring: var(--color-indigo-500);
@@ -55,14 +64,14 @@
--card-foreground: var(--docs-foreground-dark, var(--color-slate-100));
--popover: var(--docs-surface-dark, var(--color-slate-900));
--popover-foreground: var(--docs-foreground-dark, var(--color-slate-100));
--primary: var(--color-indigo-400);
--primary-foreground: var(--color-slate-950);
--accent: var(--color-indigo-400);
--accent-foreground: var(--color-slate-950);
--secondary: var(--color-slate-800);
--secondary-foreground: var(--docs-foreground-dark, var(--color-slate-100));
--muted: var(--color-slate-800);
--muted-foreground: var(--docs-muted-dark, var(--color-slate-400));
--accent: var(--color-slate-800);
--accent-foreground: var(--docs-foreground-dark, var(--color-slate-100));
--highlight: var(--color-slate-800);
--highlight-foreground: var(--docs-foreground-dark, var(--color-slate-100));
--border: var(--color-slate-800);
--input: var(--color-slate-700);
--ring: var(--color-indigo-400);
@@ -73,7 +82,7 @@
body,
#root {
@apply min-h-dvh;
@apply min-h-dvh min-w-80;
}
body {
@@ -84,3 +93,13 @@
@apply bg-indigo-200 text-indigo-950 dark:bg-indigo-500/40 dark:text-indigo-50;
}
}
@layer components {
.sandbox-code-token {
color: var(--sandbox-token-light);
}
.dark .sandbox-code-token {
color: var(--sandbox-token-dark);
}
}
+3 -14
View File
@@ -1,4 +1,4 @@
import { useCallback, useSyncExternalStore } from "react"
import { useCallback } from "react"
import { usePreference } from "@workspace/preferences"
import colors from "tailwindcss/colors"
@@ -37,7 +37,7 @@ export type ThemeMode = (typeof themeModes)[number]
export type AccentColor = (typeof accentColors)[number]
export type BackgroundShade = (typeof backgroundShades)[number]
export type ForegroundShade = (typeof foregroundShades)[number]
export type ResolvedTheme = Exclude<ThemeMode, "system">
type ResolvedTheme = Exclude<ThemeMode, "system">
export type Appearance = {
accentColor: AccentColor
@@ -91,17 +91,6 @@ export function useAppearance(): AppearanceController {
}
}
export function useResolvedTheme() {
const [themeMode] = usePreference("theme-mode")
const systemTheme = useSyncExternalStore(
subscribeToSystemTheme,
getSystemTheme,
() => "light" as const
)
return themeMode === "system" ? systemTheme : themeMode
}
export function getPaletteColor(
color: AccentColor | BackgroundShade | ForegroundShade,
step: ColorStep = 500
@@ -146,7 +135,7 @@ export function subscribeToSystemTheme(listener: VoidFunction) {
return () => mediaQuery.removeEventListener("change", listener)
}
export function getSystemTheme(): ResolvedTheme {
function getSystemTheme(): ResolvedTheme {
return window.matchMedia(systemThemeQuery).matches ? "dark" : "light"
}
+13
View File
@@ -105,6 +105,19 @@ export function readDocsPreferences(
return preferences
}
export function createDefaultDocsPreferences(
locale: DocLocale = defaultDocLocale
): Preferences {
return Object.fromEntries(
(
Object.keys(docsPreferenceDefinitions) as Array<keyof DocsPreferences>
).map((key) => [
key,
key === "locale" ? locale : docsPreferenceDefinitions[key].defaultValue,
])
) as DocsPreferences
}
export function persistDocsPreference(update: PreferenceUpdate) {
const definition = docsPreferenceDefinitions[update.key]
const serializedValue =
+21
View File
@@ -0,0 +1,21 @@
import { createContext, useContext, type ReactNode } from "react"
const PathnameContext = createContext("/")
export function PathnameProvider({
children,
pathname,
}: {
children: ReactNode
pathname: string
}) {
return (
<PathnameContext.Provider value={pathname}>
{children}
</PathnameContext.Provider>
)
}
export function usePathname() {
return useContext(PathnameContext)
}
-19
View File
@@ -1,19 +0,0 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}
+24 -7
View File
@@ -1,18 +1,35 @@
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { createRoot, hydrateRoot } from "react-dom/client"
import App from "./App.tsx"
// oxlint-disable-next-line import/no-unassigned-import -- The entry point installs the global stylesheet.
import "./index.css"
import { initializeDocsPreferences } from "./lib/preferences"
import { resolveDocLocale } from "./content/registry"
import {
createDefaultDocsPreferences,
initializeDocsPreferences,
} from "./lib/preferences"
document.documentElement.lang = window.location.pathname.startsWith("/en-US")
? "en-US"
: "zh-Hans"
const root = document.getElementById("root")!
const prerendered = root.hasChildNodes()
const locale = resolveDocLocale(window.location.pathname)
document.documentElement.lang = locale
initializeDocsPreferences()
createRoot(document.getElementById("root")!).render(
const app = (
<StrictMode>
<App />
<App
initialPreferences={
prerendered ? createDefaultDocsPreferences(locale) : undefined
}
initialUrl={window.location.href}
/>
</StrictMode>
)
if (prerendered) {
hydrateRoot(root, app)
} else {
createRoot(root).render(app)
}
+13 -32
View File
@@ -5,23 +5,18 @@ import {
getDocHref,
getDocPackages,
getDocSectionLabel,
getDocsHomeHref,
resolveDocLocation,
} from "../content/registry"
import type {
DocLocation,
DocPage,
DocTocItem,
} from "../content/types"
import { Actions, Container, HomeButton, SlashLine } from "../components/layout"
import type { DocLocation, DocPage, DocTocItem } from "../content/types"
import { Actions, SiteFooter, SiteHeader } from "../components/layout"
import { mdxComponents } from "../components/mdx"
import { useLocale, useTranslate } from "../lib/locale"
const noTocItems: readonly DocTocItem[] = []
export default function ArticlePage() {
export default function ArticlePage({ pathname }: { pathname: string }) {
const t = useTranslate()
const docLocation = resolveDocLocation(window.location.pathname)
const docLocation = resolveDocLocation(pathname)
const visibleTocIds = useVisibleTocIds(docLocation?.page.toc ?? noTocItems)
const [mobilePanel, setMobilePanel] = React.useState<
"navigation" | "toc" | null
@@ -117,17 +112,12 @@ export default function ArticlePage() {
return (
<>
<header
<SiteHeader
data-article-sticky-header
className="z-10 h-16 border-b bg-white/70 backdrop-blur-sm md:sticky md:top-0 dark:border-slate-800 dark:bg-slate-950/80"
>
<Container className="flex size-full items-center gap-4">
<HomeButton href={getDocsHomeHref(docLocation.locale)} />
<SlashLine />
<h1 className="truncate">{docLocation.package.label}</h1>
<Actions className="ml-auto" />
</Container>
</header>
subleading={<h1 className="truncate">{docLocation.package.label}</h1>}
actions={<Actions />}
/>
<section
data-article-sticky-header
@@ -224,7 +214,7 @@ export default function ArticlePage() {
<div className="flex w-full">
<div
data-slot="left"
className="sticky top-16 hidden h-[calc(100svh-(--spacing(16)))] min-w-(--min-lw) flex-[6_1_var(--min-lw)] overflow-y-auto border-r bg-slate-50 md:block dark:border-slate-800 dark:bg-slate-900/50"
className="sticky top-16 hidden h-[calc(100svh-(--spacing(16)))] min-w-(--min-lw) flex-[6_1_var(--min-lw)] overflow-y-auto border-r md:block dark:border-slate-800"
>
<aside className="ml-auto w-full max-w-(--max-lw) p-8">
<Sidebar current={docLocation} />
@@ -252,11 +242,7 @@ export default function ArticlePage() {
</div>
</div>
</div>
<footer className="border-t py-8 text-slate-500 dark:border-slate-800 dark:text-slate-400">
<Container className="flex h-full w-full items-center">
Copyright © 2026
</Container>
</footer>
<SiteFooter className="border-t py-8 text-slate-500 dark:border-slate-800 dark:text-slate-400" />
</>
)
}
@@ -401,8 +387,6 @@ function TableOfContents({
height: number
top: number
} | null>(null)
const activeKey = activeIds.join("\n")
React.useLayoutEffect(() => {
const root = rootRef.current
const rail = railRef.current
@@ -437,7 +421,7 @@ function TableOfContents({
resizeObserver.disconnect()
window.removeEventListener("resize", measureMarker)
}
}, [activeKey, activeIds])
}, [activeIds])
return (
<aside
@@ -514,7 +498,7 @@ function TocAnchor({
<a
href={href}
data-active={active || undefined}
className="text-slate-600 transition-colors hover:text-indigo-500 data-active:font-medium data-active:text-indigo-600 dark:text-slate-400 dark:hover:text-indigo-300 dark:data-active:text-indigo-400"
className="text-slate-600 transition-colors hover:text-indigo-500 dark:text-slate-400 dark:hover:text-indigo-300 data-active:font-medium data-active:text-indigo-600 dark:data-active:text-indigo-400"
>
{title}
</a>
@@ -532,10 +516,7 @@ function useVisibleTocIds(items: readonly DocTocItem[]) {
const [activeIds, setActiveIds] = React.useState<readonly string[]>([])
React.useEffect(() => {
if (entries.length === 0) {
setActiveIds([])
return
}
if (entries.length === 0) return
let animationFrame: number | undefined
const articleElement = document.querySelector<HTMLElement>(
+53 -31
View File
@@ -1,49 +1,71 @@
import { Actions, Container } from "../components/layout"
import { getDocHref, getDocPackages } from "../content/registry"
import {
Actions,
Container,
SiteFooter,
SiteHeader,
} from "../components/layout"
import { getDocHref, getDocPackageGroups } from "../content/registry"
import { useLocale, useTranslate } from "../lib/locale"
export default function IndexPage() {
const locale = useLocale()
const t = useTranslate()
const docPackages = getDocPackages(locale)
const packageGroups = getDocPackageGroups(locale)
const packageNumbers = new Map(
packageGroups
.flatMap((group) => group.packages)
.map((docPackage, index) => [docPackage.key, index + 1])
)
return (
<>
<Container className="flex items-center justify-between gap-4 pt-8 pb-10 sm:pt-12 sm:pb-14 lg:pb-16">
<h1 className="min-w-0 text-2xl font-semibold capitalize sm:text-3xl">
my-shadcn-ui
</h1>
<Actions className="ml-auto" />
</Container>
<SiteHeader
className="pt-8 pb-10 sm:pt-12 sm:pb-14 lg:pb-16"
actions={<Actions />}
/>
<Container
className="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 lg:gap-8"
className="space-y-12 lg:space-y-16"
aria-label={t({ zh: "文档导航", us: "Documentation" })}
>
{docPackages.map((docPackage, index) => (
<a
key={docPackage.key}
href={getDocHref(docPackage)}
className="block overflow-hidden rounded-xl border border-slate-200 bg-indigo-50 outline-0 outline-offset-2 transition-colors hover:border-indigo-500 hover:bg-indigo-50 focus-visible:outline-2 focus-visible:outline-indigo-500 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-indigo-400 dark:hover:bg-slate-800/80 dark:focus-visible:outline-indigo-400"
{packageGroups.map((group) => (
<section
key={group.key}
aria-labelledby={`package-group-${group.key}`}
>
<div className="min-h-fit p-4 sm:min-h-40 lg:min-h-46">
<div className="text-slate-400 dark:text-slate-500">
{index > 9 ? `${index + 1}` : `0${index + 1}`}
</div>
<h2 className="mt-2 mb-4 text-xl font-semibold text-slate-950 dark:text-slate-100">
{docPackage.label}
</h2>
<p className="text-slate-700 dark:text-slate-300">
{docPackage.description}
</p>
<h2
id={`package-group-${group.key}`}
className="mb-5 text-xl font-semibold text-slate-950 sm:mb-6 sm:text-2xl dark:text-slate-100"
>
{group.label}
</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 lg:gap-8">
{group.packages.map((docPackage) => (
<a
key={docPackage.key}
href={getDocHref(docPackage)}
className="block overflow-hidden rounded-xl border border-slate-200 bg-indigo-50 outline-0 outline-offset-2 transition-colors hover:border-indigo-500 hover:bg-indigo-50 focus-visible:outline-2 focus-visible:outline-indigo-500 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-indigo-400 dark:hover:bg-slate-800/80 dark:focus-visible:outline-indigo-400"
>
<div className="min-h-fit p-4 sm:min-h-40 lg:min-h-46">
<div className="text-slate-400 dark:text-slate-500">
{String(packageNumbers.get(docPackage.key)).padStart(
2,
"0"
)}
</div>
<h3 className="mt-2 mb-4 text-xl font-semibold text-slate-950 dark:text-slate-100">
{docPackage.label}
</h3>
<p className="text-slate-700 dark:text-slate-300">
{docPackage.description}
</p>
</div>
</a>
))}
</div>
</a>
</section>
))}
</Container>
<footer className="py-8 sm:py-12">
<Container className="text-center text-slate-500 dark:text-slate-400">
<div>Copyright © 2026</div>
</Container>
</footer>
<SiteFooter className="sm:py-12" />
</>
)
}
+17 -13
View File
@@ -1,7 +1,11 @@
import { Icon } from "@icones/react"
import { usePreference } from "@workspace/preferences"
import { BackButton, Container, SlashLine } from "../components/layout"
import {
Container,
SiteFooter,
SiteHeader,
} from "../components/layout"
import { LocaleFlag } from "../components/locale-flag"
import { SearchTrigger } from "../components/search-trigger"
import { Swatch } from "../components/swatch"
@@ -21,6 +25,7 @@ import {
import { useLocale, useTranslate, type Translation } from "../lib/locale"
import { navigateToLocale } from "../lib/locale-navigation"
import { detectLocale } from "../lib/preferences"
import { usePathname } from "../lib/router"
const foregroundOptions: ReadonlyArray<{
label: Translation
@@ -54,6 +59,7 @@ const messages = {
export default function SettingsPage() {
const locale = useLocale()
const pathname = usePathname()
const t = useTranslate()
const appearance = useAppearance()
const [localeMode, setLocaleMode] = usePreference("locale-mode")
@@ -78,19 +84,16 @@ export default function SettingsPage() {
return (
<>
<header className="border-b bg-white/70 backdrop-blur-sm dark:border-slate-800 dark:bg-slate-950/80">
<Container className="flex h-16 items-center gap-4">
<BackButton />
<SlashLine />
<SiteHeader
subleading={
<h1 className="text-xl font-semibold">{t(messages.settings)}</h1>
<div className="ml-auto">
<SearchTrigger />
</div>
</Container>
</header>
}
className="h-16 border-b bg-white/70 backdrop-blur-sm dark:border-slate-800 dark:bg-slate-950/80"
actions={<SearchTrigger />}
/>
<main>
<Container className="py-8 sm:py-12">
<Container className="pt-8 sm:pt-12">
<section aria-labelledby="appearance-heading">
<h2 id="appearance-heading" className="mb-4 text-xl font-semibold">
{t(messages.appearance)}
@@ -231,7 +234,7 @@ export default function SettingsPage() {
<div className="w-full space-y-1 rounded-xl border p-1 sm:w-fit sm:min-w-48 dark:border-slate-700">
<LocaleLink
disabled={autoDetectLocale}
href={getLocalizedHref(window.location.pathname, "zh-Hans")}
href={getLocalizedHref(pathname, "zh-Hans")}
label="简体中文"
locale="zh-Hans"
selected={locale === "zh-Hans"}
@@ -239,7 +242,7 @@ export default function SettingsPage() {
/>
<LocaleLink
disabled={autoDetectLocale}
href={getLocalizedHref(window.location.pathname, "en-US")}
href={getLocalizedHref(pathname, "en-US")}
label="English"
locale="en-US"
selected={locale === "en-US"}
@@ -269,6 +272,7 @@ export default function SettingsPage() {
</section>
</Container>
</main>
<SiteFooter />
</>
)
}
+6 -2
View File
@@ -22,5 +22,9 @@
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
"include": [
"src",
"../packages/*/docs/sandboxes/**/*.ts",
"../packages/*/docs/sandboxes/**/*.tsx"
]
}
+2 -2
View File
@@ -19,5 +19,5 @@
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
"include": ["vite.config.ts", "scripts"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"types": []
},
"files": ["src/components/sandbox/sandbox-env.d.ts"],
"include": []
}
+44 -3
View File
@@ -3,15 +3,17 @@ import babel from "@rolldown/plugin-babel"
import tailwindcss from "@tailwindcss/vite"
import { icones } from "@icones/vite"
import { readFile } from "node:fs/promises"
import expressiveCode from "satteri-expressive-code"
import { fileURLToPath } from "node:url"
import { defineConfig, type Plugin } from "vite"
import satteri from "vite-plugin-satteri"
import { satteri } from "vite-plugin-satteri"
import { parse } from "yaml"
const packagesDirectory = fileURLToPath(new URL("../packages", import.meta.url))
/**
* Vite 默认监听 docs 根目录。现有的外部 MDX 会作为模块被监听,但新建 package
* 或新建 MDX 文件在首次进入模块图之前不会触发 import.meta.glob 重新计算。
* Vite 默认监听 docs 根目录。现有的外部文档与元数据会作为模块被监听,但新建
* package、meta.yaml 或 MDX 文件在首次进入模块图之前不会触发 glob 重新计算。
*/
function watchPackageDocs(): Plugin {
return {
@@ -22,6 +24,27 @@ function watchPackageDocs(): Plugin {
}
}
function loadYamlModules(): Plugin {
return {
name: "load-yaml-modules",
enforce: "pre",
transform(source, id) {
if (!/\.ya?ml(?:\?|$)/.test(id)) return
try {
return {
code: `export default ${JSON.stringify(parse(source))}`,
map: null,
}
} catch (error) {
this.error(
`Unable to parse ${id}: ${error instanceof Error ? error.message : String(error)}`
)
}
},
}
}
function loadDocSearchSources(): Plugin {
const suffix = "?docs-search-raw"
const virtualPrefix = "\0docs-search-raw:"
@@ -59,6 +82,7 @@ function loadDocSearchSources(): Plugin {
export default defineConfig({
plugins: [
watchPackageDocs(),
loadYamlModules(),
loadDocSearchSources(),
satteri({
features: {
@@ -69,6 +93,23 @@ export default defineConfig({
mdx: {
jsxImportSource: "react",
},
hastPlugins: [
expressiveCode({
frames: false,
textMarkers: false,
themes: ["github-light", "github-dark"],
useDarkModeMediaQuery: false,
themeCssSelector: (theme) =>
theme.type === "dark" ? ".dark" : ":root:not(.dark)",
styleOverrides: {
borderRadius: "0.75rem",
borderWidth: "1px",
codeFontSize: "0.875rem",
codeLineHeight: "1.5rem",
codePaddingInline: "1rem",
},
}),
],
}),
tailwindcss(),
icones({
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "react-app-kit-docs",
"compatibility_date": "2026-09-20",
"workers_dev": false,
"routes": [
{
"pattern": "react-app-kit.go-slim.dev",
"custom_domain": true
}
],
"assets": {
"directory": "./dist",
"not_found_handling": "404-page",
"html_handling": "auto-trailing-slash"
}
}
@@ -0,0 +1,86 @@
---
title: Atomic composition
description: Build a custom appearance surface from controlled components without the preset dialog.
order: 1
toc:
- id: controlled-atoms
title: Controlled atoms
- id: compose-card
title: Compose a theme card
- id: connected-layer
title: State bindings
- id: live-preview
title: Live preview
---
## Controlled atoms {#controlled-atoms}
`ThemeModeField`, `ThemeModeOptionList`, `ColorVisionModeField`, `TextScaleField`, `TextScaleOptionList`, `MotionModeField`, `BrightnessField`, `ThemeColorPicker`, and `ThemeToggle` are driven by values and callbacks. They do not require preference context, so they can work with forms, preview tools, or another state library.
```tsx
<ThemeModeField value={mode} onValueChange={setMode} />
<ThemeModeOptionList value={mode} onValueChange={setMode} />
<ColorVisionModeField value={colorVisionMode} onValueChange={setColorVisionMode} />
<TextScaleField value={textScale} onValueChange={setTextScale} />
<MotionModeField value={motionMode} onValueChange={setMotionMode} />
<BrightnessField value={brightness} onValueChange={setBrightness} />
<TextScaleOptionList value={textScale} onValueChange={setTextScale} />
```
`AppearanceOptionList` is the reusable radio list behind `TextScaleOptionList`. Give its `items` a `value`, `label`, and optional `icon` and `description` to build theme, motion, color-vision, or brightness selectors with the same presentation. It uses native radio inputs for keyboard navigation and form semantics.
## Compose a theme card {#compose-card}
`ThemeConfigCard` combines base and accent color controls while keeping the configuration owned by the caller.
```tsx
<ThemeConfigCard
config={{ baseColor, accentColor }}
onConfigChange={setThemeColors}
scheme={resolvedTheme}
/>
```
For a more granular layout, compose `baseColorSwatches`, `accentColorSwatches`, and `ThemeColorSwatchButton` directly.
`ThemeColorPicker` uses the labeled `pill` style by default. For tighter layouts, switch to the color-only `swatch` style with its selected indicator:
```tsx
<ThemeColorPicker
title="Accent color"
swatches={accentColorSwatches}
value={accentColor}
variant="swatch"
onValueChange={setAccentColor}
/>
```
## State bindings {#connected-layer}
`AppearanceSettings` is the default binding layer between the atoms and the shared `PreferencesProvider`. A custom surface can use Appearance hooks such as `useThemeMode`, `useThemeConfig`, and `useResolvedTheme` without knowing shared-preference string keys or reusing the default layout.
```tsx
const [themeConfig, setThemeConfig] = useThemeConfig()
const resolvedTheme = useResolvedTheme()
<ThemeConfigCard
config={themeConfig}
onConfigChange={setThemeConfig}
scheme={resolvedTheme}
/>
```
## Live preview {#live-preview}
The documentation Sandbox can load a typed, multi-file example without embedding implementation details in MDX:
<Sandbox example="appearance/theme-colors" title="Theme color controls" />
For a single file, import the component and its raw source directly instead of creating a manifest:
```mdx
import Demo from "./Demo"
import source from "./Demo.tsx?raw"
<SingleFileSandbox component={Demo} filename="Demo.tsx" source={source} />
```
@@ -0,0 +1,49 @@
---
title: Appearance settings dialog
description: Compose a trigger, controller, and hotkeys into a complete appearance settings flow.
order: 1
toc:
- id: controller
title: Mount the controller
- id: trigger
title: Add a trigger
- id: custom-handle
title: Independent dialog instance
---
## Mount the controller {#controller}
Mount one `AppearanceController` inside the application's shared `PreferencesProvider`. It composes the default settings content and registers `Mod+,` to open the dialog and `Mod+D` to toggle the theme.
```tsx
<PreferencesProvider
effects={[appearancePreferenceEffect]}
initialPreferences={preferences}
>
<AppearanceController />
<App />
</PreferencesProvider>
```
## Add a trigger {#trigger}
Place `AppearanceDialogTrigger` in a user menu, settings button, or command palette. By default it connects to the handle used by the default controller.
```tsx
<AppearanceDialogTrigger render={<button type="button" />}>
Appearance
</AppearanceDialogTrigger>
```
## Independent dialog instance {#custom-handle}
When a page needs an isolated appearance dialog, pass the same new handle to its trigger and controller.
```tsx
const handle = createAppearanceDialogHandle()
<AppearanceDialogTrigger handle={handle} render={<button type="button" />}>
Open theme settings
</AppearanceDialogTrigger>
<AppearanceController handle={handle} />
```
@@ -0,0 +1,104 @@
---
title: Set up appearance preferences
description: Aggregate appearance definitions and effects with the application's shared preferences.
order: 1
toc:
- id: provider
title: Aggregate preferences
- id: effects
title: Register effects
- id: theme-style
title: Inject theme variables
- id: persistence
title: Persist updates
---
## Aggregate preferences {#provider}
Build one complete preference definition map at the application boundary. Spread definitions from appearance, locale, keyboard, and other feature packages into this map, then create or load the initial values.
Import Appearance after UI in the application stylesheet. No UI component changes are required:
```css
@import "@workspace/ui/globals.css";
@import "@workspace/appearance/globals.css";
```
```tsx
import {
createDefaultPreferences,
PreferencesProvider,
} from "@workspace/preferences"
import {
appearancePreferenceDefinitions,
appearancePreferenceEffect,
} from "@workspace/appearance"
const preferenceDefinitions = {
...appearancePreferenceDefinitions,
...localePreferenceDefinitions,
}
const initialPreferences = createDefaultPreferences(preferenceDefinitions)
const preferenceEffects = [
appearancePreferenceEffect,
localePreferenceEffect,
] as const
export function Root({ children }: { children: React.ReactNode }) {
return (
<PreferencesProvider
effects={preferenceEffects}
initialPreferences={initialPreferences}
>
{children}
</PreferencesProvider>
)
}
```
`defaultAppearancePreferences` remains available when only the appearance slice is needed, but the definition map is the preferred source when several packages are combined.
## Register effects {#effects}
`appearancePreferenceEffect` updates document classes, root attributes, and theme variables before paint, then watches operating-system theme and motion preferences after paint. Register it in the stable `effects` array passed to `PreferencesProvider`.
## Inject theme variables {#theme-style}
Use `buildThemeStyleSheet` to generate light and dark CSS variables from one base color, one accent color, and a color-vision mode. Keep the stable element ID so the appearance effect can update the same node when preferences change.
```tsx
import {
buildThemeStyleSheet,
THEME_STYLE_ELEMENT_ID,
} from "@workspace/appearance"
;<style
id={THEME_STYLE_ELEMENT_ID}
dangerouslySetInnerHTML={{
__html: buildThemeStyleSheet(
{ baseColor, accentColor },
colorVisionMode,
brightness
),
}}
/>
```
Assistance modes adjust chart and destructive semantic colors and set `data-ui-color-vision`. They are not color-vision simulators and do not transform photos, video, or brand assets.
Text scaling changes the root font size through `data-ui-text-scale`; reduced motion shortens global animation through `data-ui-motion` while preserving low-motion loading feedback; brightness adjusts theme surface variables without using `filter: brightness()`.
## Persist updates {#persistence}
The shared `onPreferenceChange` receives a typed `{ key, value }` only when a value actually changes. Forward it to a server function or cookie writer owned by the application.
```tsx
<PreferencesProvider
effects={preferenceEffects}
initialPreferences={preferences}
onPreferenceChange={(update) => savePreference(update)}
>
{children}
</PreferencesProvider>
```
@@ -0,0 +1,90 @@
---
title: Overview
description: Understand the Appearance package atoms, theme data, and preference boundary.
order: 1
toc:
- id: package-role
title: Package role
- id: atomic-components
title: Atomic components
- id: preferences
title: Preference integration
- id: hooks
title: Hooks
---
## Package role {#package-role}
`@workspace/appearance` owns theme mode, base color, accent color, color-vision assistance, text scale, motion, and interface brightness. It does not create its own provider or store, and it does not require changes to `@workspace/ui`. The application aggregates it with other preference packages through `@workspace/preferences`.
## Atomic components {#atomic-components}
The controlled components can be composed independently without preference context:
- `ThemeModeField` selects light, dark, or system mode.
- `ColorVisionModeField` selects the standard, protan, deutan, or tritan assistance palette.
- `TextScaleField` scales the interface to 100%, 125%, 150%, or 200%.
- `MotionModeField` follows the system, reduces motion, or enables full motion.
- `BrightnessField` selects dim, normal, or bright interface surfaces.
- `ThemeColorPicker` and `ThemeColorSwatchButton` select theme colors.
- `ThemeConfigCard` combines the base color and accent color controls.
- `ThemeToggle` switches between light and dark themes.
`AppearanceSettings`, `AppearanceDialog`, and `AppearanceController` are optional composition layers built from these atoms.
## Preference integration {#preferences}
```tsx
import {
createDefaultPreferences,
PreferencesProvider,
} from "@workspace/preferences"
import {
AppearanceController,
appearancePreferenceDefinitions,
appearancePreferenceEffect,
} from "@workspace/appearance"
const preferenceDefinitions = {
...appearancePreferenceDefinitions,
}
const initialPreferences = createDefaultPreferences(preferenceDefinitions)
const preferenceEffects = [appearancePreferenceEffect] as const
export function App() {
return (
<PreferencesProvider
effects={preferenceEffects}
initialPreferences={initialPreferences}
>
<AppearanceController />
</PreferencesProvider>
)
}
```
Add definitions and effects from locale, keyboard, or other packages to the same objects. The application can persist every `{ key, value }` update through the shared `onPreferenceChange` and parse cookies on the server with the aggregated definitions.
Color-vision assistance does not filter the complete document or its images. It replaces semantic colors such as `destructive` and `chart-*`, and writes `data-ui-color-vision` to `<html>` so application components can add icons, labels, shapes, or patterns. Important information must never rely on color alone.
Text, motion, and brightness are also implemented entirely through root attributes and CSS variables. Once the Appearance stylesheet is imported, existing UI components, portals, and application content inherit these settings.
## Hooks {#hooks}
Applications do not need to know the string keys that Appearance uses in the shared store. The package provides a semantic hook for every preference: `useThemeMode`, `useAccentColor`, `useBaseColor`, `useColorVisionMode`, `useTextScale`, `useMotionMode`, and `useBrightness`.
Composition and resolution hooks support higher-level surfaces:
- `useThemeConfig` reads and updates base and accent colors as one configuration.
- `useSystemTheme` and `useSystemMotion` only read operating-system settings.
- `useResolvedTheme` and `useResolvedMotion` return the final value after resolving `system`.
- `useToggleTheme` returns a callback that switches between explicit light and dark modes.
- `useAppearancePreference` is the typed low-level entry point, while `useAppearancePreferences` returns the complete Appearance snapshot.
```tsx
const [themeMode, setThemeMode] = useThemeMode()
const [themeConfig, setThemeConfig] = useThemeConfig()
const resolvedTheme = useResolvedTheme()
const resolvedMotion = useResolvedMotion()
const toggleTheme = useToggleTheme()
```
+6
View File
@@ -0,0 +1,6 @@
group: foundations
order: 20
label: Appearance
description:
zh-Hans: 配置主题、颜色、文字缩放、动态效果与界面亮度。
en-US: Configure themes, colors, text scaling, motion, and interface brightness.
@@ -0,0 +1,30 @@
import {
ThemeColorPicker,
accentColorSwatches,
type ThemeColorPickerVariant,
} from "@workspace/appearance/components/theme-color-picker"
import type { ThemeColorConfig } from "@workspace/appearance/theme"
export function AccentPicker({
config,
onConfigChange,
title,
variant,
}: {
config: ThemeColorConfig
onConfigChange: (config: ThemeColorConfig) => void
title: string
variant?: ThemeColorPickerVariant
}) {
return (
<ThemeColorPicker
title={title}
swatches={accentColorSwatches}
value={config.accentColor}
variant={variant}
onValueChange={(accentColor) =>
onConfigChange({ ...config, accentColor })
}
/>
)
}
@@ -0,0 +1,43 @@
import React from "react"
import { I18nProvider } from "@workspace/i18n"
import { messages as englishMessages } from "@workspace/appearance/locales/en-US"
import { messages as chineseMessages } from "@workspace/appearance/locales/zh-Hans"
import type { ThemeColorConfig } from "@workspace/appearance/theme"
import { AccentPicker } from "./AccentPicker"
const catalogs = {
"en-US": englishMessages,
"zh-Hans": chineseMessages,
} as const
export default function ThemeColorsExample({
locale,
}: {
locale: "en-US" | "zh-Hans"
}) {
const [config, setConfig] = React.useState<ThemeColorConfig>({
accentColor: "indigo",
baseColor: "neutral",
})
const title = locale === "zh-Hans" ? "强调色" : "Accent color"
return (
<I18nProvider catalogs={catalogs} locale={locale}>
<div className="mx-auto flex max-w-xl flex-col gap-6">
<AccentPicker
config={config}
title={title}
onConfigChange={setConfig}
/>
<AccentPicker
config={config}
title={title}
variant="swatch"
onConfigChange={setConfig}
/>
</div>
</I18nProvider>
)
}
@@ -0,0 +1,13 @@
export default {
id: "appearance/theme-colors",
title: {
"en-US": "Controlled theme colors",
"zh-Hans": "受控主题颜色",
},
entry: () => import("./App"),
files: {
"App.tsx": () => import("./App.tsx?raw"),
"AccentPicker.tsx": () => import("./AccentPicker.tsx?raw"),
},
defaultFile: "App.tsx",
} satisfies import("../../../../../docs/src/components/sandbox/types").SandboxDefinition
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../../docs/tsconfig.sandbox.json",
"compilerOptions": {
"tsBuildInfoFile": "../../../docs/node_modules/.tmp/appearance-docs.tsbuildinfo"
},
"include": ["sandboxes/**/*.ts", "sandboxes/**/*.tsx"]
}
@@ -0,0 +1,86 @@
---
title: 原子化组合
description: 不依赖预设弹窗,使用受控组件构建自己的外观界面。
order: 1
toc:
- id: controlled-atoms
title: 受控原子组件
- id: compose-card
title: 组合主题卡片
- id: connected-layer
title: 状态连接层
- id: live-preview
title: 在线预览
---
## 受控原子组件 {#controlled-atoms}
`ThemeModeField`、`ThemeModeOptionList`、`ColorVisionModeField`、`TextScaleField`、`TextScaleOptionList`、`MotionModeField`、`BrightnessField`、`ThemeColorPicker` 和 `ThemeToggle` 都由 `value` 与回调驱动。它们不要求使用偏好上下文,因此可以接入表单、预览工具或其他状态库。
```tsx
<ThemeModeField value={mode} onValueChange={setMode} />
<ThemeModeOptionList value={mode} onValueChange={setMode} />
<ColorVisionModeField value={colorVisionMode} onValueChange={setColorVisionMode} />
<TextScaleField value={textScale} onValueChange={setTextScale} />
<MotionModeField value={motionMode} onValueChange={setMotionMode} />
<BrightnessField value={brightness} onValueChange={setBrightness} />
<TextScaleOptionList value={textScale} onValueChange={setTextScale} />
```
`AppearanceOptionList` 提供 `TextScaleOptionList` 使用的通用单选列表。向 `items` 传入 `value`、`label`、可选 `icon` 与 `description`,即可用相同风格实现主题、动态效果、色彩辨识和亮度选择器。组件使用原生 radio,自动支持键盘导航与表单语义。
## 组合主题卡片 {#compose-card}
`ThemeConfigCard` 把基础色和强调色选择组合在一起,但仍由调用方持有配置。
```tsx
<ThemeConfigCard
config={{ baseColor, accentColor }}
onConfigChange={setThemeColors}
scheme={resolvedTheme}
/>
```
需要更细粒度的布局时,可以分别使用 `baseColorSwatches`、`accentColorSwatches` 和 `ThemeColorSwatchButton`。
`ThemeColorPicker` 默认使用带文字的 `pill` 样式;空间紧凑时可切换为仅显示颜色和选中标记的 `swatch` 样式:
```tsx
<ThemeColorPicker
title="强调色"
swatches={accentColorSwatches}
value={accentColor}
variant="swatch"
onValueChange={setAccentColor}
/>
```
## 状态连接层 {#connected-layer}
`AppearanceSettings` 是原子组件与共享 `PreferencesProvider` 之间的默认连接层。自定义界面可以使用 Appearance 提供的 `useThemeMode`、`useThemeConfig`、`useResolvedTheme` 等 hooks,而不必知道共享偏好的字符串键,也不必复用默认布局。
```tsx
const [themeConfig, setThemeConfig] = useThemeConfig()
const resolvedTheme = useResolvedTheme()
<ThemeConfigCard
config={themeConfig}
onConfigChange={setThemeConfig}
scheme={resolvedTheme}
/>
```
## 在线预览 {#live-preview}
文档 Sandbox 可以加载通过类型检查的多文件示例,而不需要把实现细节内联到 MDX:
<Sandbox example="appearance/theme-colors" title="主题颜色控件" />
单文件示例不需要创建 Manifest,直接导入组件和原始源码即可:
```mdx
import Demo from "./Demo"
import source from "./Demo.tsx?raw"
<SingleFileSandbox component={Demo} filename="Demo.tsx" source={source} />
```
@@ -0,0 +1,49 @@
---
title: 外观设置弹窗
description: 组合触发器、控制器和快捷键得到完整的外观设置流程。
order: 1
toc:
- id: controller
title: 挂载控制器
- id: trigger
title: 添加触发器
- id: custom-handle
title: 独立弹窗实例
---
## 挂载控制器 {#controller}
在应用共享的 `PreferencesProvider` 内挂载一次 `AppearanceController`。它组合默认设置内容,并注册 `Mod+,` 打开弹窗和 `Mod+D` 切换主题。
```tsx
<PreferencesProvider
effects={[appearancePreferenceEffect]}
initialPreferences={preferences}
>
<AppearanceController />
<App />
</PreferencesProvider>
```
## 添加触发器 {#trigger}
`AppearanceDialogTrigger` 可以放在用户菜单、设置按钮或命令面板中。默认情况下,它会连接到默认控制器使用的 handle。
```tsx
<AppearanceDialogTrigger render={<button type="button" />}>
界面外观
</AppearanceDialogTrigger>
```
## 独立弹窗实例 {#custom-handle}
页面中需要隔离的外观弹窗时,为控制器和触发器传入同一个新 handle。
```tsx
const handle = createAppearanceDialogHandle()
<AppearanceDialogTrigger handle={handle} render={<button type="button" />}>
打开主题设置
</AppearanceDialogTrigger>
<AppearanceController handle={handle} />
```
@@ -0,0 +1,104 @@
---
title: 接入外观偏好
description: 使用应用共享的偏好系统聚合外观定义与副作用。
order: 1
toc:
- id: provider
title: 聚合偏好
- id: effects
title: 注册副作用
- id: theme-style
title: 注入主题变量
- id: persistence
title: 持久化更新
---
## 聚合偏好 {#provider}
在应用边界构建一份完整的偏好定义。把外观、语言、快捷键等功能 package 的 definitions 合并,再创建默认值或从服务端加载初始值。
在应用样式中于 UI 之后导入 Appearance,无需修改任何 UI 组件:
```css
@import "@workspace/ui/globals.css";
@import "@workspace/appearance/globals.css";
```
```tsx
import {
createDefaultPreferences,
PreferencesProvider,
} from "@workspace/preferences"
import {
appearancePreferenceDefinitions,
appearancePreferenceEffect,
} from "@workspace/appearance"
const preferenceDefinitions = {
...appearancePreferenceDefinitions,
...localePreferenceDefinitions,
}
const initialPreferences = createDefaultPreferences(preferenceDefinitions)
const preferenceEffects = [
appearancePreferenceEffect,
localePreferenceEffect,
] as const
export function Root({ children }: { children: React.ReactNode }) {
return (
<PreferencesProvider
effects={preferenceEffects}
initialPreferences={initialPreferences}
>
{children}
</PreferencesProvider>
)
}
```
只需要外观偏好切片时仍可使用 `defaultAppearancePreferences`;聚合多个 package 时,应优先以 definitions 作为生成完整默认值的单一来源。
## 注册副作用 {#effects}
`appearancePreferenceEffect` 在浏览器绘制前更新文档类名、根节点属性和主题变量,并在绘制后监听操作系统主题与动态效果偏好。把组合后的 effect 放进传给 `PreferencesProvider` 的稳定 `effects` 数组中。
## 注入主题变量 {#theme-style}
使用 `buildThemeStyleSheet` 根据一组基础色、强调色以及色彩辨识模式生成浅色和深色 CSS 变量。保留固定的元素 ID 后,外观 effect 可以在偏好变化时更新同一个节点。
```tsx
import {
buildThemeStyleSheet,
THEME_STYLE_ELEMENT_ID,
} from "@workspace/appearance"
;<style
id={THEME_STYLE_ELEMENT_ID}
dangerouslySetInnerHTML={{
__html: buildThemeStyleSheet(
{ baseColor, accentColor },
colorVisionMode,
brightness
),
}}
/>
```
辅助模式会调整需要区分的图表与破坏性语义色,并设置 `data-ui-color-vision`。它不是色觉模拟器,也不会改变照片、视频或品牌资产。
文字缩放通过 `data-ui-text-scale` 改变根字号;减少动态效果通过 `data-ui-motion` 缩短全局动画,并为加载状态保留低运动反馈;亮度只调整主题表面变量,不使用 `filter: brightness()`。
## 持久化更新 {#persistence}
共享的 `onPreferenceChange` 只在值实际变化时收到类型安全的 `{ key, value }`。应用可以把它交给服务端函数或 Cookie 写入器。
```tsx
<PreferencesProvider
effects={preferenceEffects}
initialPreferences={preferences}
onPreferenceChange={(update) => savePreference(update)}
>
{children}
</PreferencesProvider>
```
@@ -0,0 +1,90 @@
---
title: 概览
description: 了解 Appearance 包的原子组件、主题数据和偏好边界。
order: 1
toc:
- id: package-role
title: 包的职责
- id: atomic-components
title: 原子组件
- id: preferences
title: 偏好集成
- id: hooks
title: Hooks
---
## 包的职责 {#package-role}
`@workspace/appearance` 负责主题模式、基础色、强调色、色彩辨识、文字缩放、动态效果和界面亮度。它不创建自己的 Provider 或 store,也不要求修改 `@workspace/ui`;应用通过 `@workspace/preferences` 与其他偏好 package 统一聚合。
## 原子组件 {#atomic-components}
受控组件可以独立组合,不依赖偏好上下文:
- `ThemeModeField` 选择浅色、深色或跟随系统。
- `ColorVisionModeField` 选择标准、Protan、Deutan 或 Tritan 辅助色板。
- `TextScaleField` 将界面缩放到 100%、125%、150% 或 200%。
- `MotionModeField` 跟随系统、减少动态效果或启用完整动效。
- `BrightnessField` 选择柔暗、标准或明亮的界面表面。
- `ThemeColorPicker` 与 `ThemeColorSwatchButton` 选择主题颜色。
- `ThemeConfigCard` 组合基础色与强调色设置。
- `ThemeToggle` 在浅色和深色主题之间切换。
`AppearanceSettings`、`AppearanceDialog` 与 `AppearanceController` 是建立在这些原子组件之上的可选组合层。
## 偏好集成 {#preferences}
```tsx
import {
createDefaultPreferences,
PreferencesProvider,
} from "@workspace/preferences"
import {
AppearanceController,
appearancePreferenceDefinitions,
appearancePreferenceEffect,
} from "@workspace/appearance"
const preferenceDefinitions = {
...appearancePreferenceDefinitions,
}
const initialPreferences = createDefaultPreferences(preferenceDefinitions)
const preferenceEffects = [appearancePreferenceEffect] as const
export function App() {
return (
<PreferencesProvider
effects={preferenceEffects}
initialPreferences={initialPreferences}
>
<AppearanceController />
</PreferencesProvider>
)
}
```
把语言、快捷键等 package 的 definitions 和 effects 添加到相同对象即可。应用可以通过共享的 `onPreferenceChange` 持久化所有 `{ key, value }` 更新,并在服务端使用聚合后的 definitions 解析 Cookie。
色彩辨识辅助不会给整个页面或图片添加滤镜。它只替换 `destructive` 与 `chart-*` 等语义颜色,并在 `<html>` 上写入 `data-ui-color-vision`,供业务组件添加图标、文字、形状或纹理。关键信息不能只靠颜色表达。
文字、动态效果和亮度同样只通过根节点属性与 CSS 变量实现。导入 Appearance 样式后,现有 UI 组件、Portal 和应用内容都会继承这些设置。
## Hooks {#hooks}
应用不需要知道 Appearance 在共享 store 中使用的字符串键。Package 为每个偏好提供了语义化 hook:`useThemeMode`、`useAccentColor`、`useBaseColor`、`useColorVisionMode`、`useTextScale`、`useMotionMode` 和 `useBrightness`。
组合和解析类 hook 用于更高层的界面:
- `useThemeConfig` 将基础色和强调色作为一个配置读写。
- `useSystemTheme` 与 `useSystemMotion` 只读取操作系统设置。
- `useResolvedTheme` 与 `useResolvedMotion` 解析 `system` 后返回最终值。
- `useToggleTheme` 返回在显式浅色与深色之间切换的回调。
- `useAppearancePreference` 提供类型安全的底层入口,`useAppearancePreferences` 返回 Appearance 所有偏好的快照。
```tsx
const [themeMode, setThemeMode] = useThemeMode()
const [themeConfig, setThemeConfig] = useThemeConfig()
const resolvedTheme = useResolvedTheme()
const resolvedMotion = useResolvedMotion()
const toggleTheme = useToggleTheme()
```
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@workspace/appearance",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@tanstack/react-hotkeys": "^0.10.0",
"@workspace/i18n": "workspace:*",
"@workspace/preferences": "workspace:*",
"@workspace/ui": "workspace:*",
"react": "^19.2.6"
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"jsdom": "^30.0.1",
"react-dom": "^19.2.6",
"typescript": "~6",
"vitest": "^4.1.10"
},
"exports": {
".": "./src/index.ts",
"./color-vision": "./src/color-vision.ts",
"./components/appearance-option-list": "./src/components/option-lists/appearance-option-list.tsx",
"./components/brightness-field": "./src/components/fields/brightness-field.tsx",
"./components/color-vision-mode-field": "./src/components/fields/color-vision-mode-field.tsx",
"./components/motion-mode-field": "./src/components/fields/motion-mode-field.tsx",
"./components/text-scale-field": "./src/components/fields/text-scale-field.tsx",
"./components/text-scale-option-list": "./src/components/option-lists/text-scale-option-list.tsx",
"./components/theme-color-picker": "./src/components/theme/theme-color-picker.tsx",
"./components/theme-config-card": "./src/components/theme/theme-config-card.tsx",
"./components/theme-mode-field": "./src/components/fields/theme-mode-field.tsx",
"./components/theme-mode-option-list": "./src/components/option-lists/theme-mode-option-list.tsx",
"./components/theme-toggle-button": "./src/components/theme/theme-toggle-button.tsx",
"./components/fields": "./src/components/fields/index.ts",
"./components/fields/*": "./src/components/fields/*.tsx",
"./components/option-lists": "./src/components/option-lists/index.ts",
"./components/option-lists/*": "./src/components/option-lists/*.tsx",
"./components/theme": "./src/components/theme/index.ts",
"./components/theme/*": "./src/components/theme/*.tsx",
"./components/*": "./src/components/*.tsx",
"./display": "./src/display.ts",
"./effects": "./src/effects.ts",
"./globals.css": "./src/styles/globals.css",
"./hooks": "./src/hooks/index.ts",
"./hooks/*": "./src/hooks/*.ts",
"./locales": "./src/locales/catalogs.ts",
"./locales/*": "./src/locales/*.ts",
"./preferences": "./src/preferences.ts",
"./theme": "./src/theme.ts",
"./use-resolved-theme": "./src/hooks/use-resolved-theme.ts"
}
}
+103
View File
@@ -0,0 +1,103 @@
import type { ThemeScheme } from "./internal/themes"
/**
* Color-vision assistance modes.
*
* These modes select palettes that are easier to distinguish. They are not
* simulations of how another person sees the interface and intentionally do
* not apply a filter to images or the complete document.
*/
export type ColorVisionMode = "standard" | "protan" | "deutan" | "tritan"
export const colorVisionModes = [
"standard",
"protan",
"deutan",
"tritan",
] as const satisfies readonly ColorVisionMode[]
export const COLOR_VISION_ATTRIBUTE = "data-ui-color-vision"
type ColorVisionVariables = Readonly<
Record<
"destructive" | "chart-1" | "chart-2" | "chart-3" | "chart-4" | "chart-5",
string
>
>
/**
* Palettes for semantic data colors that commonly need to be distinguished.
* Components must still pair color with text, icons, shapes, or patterns.
*/
export const colorVisionPalettes: Readonly<
Record<
Exclude<ColorVisionMode, "standard">,
Record<ThemeScheme, ColorVisionVariables>
>
> = {
protan: {
light: {
destructive: "#b54a00",
"chart-1": "#0072b2",
"chart-2": "#e69f00",
"chart-3": "#009e73",
"chart-4": "#cc79a7",
"chart-5": "#6b5ca5",
},
dark: {
destructive: "#ff9d66",
"chart-1": "#56b4e9",
"chart-2": "#f0e442",
"chart-3": "#65d5b3",
"chart-4": "#e8a4ce",
"chart-5": "#afa4e8",
},
},
deutan: {
light: {
destructive: "#a64b00",
"chart-1": "#005f9e",
"chart-2": "#d88c00",
"chart-3": "#8a6f00",
"chart-4": "#bd63a0",
"chart-5": "#168c9e",
},
dark: {
destructive: "#ff9d66",
"chart-1": "#65bff0",
"chart-2": "#f3c969",
"chart-3": "#d7c85d",
"chart-4": "#e5a0cf",
"chart-5": "#63c7d5",
},
},
tritan: {
light: {
destructive: "#b83261",
"chart-1": "#b83280",
"chart-2": "#008f72",
"chart-3": "#d55e00",
"chart-4": "#6b5ca5",
"chart-5": "#53606d",
},
dark: {
destructive: "#f27ca5",
"chart-1": "#ed8bc9",
"chart-2": "#62d1b3",
"chart-3": "#ff9d66",
"chart-4": "#afa4e8",
"chart-5": "#b8c2cc",
},
},
}
export function isColorVisionMode(value: unknown): value is ColorVisionMode {
return colorVisionModes.some((mode) => mode === value)
}
export function getColorVisionCssVariables(
mode: ColorVisionMode,
scheme: ThemeScheme
): ColorVisionVariables | undefined {
return mode === "standard" ? undefined : colorVisionPalettes[mode][scheme]
}
@@ -0,0 +1,147 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react"
import { I18nProvider } from "@workspace/i18n"
import { afterEach, describe, expect, it, vi } from "vitest"
import { messages as englishMessages } from "../locales/en-US"
import { BrightnessField } from "./fields/brightness-field"
import { ColorVisionModeField } from "./fields/color-vision-mode-field"
import { MotionModeField } from "./fields/motion-mode-field"
import { TextScaleField } from "./fields/text-scale-field"
import { TextScaleOptionList } from "./option-lists/text-scale-option-list"
import { ThemeModeOptionList } from "./option-lists/theme-mode-option-list"
import {
accentColorSwatches,
ThemeColorPicker,
} from "./theme/theme-color-picker"
afterEach(cleanup)
function renderLocalized(node: React.ReactNode) {
return render(
<I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
{node}
</I18nProvider>
)
}
function selectOption(triggerName: string, optionName: string) {
fireEvent.click(screen.getByRole("combobox", { name: triggerName }))
const option = screen.getByRole("option", { name: optionName })
fireEvent.pointerDown(option, { pointerType: "mouse" })
fireEvent.click(option, { detail: 1 })
}
describe("appearance atoms", () => {
it("reports color-vision mode changes without owning state", () => {
const onValueChange = vi.fn()
renderLocalized(
<ColorVisionModeField value="standard" onValueChange={onValueChange} />
)
selectOption("Color-vision assistance", "Green-weak assistance (deutan)")
expect(onValueChange).toHaveBeenCalledWith("deutan")
})
it("reports text scale, motion, and brightness changes", () => {
const setTextScale = vi.fn()
const setMotionMode = vi.fn()
const setBrightness = vi.fn()
const { rerender } = renderLocalized(
<TextScaleField value="100" onValueChange={setTextScale} />
)
selectOption("Text size", "150%")
expect(setTextScale).toHaveBeenCalledWith("150")
rerender(
<I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
<MotionModeField value="system" onValueChange={setMotionMode} />
</I18nProvider>
)
selectOption("Motion", "Reduce motion")
expect(setMotionMode).toHaveBeenCalledWith("reduce")
rerender(
<I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
<BrightnessField value="normal" onValueChange={setBrightness} />
</I18nProvider>
)
selectOption("Brightness", "Dim")
expect(setBrightness).toHaveBeenCalledWith("dim")
})
it("provides a radio-based text scale option list", () => {
const onValueChange = vi.fn()
renderLocalized(
<TextScaleOptionList value="125" onValueChange={onValueChange} />
)
expect(screen.getByRole("group", { name: "Text size" })).not.toBeNull()
expect(
(screen.getByRole("radio", { name: "125%" }) as HTMLInputElement).checked
).toBe(true)
fireEvent.click(screen.getByRole("radio", { name: "200%" }))
expect(onValueChange).toHaveBeenCalledWith("200")
})
it("provides a radio-based theme mode option list", () => {
const onValueChange = vi.fn()
renderLocalized(
<ThemeModeOptionList value="system" onValueChange={onValueChange} />
)
expect(screen.getByRole("group", { name: "Theme mode" })).not.toBeNull()
expect(
(screen.getByRole("radio", { name: "System" }) as HTMLInputElement)
.checked
).toBe(true)
fireEvent.click(screen.getByRole("radio", { name: "Dark" }))
expect(onValueChange).toHaveBeenCalledWith("dark")
})
it("reports the selected theme color", () => {
const onValueChange = vi.fn()
renderLocalized(
<ThemeColorPicker
onValueChange={onValueChange}
swatches={accentColorSwatches}
title="Primary color"
value="blue"
/>
)
fireEvent.click(screen.getByRole("button", { name: "Rose" }))
expect(onValueChange).toHaveBeenCalledWith("rose")
})
it("renders compact color swatches without visible labels", () => {
const onValueChange = vi.fn()
renderLocalized(
<ThemeColorPicker
onValueChange={onValueChange}
swatches={accentColorSwatches}
title="Accent color"
value="blue"
variant="swatch"
/>
)
const blue = screen.getByRole("button", { name: "Blue" })
const rose = screen.getByRole("button", { name: "Rose" })
expect(blue.getAttribute("data-variant")).toBe("swatch")
expect(blue.getAttribute("aria-pressed")).toBe("true")
expect(rose.textContent).toBe("")
fireEvent.click(rose)
expect(onValueChange).toHaveBeenCalledWith("rose")
})
})
@@ -0,0 +1,49 @@
import { useCallback, useState } from "react"
import { useToggleTheme } from "../hooks/use-toggle-theme"
import {
AppearanceDialog,
AppearanceDialogContent,
appearanceDialogHandle,
type AppearanceDialogHandle,
} from "./appearance-dialog"
import { AppearanceHotkeys } from "./appearance-hotkeys"
export interface AppearanceControllerProps {
handle?: AppearanceDialogHandle
onOpenChange?: (open: boolean) => void
}
export function AppearanceController({
handle = appearanceDialogHandle,
onOpenChange,
}: AppearanceControllerProps) {
const [isOpen, setIsOpen] = useState(false)
const toggleTheme = useToggleTheme()
const toggleDialog = useCallback(() => {
if (isOpen) {
handle.close()
} else {
handle.open(null)
}
}, [handle, isOpen])
const handleOpenChange = useCallback(
(open: boolean) => {
setIsOpen(open)
onOpenChange?.(open)
},
[onOpenChange]
)
return (
<AppearanceDialog handle={handle} onOpenChange={handleOpenChange}>
<AppearanceHotkeys
onToggleDialog={toggleDialog}
onToggleTheme={toggleTheme}
/>
<AppearanceDialogContent />
</AppearanceDialog>
)
}
@@ -0,0 +1,93 @@
import type { ComponentProps, ReactNode } from "react"
import { LocalizedText } from "@workspace/i18n"
import {
createDialogHandle,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
type DialogHandle,
} from "@workspace/ui/components/dialog"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../messages"
import { AppearanceSettings } from "./appearance-settings"
export type AppearanceDialogHandle = DialogHandle<void>
export function createAppearanceDialogHandle(): AppearanceDialogHandle {
return createDialogHandle<void>()
}
export const appearanceDialogHandle = createAppearanceDialogHandle()
export interface AppearanceDialogProps {
children?: ReactNode
handle?: AppearanceDialogHandle
onOpenChange?: (open: boolean) => void
}
export function AppearanceDialog({
children,
handle = appearanceDialogHandle,
onOpenChange,
}: AppearanceDialogProps) {
return (
<Dialog handle={handle} onOpenChange={onOpenChange}>
{children ?? <AppearanceDialogContent />}
</Dialog>
)
}
export function AppearanceDialogHeader() {
return (
<DialogHeader>
<DialogTitle>
<LocalizedText message={appearanceMessages.appearance} />
</DialogTitle>
<DialogDescription>
<LocalizedText message={appearanceMessages.appearanceDescription} />
</DialogDescription>
</DialogHeader>
)
}
export interface AppearanceDialogContentProps extends Omit<
ComponentProps<typeof DialogContent>,
"children"
> {
children?: ReactNode
}
export function AppearanceDialogContent({
children,
className,
...props
}: AppearanceDialogContentProps) {
return (
<DialogContent className={cn("sm:max-w-max", className)} {...props}>
<AppearanceDialogHeader />
<div className="-mx-6 max-h-[75vh] overflow-y-auto px-6">
{children ?? <AppearanceSettings />}
</div>
</DialogContent>
)
}
export interface AppearanceDialogTriggerProps extends Omit<
ComponentProps<typeof DialogTrigger>,
"handle"
> {
handle?: AppearanceDialogHandle
}
export function AppearanceDialogTrigger({
handle = appearanceDialogHandle,
...props
}: AppearanceDialogTriggerProps) {
return <DialogTrigger handle={handle} {...props} />
}
export const AppearanceTrigger = AppearanceDialogTrigger
@@ -0,0 +1,45 @@
import { useHotkey } from "@tanstack/react-hotkeys"
import { useMessage } from "@workspace/i18n"
import { appearanceMessages } from "../messages"
export interface AppearanceHotkeysProps {
onToggleDialog: VoidFunction
onToggleTheme: VoidFunction
}
export function AppearanceHotkeys({
onToggleDialog,
onToggleTheme,
}: AppearanceHotkeysProps) {
const appearanceTitle = useMessage(appearanceMessages.appearance)
const appearanceDescription = useMessage(
appearanceMessages.appearanceCommandDescription
)
const toggleThemeTitle = useMessage(appearanceMessages.toggleTheme)
const toggleThemeDescription = useMessage(
appearanceMessages.toggleThemeDescription
)
useHotkey("Mod+,", onToggleDialog, {
ignoreInputs: true,
preventDefault: true,
stopPropagation: false,
meta: {
name: appearanceTitle,
description: appearanceDescription,
},
})
useHotkey("Mod+D", onToggleTheme, {
ignoreInputs: true,
preventDefault: true,
stopPropagation: false,
meta: {
name: toggleThemeTitle,
description: toggleThemeDescription,
},
})
return null
}
@@ -0,0 +1,51 @@
import { cn } from "@workspace/ui/lib/utils"
import {
useBrightness,
useColorVisionMode,
useMotionMode,
useTextScale,
useThemeConfig,
useThemeMode,
} from "../hooks/use-appearance-preference"
import { useResolvedTheme } from "../hooks/use-resolved-theme"
import { BrightnessField } from "./fields/brightness-field"
import { ColorVisionModeField } from "./fields/color-vision-mode-field"
import { MotionModeField } from "./fields/motion-mode-field"
import { TextScaleField } from "./fields/text-scale-field"
import { ThemeModeField } from "./fields/theme-mode-field"
import { ThemeConfigCard } from "./theme/theme-config-card"
export interface AppearanceSettingsProps {
className?: string
}
export function AppearanceSettings({ className }: AppearanceSettingsProps) {
const [themeMode, setThemeMode] = useThemeMode()
const [themeConfig, setThemeConfig] = useThemeConfig()
const [brightness, setBrightness] = useBrightness()
const [colorVisionMode, setColorVisionMode] = useColorVisionMode()
const [motionMode, setMotionMode] = useMotionMode()
const [textScale, setTextScale] = useTextScale()
const resolvedTheme = useResolvedTheme()
return (
<div className={cn("space-y-10", className)}>
<div className="grid gap-6 lg:grid-cols-2">
<ThemeModeField value={themeMode} onValueChange={setThemeMode} />
<ColorVisionModeField
value={colorVisionMode}
onValueChange={setColorVisionMode}
/>
<TextScaleField value={textScale} onValueChange={setTextScale} />
<MotionModeField value={motionMode} onValueChange={setMotionMode} />
<BrightnessField value={brightness} onValueChange={setBrightness} />
</div>
<ThemeConfigCard
config={themeConfig}
onConfigChange={setThemeConfig}
scheme={resolvedTheme}
/>
</div>
)
}
@@ -0,0 +1,66 @@
import { useMessage } from "@workspace/i18n"
import { SunIcon } from "@workspace/ui/components/icon"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { Brightness } from "../../display"
import { appearanceMessages } from "../../messages"
export interface BrightnessFieldProps {
className?: string
onValueChange: (value: Brightness) => void
value: Brightness
}
export function BrightnessField({
className,
onValueChange,
value,
}: BrightnessFieldProps) {
const title = useMessage(appearanceMessages.brightness)
const dim = useMessage(appearanceMessages.brightnessDim)
const normal = useMessage(appearanceMessages.brightnessNormal)
const bright = useMessage(appearanceMessages.brightnessBright)
const description = useMessage(appearanceMessages.brightnessDescription)
const items: ReadonlyArray<{ label: string; value: Brightness }> = [
{ label: dim, value: "dim" },
{ label: normal, value: "normal" },
{ label: bright, value: "bright" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<Select
items={items}
value={value}
onValueChange={(brightness) => {
if (brightness) onValueChange(brightness)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-40">
<SunIcon />
<SelectValue>
{(selectedValue: Brightness | null) =>
items.find((item) => item.value === selectedValue)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,75 @@
import { useMessage } from "@workspace/i18n"
import { EyeIcon } from "@workspace/ui/components/icon"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { ColorVisionMode } from "../../color-vision"
import { appearanceMessages } from "../../messages"
export interface ColorVisionModeFieldProps {
className?: string
onValueChange: (value: ColorVisionMode) => void
value: ColorVisionMode
}
export function ColorVisionModeField({
className,
onValueChange,
value,
}: ColorVisionModeFieldProps) {
const title = useMessage(appearanceMessages.colorVisionMode)
const standard = useMessage(appearanceMessages.colorVisionStandard)
const protan = useMessage(appearanceMessages.colorVisionProtan)
const deutan = useMessage(appearanceMessages.colorVisionDeutan)
const tritan = useMessage(appearanceMessages.colorVisionTritan)
const standardDescription = useMessage(
appearanceMessages.colorVisionStandardDescription
)
const assistedDescription = useMessage(
appearanceMessages.colorVisionAssistedDescription
)
const items: ReadonlyArray<{ label: string; value: ColorVisionMode }> = [
{ label: standard, value: "standard" },
{ label: protan, value: "protan" },
{ label: deutan, value: "deutan" },
{ label: tritan, value: "tritan" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{value === "standard" ? standardDescription : assistedDescription}
</p>
<Select
items={items}
value={value}
onValueChange={(mode) => {
if (mode) onValueChange(mode)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-56">
<EyeIcon />
<SelectValue>
{(selectedValue: ColorVisionMode | null) =>
items.find((item) => item.value === selectedValue)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,12 @@
export { BrightnessField, type BrightnessFieldProps } from "./brightness-field"
export {
ColorVisionModeField,
type ColorVisionModeFieldProps,
} from "./color-vision-mode-field"
export { MotionModeField, type MotionModeFieldProps } from "./motion-mode-field"
export { TextScaleField, type TextScaleFieldProps } from "./text-scale-field"
export {
ThemeModeField,
ThemeModeIcon,
type ThemeModeFieldProps,
} from "./theme-mode-field"
@@ -0,0 +1,70 @@
import { useMessage } from "@workspace/i18n"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { MotionMode } from "../../display"
import { appearanceMessages } from "../../messages"
export interface MotionModeFieldProps {
className?: string
onValueChange: (value: MotionMode) => void
value: MotionMode
}
export function MotionModeField({
className,
onValueChange,
value,
}: MotionModeFieldProps) {
const title = useMessage(appearanceMessages.motionMode)
const system = useMessage(appearanceMessages.motionSystem)
const reduce = useMessage(appearanceMessages.motionReduce)
const full = useMessage(appearanceMessages.motionFull)
const descriptions = {
full: useMessage(appearanceMessages.motionFullDescription),
reduce: useMessage(appearanceMessages.motionReduceDescription),
system: useMessage(appearanceMessages.motionSystemDescription),
}
const items: ReadonlyArray<{ label: string; value: MotionMode }> = [
{ label: system, value: "system" },
{ label: reduce, value: "reduce" },
{ label: full, value: "full" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{descriptions[value]}
</p>
<Select
items={items}
value={value}
onValueChange={(mode) => {
if (mode) onValueChange(mode)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-56">
<SelectValue>
{(selectedValue: MotionMode | null) =>
items.find((item) => item.value === selectedValue)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,64 @@
import { useMessage } from "@workspace/i18n"
import { ALargeSmall } from "@workspace/ui/components/icon"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import type { TextScale } from "../../display"
import { appearanceMessages } from "../../messages"
export interface TextScaleFieldProps {
className?: string
onValueChange: (value: TextScale) => void
value: TextScale
}
export function TextScaleField({
className,
onValueChange,
value,
}: TextScaleFieldProps) {
const title = useMessage(appearanceMessages.textScale)
const description = useMessage(appearanceMessages.textScaleDescription)
const items: ReadonlyArray<{ label: string; value: TextScale }> = [
{ label: "100%", value: "100" },
{ label: "125%", value: "125" },
{ label: "150%", value: "150" },
{ label: "200%", value: "200" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<Select
items={items}
value={value}
onValueChange={(scale) => {
if (scale) onValueChange(scale)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-40">
<ALargeSmall />
<SelectValue>
{(selectedValue: TextScale | null) =>
items.find((item) => item.value === selectedValue)?.label
}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,84 @@
import { MonitorIcon, MoonIcon, SunIcon } from "@workspace/ui/components/icon"
import { useMessage } from "@workspace/i18n"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../../messages"
import type { ThemeMode } from "../../preferences"
export interface ThemeModeFieldProps {
className?: string
onValueChange: (value: ThemeMode) => void
value: ThemeMode
}
export function ThemeModeIcon({ mode }: { mode: ThemeMode | null }) {
if (mode === "light") return <SunIcon />
if (mode === "dark") return <MoonIcon />
return <MonitorIcon />
}
export function ThemeModeField({
className,
onValueChange,
value,
}: ThemeModeFieldProps) {
const lightLabel = useMessage(appearanceMessages.lightMode)
const darkLabel = useMessage(appearanceMessages.darkMode)
const systemLabel = useMessage(appearanceMessages.followSystem)
const lightDescription = useMessage(appearanceMessages.lightModeDescription)
const darkDescription = useMessage(appearanceMessages.darkModeDescription)
const systemDescription = useMessage(
appearanceMessages.followSystemDescription
)
const title = useMessage(appearanceMessages.themeMode)
const modeItems: ReadonlyArray<{ label: string; value: ThemeMode }> = [
{ label: lightLabel, value: "light" },
{ label: darkLabel, value: "dark" },
{ label: systemLabel, value: "system" },
]
const description = {
light: lightDescription,
dark: darkDescription,
system: systemDescription,
}[value]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<Select
items={modeItems}
value={value}
onValueChange={(mode) => {
if (mode) onValueChange(mode)
}}
>
<SelectTrigger aria-label={title} className="mt-2 w-full max-w-40">
<SelectValue>
{(selectedValue: ThemeMode | null) => (
<>
<ThemeModeIcon mode={selectedValue} />
{modeItems.find((item) => item.value === selectedValue)?.label}
</>
)}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
{modeItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
<ThemeModeIcon mode={item.value} />
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
@@ -0,0 +1,111 @@
import { useId, type ComponentProps, type ReactNode } from "react"
import { CheckIcon } from "@workspace/ui/components/icon"
import { cn } from "@workspace/ui/lib/utils"
export interface AppearanceOption<TValue extends string> {
description?: ReactNode
disabled?: boolean
icon?: ReactNode
label: ReactNode
value: TValue
}
export interface AppearanceOptionListProps<TValue extends string> extends Omit<
ComponentProps<"fieldset">,
"onChange"
> {
items: readonly AppearanceOption<TValue>[]
label: ReactNode
name?: string
onValueChange: (value: TValue) => void
value: TValue
}
/**
* A controlled, accessible option list shared by appearance preferences.
* Native radio inputs provide keyboard navigation and form semantics while the
* labels supply the larger visual target.
*/
export function AppearanceOptionList<TValue extends string>({
className,
disabled,
items,
label,
name,
onValueChange,
value,
...props
}: AppearanceOptionListProps<TValue>) {
const generatedName = useId()
return (
<fieldset
data-slot="appearance-option-list"
className={cn(
"min-w-0 space-y-1 rounded-xl border p-1 disabled:pointer-events-none disabled:opacity-50",
className
)}
disabled={disabled}
{...props}
>
<legend className="sr-only">{label}</legend>
{items.map((item) => {
const checked = item.value === value
return (
<label
key={item.value}
data-checked={checked}
data-disabled={item.disabled || undefined}
className={cn(
"relative grid min-h-12 cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-lg px-3 py-2 transition-colors outline-none",
"has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-ring has-[input:focus-visible]:ring-offset-2 has-[input:focus-visible]:ring-offset-background",
checked
? "bg-accent/12 text-accent dark:bg-accent/20"
: "text-foreground hover:bg-highlight hover:text-highlight-foreground",
item.disabled && "pointer-events-none opacity-50"
)}
>
<input
type="radio"
className="peer sr-only"
checked={checked}
disabled={item.disabled}
name={name ?? generatedName}
value={item.value}
onChange={() => onValueChange(item.value)}
/>
<span
className="inline-flex size-6 shrink-0 items-center justify-center [&_svg]:size-5"
aria-hidden="true"
>
{item.icon}
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-medium">
{item.label}
</span>
{item.description == null ? null : (
<span
className={cn(
"mt-0.5 block text-xs",
checked ? "text-accent/80" : "text-muted-foreground"
)}
>
{item.description}
</span>
)}
</span>
<CheckIcon
className={cn(
"size-4 shrink-0 transition-opacity",
checked ? "opacity-100" : "opacity-0"
)}
aria-hidden="true"
/>
</label>
)
})}
</fieldset>
)
}
@@ -0,0 +1,13 @@
export {
AppearanceOptionList,
type AppearanceOption,
type AppearanceOptionListProps,
} from "./appearance-option-list"
export {
TextScaleOptionList,
type TextScaleOptionListProps,
} from "./text-scale-option-list"
export {
ThemeModeOptionList,
type ThemeModeOptionListProps,
} from "./theme-mode-option-list"
@@ -0,0 +1,61 @@
import { useMessage } from "@workspace/i18n"
import { cn } from "@workspace/ui/lib/utils"
import type { TextScale } from "../../display"
import { appearanceMessages } from "../../messages"
import {
AppearanceOptionList,
type AppearanceOption,
} from "./appearance-option-list"
const textScaleItems = [
{
icon: <span className="text-xs font-semibold">A</span>,
label: "100%",
value: "100",
},
{
icon: <span className="text-sm font-semibold">A</span>,
label: "125%",
value: "125",
},
{
icon: <span className="text-base font-semibold">A</span>,
label: "150%",
value: "150",
},
{
icon: <span className="text-lg font-semibold">A</span>,
label: "200%",
value: "200",
},
] as const satisfies readonly AppearanceOption<TextScale>[]
export interface TextScaleOptionListProps {
className?: string
onValueChange: (value: TextScale) => void
value: TextScale
}
export function TextScaleOptionList({
className,
onValueChange,
value,
}: TextScaleOptionListProps) {
const title = useMessage(appearanceMessages.textScale)
const description = useMessage(appearanceMessages.textScaleDescription)
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<AppearanceOptionList
className="mt-2 w-full max-w-64"
items={textScaleItems}
label={title}
value={value}
onValueChange={onValueChange}
/>
</div>
)
}
@@ -0,0 +1,53 @@
import { useMessage } from "@workspace/i18n"
import { MonitorIcon, MoonIcon, SunIcon } from "@workspace/ui/components/icon"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../../messages"
import type { ThemeMode } from "../../preferences"
import {
AppearanceOptionList,
type AppearanceOption,
} from "./appearance-option-list"
export interface ThemeModeOptionListProps {
className?: string
onValueChange: (value: ThemeMode) => void
value: ThemeMode
}
export function ThemeModeOptionList({
className,
onValueChange,
value,
}: ThemeModeOptionListProps) {
const title = useMessage(appearanceMessages.themeMode)
const lightLabel = useMessage(appearanceMessages.lightMode)
const darkLabel = useMessage(appearanceMessages.darkMode)
const systemLabel = useMessage(appearanceMessages.followSystem)
const descriptions: Record<ThemeMode, string> = {
light: useMessage(appearanceMessages.lightModeDescription),
dark: useMessage(appearanceMessages.darkModeDescription),
system: useMessage(appearanceMessages.followSystemDescription),
}
const items: readonly AppearanceOption<ThemeMode>[] = [
{ icon: <SunIcon />, label: lightLabel, value: "light" },
{ icon: <MoonIcon />, label: darkLabel, value: "dark" },
{ icon: <MonitorIcon />, label: systemLabel, value: "system" },
]
return (
<div className={cn("min-w-0 lg:flex-1", className)}>
<h2 className="font-semibold">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{descriptions[value]}
</p>
<AppearanceOptionList
className="mt-2 w-full max-w-64"
items={items}
label={title}
value={value}
onValueChange={onValueChange}
/>
</div>
)
}
@@ -0,0 +1,20 @@
export {
ThemeColorPicker,
ThemeColorSwatchButton,
accentColorSwatches,
baseColorSwatches,
createThemeColorSwatches,
type ThemeColor,
type ThemeColorPickerProps,
type ThemeColorPickerVariant,
type ThemeColorSwatch,
type ThemeColorSwatchButtonProps,
type ThemeColorSwatchScheme,
} from "./theme-color-picker"
export { ThemeConfigCard, type ThemeConfigCardProps } from "./theme-config-card"
export {
ThemeToggle,
ThemeToggleButton,
type ThemeToggleButtonProps,
type ThemeToggleProps,
} from "./theme-toggle-button"
@@ -0,0 +1,162 @@
import type { CSSProperties, ReactNode } from "react"
import { useMessage } from "@workspace/i18n"
import { CheckIcon } from "@workspace/ui/components/icon"
import { cn } from "@workspace/ui/lib/utils"
import { themeColorMessages } from "../../messages"
import {
accentColors,
baseColors,
getTheme,
type AccentColor,
type BaseColor,
} from "../../theme"
export type ThemeColor = BaseColor | AccentColor
export type ThemeColorPickerVariant = "pill" | "swatch"
export interface ThemeColorSwatch<T extends ThemeColor = ThemeColor> {
dark: ThemeColorSwatchScheme
light: ThemeColorSwatchScheme
name: T
}
export interface ThemeColorSwatchScheme {
background: string
foreground: string
}
export function createThemeColorSwatches<T extends ThemeColor>(
colors: readonly T[]
): ThemeColorSwatch<T>[] {
return colors.map((name) => {
const theme = getTheme(name)!
return {
name,
light: {
background: theme.cssVars.light.accent,
foreground: theme.cssVars.light["accent-foreground"],
},
dark: {
background: theme.cssVars.dark.accent,
foreground: theme.cssVars.dark["accent-foreground"],
},
}
})
}
export const baseColorSwatches = createThemeColorSwatches(baseColors)
export const accentColorSwatches = createThemeColorSwatches(accentColors)
export interface ThemeColorPickerProps<T extends ThemeColor> {
className?: string
onValueChange: (color: T) => void
swatches: readonly ThemeColorSwatch<T>[]
title: ReactNode
value: T
variant?: ThemeColorPickerVariant
}
export function ThemeColorPicker<T extends ThemeColor>({
className,
onValueChange,
swatches,
title,
value,
variant = "pill",
}: ThemeColorPickerProps<T>) {
return (
<fieldset
data-slot="theme-color-picker"
data-variant={variant}
className={cn(
variant === "pill" ? "relative rounded-lg border p-4" : "border-0 p-0",
className
)}
>
<legend
className={cn(
variant === "pill"
? "absolute top-0 left-4 z-2 block -translate-y-1/2 bg-popover px-2 text-xs text-muted-foreground"
: "mb-3 px-0 text-sm font-medium text-foreground"
)}
>
{title}
</legend>
<div
className={cn(
"flex flex-wrap items-center",
variant === "pill" ? "gap-2" : "gap-3"
)}
>
{swatches.map((swatch) => (
<ThemeColorSwatchButton
key={swatch.name}
swatch={swatch}
pressed={value === swatch.name}
variant={variant}
onClick={() => onValueChange(swatch.name)}
/>
))}
</div>
</fieldset>
)
}
export interface ThemeColorSwatchButtonProps {
onClick: VoidFunction
pressed: boolean
swatch: ThemeColorSwatch
variant?: ThemeColorPickerVariant
}
export function ThemeColorSwatchButton({
onClick,
pressed,
swatch,
variant = "pill",
}: ThemeColorSwatchButtonProps) {
const swatchTitle = useMessage(themeColorMessages[swatch.name])
const style = {
"--light-bg": swatch.light.background,
"--light-fg": swatch.light.foreground,
"--dark-bg": swatch.dark.background,
"--dark-fg": swatch.dark.foreground,
} as CSSProperties
return (
<button
type="button"
data-slot="theme-color-swatch"
data-variant={variant}
aria-label={swatchTitle}
aria-pressed={pressed}
title={variant === "swatch" ? swatchTitle : undefined}
className={cn(
"appearance-none bg-(--light-bg) dark:bg-(--dark-bg)",
variant === "pill"
? [
"h-8 min-w-8 truncate overflow-hidden rounded-4xl border-2 px-2 text-xs whitespace-nowrap ring-popover ring-inset",
"border-(--light-bg) text-(--light-fg) dark:border-(--dark-bg) dark:text-(--dark-fg)",
"ring-0 aria-pressed:ring-2",
]
: [
"inline-flex size-8 shrink-0 items-center justify-center rounded-full border border-black/10 p-0 ring-offset-2 ring-offset-background transition-[box-shadow,transform]",
"hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none",
"aria-pressed:ring-2 aria-pressed:ring-(--light-bg) dark:border-white/15 dark:aria-pressed:ring-(--dark-bg)",
]
)}
style={style}
onClick={onClick}
>
{variant === "pill" ? (
swatchTitle
) : pressed ? (
<span className="inline-flex size-5 items-center justify-center rounded-full bg-(--light-fg) text-(--light-bg) shadow-sm dark:bg-(--dark-fg) dark:text-(--dark-bg)">
<CheckIcon className="size-3.5" />
</span>
) : null}
</button>
)
}
@@ -0,0 +1,61 @@
import { LocalizedText } from "@workspace/i18n"
import { MoonIcon, SunIcon } from "@workspace/ui/components/icon"
import { cn } from "@workspace/ui/lib/utils"
import { appearanceMessages } from "../../messages"
import type { ThemeColorConfig, ThemeScheme } from "../../theme"
import {
accentColorSwatches,
baseColorSwatches,
ThemeColorPicker,
} from "./theme-color-picker"
export interface ThemeConfigCardProps {
className?: string
config: ThemeColorConfig
onConfigChange: (config: ThemeColorConfig) => void
scheme: ThemeScheme
}
export function ThemeConfigCard({
className,
config,
onConfigChange,
scheme,
}: ThemeConfigCardProps) {
return (
<section className={cn("w-full max-w-md rounded-xl border", className)}>
<header className="flex gap-4 border-b p-3">
{scheme === "light" ? (
<SunIcon className="size-5" />
) : (
<MoonIcon className="size-5" />
)}
<h2>
<LocalizedText message={appearanceMessages.themeColors} />
</h2>
</header>
<div className="space-y-4 p-4">
<p>
<LocalizedText message={appearanceMessages.themeColorsDescription} />
</p>
<ThemeColorPicker
swatches={baseColorSwatches}
title={<LocalizedText message={appearanceMessages.baseColor} />}
value={config.baseColor}
onValueChange={(baseColor) =>
onConfigChange({ ...config, baseColor })
}
/>
<ThemeColorPicker
swatches={accentColorSwatches}
title={<LocalizedText message={appearanceMessages.accentColor} />}
value={config.accentColor}
onValueChange={(accentColor) =>
onConfigChange({ ...config, accentColor })
}
/>
</div>
</section>
)
}
@@ -0,0 +1,59 @@
import type { ComponentProps } from "react"
import { MoonIcon, SunIcon } from "@workspace/ui/components/icon"
import { useMessage } from "@workspace/i18n"
import { Button } from "@workspace/ui/components/button"
import { cn } from "@workspace/ui/lib/utils"
import { useResolvedTheme } from "../../hooks/use-resolved-theme"
import { useToggleTheme } from "../../hooks/use-toggle-theme"
import { appearanceMessages } from "../../messages"
import type { ThemeScheme } from "../../theme"
export interface ThemeToggleProps extends Omit<
ComponentProps<typeof Button>,
"children" | "onClick"
> {
onThemeChange: (theme: ThemeScheme) => void
theme: ThemeScheme
}
export function ThemeToggle({
className,
onThemeChange,
theme,
...props
}: ThemeToggleProps) {
const switchToDarkLabel = useMessage(appearanceMessages.switchToDark)
const switchToLightLabel = useMessage(appearanceMessages.switchToLight)
const nextTheme = theme === "dark" ? "light" : "dark"
return (
<Button
variant="ghost"
size="icon-lg"
aria-label={nextTheme === "dark" ? switchToDarkLabel : switchToLightLabel}
className={cn(
"rounded-lg [&_svg]:opacity-45 [&_svg]:transition-opacity hover:[&_svg]:opacity-100",
className
)}
{...props}
onClick={() => onThemeChange(nextTheme)}
>
{theme === "dark" ? <SunIcon /> : <MoonIcon />}
</Button>
)
}
export type ThemeToggleButtonProps = Omit<
ThemeToggleProps,
"onThemeChange" | "theme"
>
export function ThemeToggleButton(props: ThemeToggleButtonProps) {
const resolvedTheme = useResolvedTheme()
const toggleTheme = useToggleTheme()
return (
<ThemeToggle {...props} theme={resolvedTheme} onThemeChange={toggleTheme} />
)
}
+77
View File
@@ -0,0 +1,77 @@
import type { ThemeScheme } from "./internal/themes"
export type Brightness = "dim" | "normal" | "bright"
export type MotionMode = "system" | "reduce" | "full"
export type ResolvedMotionMode = Exclude<MotionMode, "system">
export type TextScale = "100" | "125" | "150" | "200"
export const brightnessModes = [
"dim",
"normal",
"bright",
] as const satisfies readonly Brightness[]
export const motionModes = [
"system",
"reduce",
"full",
] as const satisfies readonly MotionMode[]
export const textScales = [
"100",
"125",
"150",
"200",
] as const satisfies readonly TextScale[]
export const BRIGHTNESS_ATTRIBUTE = "data-ui-brightness"
export const MOTION_ATTRIBUTE = "data-ui-motion"
export const TEXT_SCALE_ATTRIBUTE = "data-ui-text-scale"
const surfaceVariableNames = [
"background",
"card",
"popover",
"secondary",
"muted",
"highlight",
"sidebar",
] as const
export function isBrightness(value: unknown): value is Brightness {
return brightnessModes.some((mode) => mode === value)
}
export function isMotionMode(value: unknown): value is MotionMode {
return motionModes.some((mode) => mode === value)
}
export function isTextScale(value: unknown): value is TextScale {
return textScales.some((scale) => scale === value)
}
/**
* Adjusts interface surfaces without filtering media, illustrations, or brand
* colors. Foreground colors are preserved so dimming does not reduce contrast.
*/
export function applyBrightnessToTheme(
variables: Record<string, string>,
brightness: Brightness,
scheme: ThemeScheme
): Record<string, string> {
if (brightness === "normal") return variables
const target = brightness === "dim" ? "black" : "white"
const amount = {
bright: scheme === "light" ? 12 : 8,
dim: scheme === "light" ? 5 : 12,
}[brightness]
const adjusted = { ...variables }
for (const name of surfaceVariableNames) {
const value = variables[name]
if (value) {
adjusted[name] = `color-mix(in oklch,${value},${target} ${amount}%)`
}
}
return adjusted
}
+106
View File
@@ -0,0 +1,106 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react"
import { PreferencesProvider, usePreference } from "@workspace/preferences"
import {
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest"
import { appearancePreferenceEffect } from "./effects"
import { defaultAppearancePreferences } from "./preferences"
import { COLOR_VISION_ATTRIBUTE } from "./color-vision"
import {
BRIGHTNESS_ATTRIBUTE,
MOTION_ATTRIBUTE,
TEXT_SCALE_ATTRIBUTE,
} from "./display"
import { THEME_STYLE_ELEMENT_ID } from "./theme"
beforeAll(() => {
vi.stubGlobal(
"matchMedia",
vi.fn().mockImplementation((query: string) => ({
addEventListener: vi.fn(),
matches: false,
media: query,
removeEventListener: vi.fn(),
}))
)
vi.stubGlobal(
"requestAnimationFrame",
vi.fn(() => 1)
)
vi.stubGlobal("cancelAnimationFrame", vi.fn())
})
beforeEach(() => {
const style = document.createElement("style")
style.id = THEME_STYLE_ELEMENT_ID
document.head.append(style)
})
afterEach(() => {
cleanup()
document.getElementById(THEME_STYLE_ELEMENT_ID)?.remove()
document.documentElement.className = ""
document.documentElement.removeAttribute(COLOR_VISION_ATTRIBUTE)
document.documentElement.removeAttribute(BRIGHTNESS_ATTRIBUTE)
document.documentElement.removeAttribute(MOTION_ATTRIBUTE)
document.documentElement.removeAttribute(TEXT_SCALE_ATTRIBUTE)
document.documentElement.style.colorScheme = ""
})
function PreferenceProbe() {
const [themeMode, setThemeMode] = usePreference("theme-mode")
return (
<button type="button" onClick={() => setThemeMode("dark")}>
{themeMode}
</button>
)
}
describe("appearancePreferenceEffect", () => {
it("integrates with the shared PreferencesProvider", () => {
const onPreferenceChange = vi.fn()
render(
<PreferencesProvider
effects={[appearancePreferenceEffect]}
initialPreferences={defaultAppearancePreferences}
onPreferenceChange={onPreferenceChange}
>
<PreferenceProbe />
</PreferencesProvider>
)
expect(document.documentElement.classList.contains("ui:light")).toBe(true)
expect(document.documentElement.getAttribute(COLOR_VISION_ATTRIBUTE)).toBe(
"standard"
)
expect(document.documentElement.getAttribute(BRIGHTNESS_ATTRIBUTE)).toBe(
"normal"
)
expect(document.documentElement.getAttribute(MOTION_ATTRIBUTE)).toBe("full")
expect(document.documentElement.getAttribute(TEXT_SCALE_ATTRIBUTE)).toBe(
"100"
)
expect(
document.getElementById(THEME_STYLE_ELEMENT_ID)?.textContent
).toContain("--background:")
fireEvent.click(screen.getByRole("button"))
expect(document.documentElement.classList.contains("ui:dark")).toBe(true)
expect(onPreferenceChange).toHaveBeenCalledWith({
key: "theme-mode",
value: "dark",
})
})
})
+158
View File
@@ -0,0 +1,158 @@
import {
type PreferenceEffect,
type PreferenceEffectCallback,
} from "@workspace/preferences"
import type { AppearancePreferences } from "./preferences"
import { COLOR_VISION_ATTRIBUTE } from "./color-vision"
import {
BRIGHTNESS_ATTRIBUTE,
MOTION_ATTRIBUTE,
TEXT_SCALE_ATTRIBUTE,
} from "./display"
import { buildThemeStyleSheet, THEME_STYLE_ELEMENT_ID } from "./theme"
import {
getSystemMotion,
getSystemTheme,
subscribeToSystemMotion,
subscribeToSystemTheme,
} from "./internal/system-preferences"
const appearancePreferenceKeys = [
"accent-color",
"base-color",
"brightness",
"color-vision-mode",
"motion-mode",
"text-scale",
"theme-mode",
] as const satisfies readonly (keyof AppearancePreferences)[]
let restoreThemeTransitionFrame: number | undefined
function disableThemeTransitionsTemporarily() {
if (typeof document === "undefined") return
const root = document.documentElement
root.classList.add("ui:theme-changing")
if (restoreThemeTransitionFrame !== undefined) {
cancelAnimationFrame(restoreThemeTransitionFrame)
}
restoreThemeTransitionFrame = requestAnimationFrame(() => {
restoreThemeTransitionFrame = requestAnimationFrame(() => {
root.classList.remove("ui:theme-changing")
restoreThemeTransitionFrame = undefined
})
})
}
export function applyAppearancePreferences(preferences: AppearancePreferences) {
if (typeof document === "undefined") return
const root = document.documentElement
const resolvedTheme =
preferences["theme-mode"] === "system"
? getSystemTheme()
: preferences["theme-mode"]
const resolvedMotion =
preferences["motion-mode"] === "system"
? getSystemMotion()
: preferences["motion-mode"]
const themeStyle = document.getElementById(THEME_STYLE_ELEMENT_ID)
if (themeStyle) {
themeStyle.textContent = buildThemeStyleSheet(
{
accentColor: preferences["accent-color"],
baseColor: preferences["base-color"],
},
preferences["color-vision-mode"],
preferences.brightness
)
}
root.classList.toggle("ui:dark", resolvedTheme === "dark")
root.classList.toggle("ui:light", resolvedTheme === "light")
root.setAttribute(BRIGHTNESS_ATTRIBUTE, preferences.brightness)
root.setAttribute(COLOR_VISION_ATTRIBUTE, preferences["color-vision-mode"])
root.setAttribute(MOTION_ATTRIBUTE, resolvedMotion)
root.setAttribute(TEXT_SCALE_ATTRIBUTE, preferences["text-scale"])
root.style.colorScheme = resolvedTheme
}
/** Applies appearance preferences before the browser paints the updated UI. */
export const applyAppearancePreferenceEffect: PreferenceEffectCallback = ({
previousPreferences,
preferences,
}) => {
const appearancePreferences = preferences as AppearancePreferences
const previousAppearancePreferences =
previousPreferences as AppearancePreferences
if (
appearancePreferenceKeys.some(
(key) =>
!Object.is(
previousAppearancePreferences[key],
appearancePreferences[key]
)
)
) {
disableThemeTransitionsTemporarily()
}
applyAppearancePreferences(appearancePreferences)
}
/** Keeps system-mode themes synchronized with the operating system setting. */
export const subscribeSystemThemePreferenceEffect: PreferenceEffectCallback = ({
preferences,
store,
}) => {
const appearancePreferences = preferences as AppearancePreferences
if (appearancePreferences["theme-mode"] !== "system") return
const applySystemTheme = () => {
disableThemeTransitionsTemporarily()
applyAppearancePreferences(store.getSnapshot() as AppearancePreferences)
}
return subscribeToSystemTheme(applySystemTheme)
}
/** Keeps system-mode motion synchronized with the operating system setting. */
export const subscribeSystemMotionPreferenceEffect: PreferenceEffectCallback =
({ preferences, store }) => {
const appearancePreferences = preferences as AppearancePreferences
if (appearancePreferences["motion-mode"] !== "system") return
const applySystemMotion = () => {
applyAppearancePreferences(store.getSnapshot() as AppearancePreferences)
}
return subscribeToSystemMotion(applySystemMotion)
}
/** Subscribes to every operating-system preference used by Appearance. */
export const subscribeSystemAppearancePreferenceEffect: PreferenceEffectCallback =
(context) => {
const cleanups = [
subscribeSystemThemePreferenceEffect(context),
subscribeSystemMotionPreferenceEffect(context),
].filter(
(cleanup): cleanup is VoidFunction => typeof cleanup === "function"
)
if (cleanups.length === 0) return
return () => {
for (const cleanup of cleanups) cleanup()
}
}
/** Register this effect with the application's shared `PreferencesProvider`. */
export const appearancePreferenceEffect: PreferenceEffect = {
layoutEffect: applyAppearancePreferenceEffect,
effect: subscribeSystemAppearancePreferenceEffect,
}
@@ -0,0 +1,107 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react"
import { PreferencesProvider } from "@workspace/preferences"
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"
import { defaultAppearancePreferences } from "../preferences"
import {
useAccentColor,
useAppearancePreferences,
useThemeConfig,
useThemeMode,
} from "./use-appearance-preference"
import { useResolvedMotion } from "./use-resolved-motion"
import { useResolvedTheme } from "./use-resolved-theme"
import { useToggleTheme } from "./use-toggle-theme"
beforeAll(() => {
vi.stubGlobal(
"matchMedia",
vi.fn().mockImplementation((query: string) => ({
addEventListener: vi.fn(),
matches: false,
media: query,
removeEventListener: vi.fn(),
}))
)
})
afterEach(cleanup)
function renderPreferences(node: React.ReactNode) {
return render(
<PreferencesProvider initialPreferences={defaultAppearancePreferences}>
{node}
</PreferencesProvider>
)
}
function PreferenceProbe() {
const [accentColor, setAccentColor] = useAccentColor()
const [themeMode] = useThemeMode()
const preferences = useAppearancePreferences()
return (
<button type="button" onClick={() => setAccentColor("rose")}>
{accentColor}:{themeMode}:{preferences.brightness}
</button>
)
}
function ThemeConfigProbe() {
const [config, setConfig] = useThemeConfig()
return (
<button
type="button"
onClick={() => setConfig({ accentColor: "violet", baseColor: "zinc" })}
>
{config.accentColor}:{config.baseColor}
</button>
)
}
function ResolvedPreferenceProbe() {
const resolvedTheme = useResolvedTheme()
const resolvedMotion = useResolvedMotion()
const toggleTheme = useToggleTheme()
return (
<button type="button" onClick={toggleTheme}>
{resolvedTheme}:{resolvedMotion}
</button>
)
}
describe("appearance hooks", () => {
it("exposes typed preference-specific hooks and the complete snapshot", () => {
renderPreferences(<PreferenceProbe />)
const button = screen.getByRole("button")
expect(button.textContent).toBe("blue:system:normal")
fireEvent.click(button)
expect(button.textContent).toBe("rose:system:normal")
})
it("updates accent and base colors through one theme config API", () => {
renderPreferences(<ThemeConfigProbe />)
const button = screen.getByRole("button")
expect(button.textContent).toBe("blue:neutral")
fireEvent.click(button)
expect(button.textContent).toBe("violet:zinc")
})
it("resolves system preferences and toggles to an explicit theme", () => {
renderPreferences(<ResolvedPreferenceProbe />)
const button = screen.getByRole("button")
expect(button.textContent).toBe("light:full")
fireEvent.click(button)
expect(button.textContent).toBe("dark:full")
})
})
+16
View File
@@ -0,0 +1,16 @@
export {
useAccentColor,
useAppearancePreference,
useAppearancePreferences,
useBaseColor,
useBrightness,
useColorVisionMode,
useMotionMode,
useTextScale,
useThemeConfig,
useThemeMode,
type AppearancePreferenceState,
} from "./use-appearance-preference"
export { useResolvedMotion, useSystemMotion } from "./use-resolved-motion"
export { useResolvedTheme, useSystemTheme } from "./use-resolved-theme"
export { useToggleTheme } from "./use-toggle-theme"
@@ -0,0 +1,112 @@
import React from "react"
import { usePreference, usePreferences } from "@workspace/preferences"
import type {
AppearancePreferenceKey,
AppearancePreferences,
AppearancePreferenceValue,
} from "../preferences"
import type { ThemeColorConfig } from "../theme"
export type AppearancePreferenceState<Key extends AppearancePreferenceKey> = [
AppearancePreferenceValue<Key>,
React.Dispatch<React.SetStateAction<AppearancePreferenceValue<Key>>>,
]
/** Reads and updates one preference owned by the Appearance package. */
export function useAppearancePreference<Key extends AppearancePreferenceKey>(
key: Key
): AppearancePreferenceState<Key> {
return usePreference(key)
}
/** Returns the complete Appearance-owned portion of the shared preferences. */
export function useAppearancePreferences(): AppearancePreferences {
const preferences = usePreferences()
const accentColor = preferences["accent-color"]
const baseColor = preferences["base-color"]
const brightness = preferences.brightness
const colorVisionMode = preferences["color-vision-mode"]
const motionMode = preferences["motion-mode"]
const textScale = preferences["text-scale"]
const themeMode = preferences["theme-mode"]
return React.useMemo(
() => ({
"accent-color": accentColor,
"base-color": baseColor,
brightness,
"color-vision-mode": colorVisionMode,
"motion-mode": motionMode,
"text-scale": textScale,
"theme-mode": themeMode,
}),
[
accentColor,
baseColor,
brightness,
colorVisionMode,
motionMode,
textScale,
themeMode,
]
)
}
export function useAccentColor() {
return useAppearancePreference("accent-color")
}
export function useBaseColor() {
return useAppearancePreference("base-color")
}
export function useBrightness() {
return useAppearancePreference("brightness")
}
export function useColorVisionMode() {
return useAppearancePreference("color-vision-mode")
}
export function useMotionMode() {
return useAppearancePreference("motion-mode")
}
export function useTextScale() {
return useAppearancePreference("text-scale")
}
export function useThemeMode() {
return useAppearancePreference("theme-mode")
}
/** Treats the two color preferences as one controlled theme configuration. */
export function useThemeConfig(): [
ThemeColorConfig,
React.Dispatch<React.SetStateAction<ThemeColorConfig>>,
] {
const [accentColor, setAccentColor] = useAccentColor()
const [baseColor, setBaseColor] = useBaseColor()
const config = React.useMemo(
() => ({ accentColor, baseColor }),
[accentColor, baseColor]
)
const configRef = React.useRef(config)
configRef.current = config
const setConfig = React.useCallback<
React.Dispatch<React.SetStateAction<ThemeColorConfig>>
>(
(action) => {
const nextConfig =
typeof action === "function" ? action(configRef.current) : action
configRef.current = nextConfig
setAccentColor(nextConfig.accentColor)
setBaseColor(nextConfig.baseColor)
},
[setAccentColor, setBaseColor]
)
return React.useMemo(() => [config, setConfig], [config, setConfig])
}
@@ -0,0 +1,26 @@
import React from "react"
import type { ResolvedMotionMode } from "../display"
import {
getServerSystemMotion,
getSystemMotion,
subscribeToSystemMotion,
} from "../internal/system-preferences"
import { useMotionMode } from "./use-appearance-preference"
/** Tracks the operating system reduced-motion preference. */
export function useSystemMotion(): ResolvedMotionMode {
return React.useSyncExternalStore(
subscribeToSystemMotion,
getSystemMotion,
getServerSystemMotion
)
}
/** Resolves `system` to the current operating system motion preference. */
export function useResolvedMotion(): ResolvedMotionMode {
const [motionMode] = useMotionMode()
const systemMotion = useSystemMotion()
return motionMode === "system" ? systemMotion : motionMode
}
@@ -0,0 +1,26 @@
import React from "react"
import {
getServerSystemTheme,
getSystemTheme,
subscribeToSystemTheme,
} from "../internal/system-preferences"
import type { ThemeScheme } from "../theme"
import { useThemeMode } from "./use-appearance-preference"
/** Tracks the operating system color scheme independently of theme mode. */
export function useSystemTheme(): ThemeScheme {
return React.useSyncExternalStore(
subscribeToSystemTheme,
getSystemTheme,
getServerSystemTheme
)
}
/** Resolves `system` to the current operating system color scheme. */
export function useResolvedTheme(): ThemeScheme {
const [themeMode] = useThemeMode()
const systemTheme = useSystemTheme()
return themeMode === "system" ? systemTheme : themeMode
}
@@ -0,0 +1,14 @@
import React from "react"
import { useThemeMode } from "./use-appearance-preference"
import { useResolvedTheme } from "./use-resolved-theme"
/** Returns a stable callback that toggles between explicit light and dark modes. */
export function useToggleTheme(): VoidFunction {
const [, setThemeMode] = useThemeMode()
const resolvedTheme = useResolvedTheme()
return React.useCallback(() => {
setThemeMode(resolvedTheme === "dark" ? "light" : "dark")
}, [resolvedTheme, setThemeMode])
}

Some files were not shown because too many files have changed in this diff Show More