Compare commits
48 Commits
f442c07dbf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 774495912c | |||
| 57d006a39b | |||
| ffda0b9099 | |||
| 8b880a19e2 | |||
| e34675a931 | |||
| 642cb71b93 | |||
| 352db2c4c0 | |||
| 983ebead02 | |||
| 9a6ec3a396 | |||
| 4710a56242 | |||
| aae02d1502 | |||
| 75d8cbbfc3 | |||
| 68f142dfa3 | |||
| 24b32ea5cc | |||
| f56397fb97 | |||
| 844acad316 | |||
| 23e0867027 | |||
| acbc62463e | |||
| ffe180f482 | |||
| 91e010cff7 | |||
| d5cec8342d | |||
| c39d131a86 | |||
| d23deca0e7 | |||
| 0ec968c344 | |||
| bca874fd30 | |||
| 1353dc3145 | |||
| d3de7cc172 | |||
| 3eb0246c40 | |||
| 93373adb9a | |||
| 91cfb3d068 | |||
| 4c61e79d0d | |||
| fb985edb20 | |||
| a9d6856ab8 | |||
| b26c8886cf | |||
| 3e671cce47 | |||
| 4d3892a5ea | |||
| df5bbc739c | |||
| 6711401f5f | |||
| e2dbbcb884 | |||
| 71c416caab | |||
| a6f5f0b649 | |||
| 02df33ce6b | |||
| eebf29c3d5 | |||
| c519d4e74b | |||
| 2a6284d1b0 | |||
| 7d57c9e7ae | |||
| 9635de56e8 | |||
| a57fdbe7c6 |
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@": path.resolve(import.meta.dirname, "./src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
}
|
||||
|
||||
function escapeAttribute(value: string) {
|
||||
return escapeText(value).replaceAll('"', """)
|
||||
}
|
||||
+36
-18
@@ -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 +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 |
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)!
|
||||
}
|
||||
@@ -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> </span>
|
||||
<span>R</span>
|
||||
<span className="text-slate-300 dark:text-slate-400">eact</span>
|
||||
<span> </span>
|
||||
<span>A</span>
|
||||
<span className="text-slate-300 dark:text-slate-400">pp</span>
|
||||
<span> </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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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|$)/, "")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,5 +22,9 @@
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
"include": [
|
||||
"src",
|
||||
"../packages/*/docs/sandboxes/**/*.ts",
|
||||
"../packages/*/docs/sandboxes/**/*.tsx"
|
||||
]
|
||||
}
|
||||
@@ -19,5 +19,5 @@
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
"include": ["vite.config.ts", "scripts"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/components/sandbox/sandbox-env.d.ts"],
|
||||
"include": []
|
||||
}
|
||||
+44
-3
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
@@ -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
Reference in New Issue
Block a user