From 3a78d6f7a443b7a32f4a025a2e704d767c8aaaf6 Mon Sep 17 00:00:00 2001 From: Olim Saidov Date: Wed, 14 Jan 2026 12:16:29 +0500 Subject: [PATCH] wip: add OpenTUI theme and CLI demo --- cli/README.md | 23 + cli/package.json | 31 + cli/src/css.d.ts | 1 + cli/src/index.tsx | 259 +++++ cli/tsconfig.json | 18 + package.json | 3 + packages/renderer/lib/ui/theme.tsx | 18 +- packages/renderer/tsconfig.json | 13 + pnpm-lock.yaml | 958 +++++++++++++++++- pnpm-workspace.yaml | 1 + .../questionnaire/questionnaire.stories.tsx | 5 + .../samples/answer-options-choice.json | 79 ++ themes/hs-theme/tsconfig.json | 13 + themes/opentui-theme/README.md | 33 + .../lib/components/action-button.tsx | 42 + .../lib/components/answer-list.tsx | 27 + .../lib/components/answer-scaffold.tsx | 36 + .../lib/components/checkbox-list.tsx | 68 ++ .../opentui-theme/lib/components/checkbox.tsx | 47 + .../lib/components/custom-option-form.tsx | 34 + .../lib/components/date-input.tsx | 31 + .../lib/components/date-time-input.tsx | 31 + .../lib/components/display-renderer.tsx | 12 + .../lib/components/error-summary.tsx | 33 + .../opentui-theme/lib/components/errors.tsx | 20 + .../lib/components/file-input.tsx | 28 + .../opentui-theme/lib/components/flyover.tsx | 6 + .../lib/components/focus-provider.tsx | 183 ++++ themes/opentui-theme/lib/components/form.tsx | 122 +++ .../lib/components/group-list.tsx | 29 + .../lib/components/group-scaffold.tsx | 32 + themes/opentui-theme/lib/components/help.tsx | 6 + .../lib/components/input-group.tsx | 25 + themes/opentui-theme/lib/components/label.tsx | 59 ++ themes/opentui-theme/lib/components/legal.tsx | 6 + themes/opentui-theme/lib/components/link.tsx | 12 + .../lib/components/multi-select-input.tsx | 100 ++ .../lib/components/number-input.tsx | 83 ++ .../lib/components/options-loading.tsx | 6 + .../lib/components/question-scaffold.tsx | 18 + .../lib/components/radio-button-list.tsx | 107 ++ .../lib/components/radio-button.tsx | 47 + .../lib/components/select-input.tsx | 138 +++ .../lib/components/slider-input.tsx | 93 ++ .../lib/components/spinner-input.tsx | 83 ++ themes/opentui-theme/lib/components/stack.tsx | 9 + .../lib/components/tab-container.tsx | 51 + themes/opentui-theme/lib/components/table.tsx | 71 ++ .../lib/components/text-area.tsx | 46 + .../lib/components/text-input.tsx | 31 + .../lib/components/time-input.tsx | 31 + .../lib/components/utilities.tsx | 130 +++ themes/opentui-theme/lib/index.ts | 3 + themes/opentui-theme/lib/style.css | 0 themes/opentui-theme/lib/theme.ts | 111 ++ themes/opentui-theme/package.json | 52 + themes/opentui-theme/tsconfig.json | 20 + themes/opentui-theme/tsconfig.lib.json | 8 + themes/opentui-theme/tsconfig.node.json | 4 + themes/opentui-theme/vite.config.ts | 56 + tsconfig.base.json | 1 + 61 files changed, 3638 insertions(+), 5 deletions(-) create mode 100644 cli/README.md create mode 100644 cli/package.json create mode 100644 cli/src/css.d.ts create mode 100644 cli/src/index.tsx create mode 100644 cli/tsconfig.json create mode 100644 site/stories/questionnaire/samples/answer-options-choice.json create mode 100644 themes/opentui-theme/README.md create mode 100644 themes/opentui-theme/lib/components/action-button.tsx create mode 100644 themes/opentui-theme/lib/components/answer-list.tsx create mode 100644 themes/opentui-theme/lib/components/answer-scaffold.tsx create mode 100644 themes/opentui-theme/lib/components/checkbox-list.tsx create mode 100644 themes/opentui-theme/lib/components/checkbox.tsx create mode 100644 themes/opentui-theme/lib/components/custom-option-form.tsx create mode 100644 themes/opentui-theme/lib/components/date-input.tsx create mode 100644 themes/opentui-theme/lib/components/date-time-input.tsx create mode 100644 themes/opentui-theme/lib/components/display-renderer.tsx create mode 100644 themes/opentui-theme/lib/components/error-summary.tsx create mode 100644 themes/opentui-theme/lib/components/errors.tsx create mode 100644 themes/opentui-theme/lib/components/file-input.tsx create mode 100644 themes/opentui-theme/lib/components/flyover.tsx create mode 100644 themes/opentui-theme/lib/components/focus-provider.tsx create mode 100644 themes/opentui-theme/lib/components/form.tsx create mode 100644 themes/opentui-theme/lib/components/group-list.tsx create mode 100644 themes/opentui-theme/lib/components/group-scaffold.tsx create mode 100644 themes/opentui-theme/lib/components/help.tsx create mode 100644 themes/opentui-theme/lib/components/input-group.tsx create mode 100644 themes/opentui-theme/lib/components/label.tsx create mode 100644 themes/opentui-theme/lib/components/legal.tsx create mode 100644 themes/opentui-theme/lib/components/link.tsx create mode 100644 themes/opentui-theme/lib/components/multi-select-input.tsx create mode 100644 themes/opentui-theme/lib/components/number-input.tsx create mode 100644 themes/opentui-theme/lib/components/options-loading.tsx create mode 100644 themes/opentui-theme/lib/components/question-scaffold.tsx create mode 100644 themes/opentui-theme/lib/components/radio-button-list.tsx create mode 100644 themes/opentui-theme/lib/components/radio-button.tsx create mode 100644 themes/opentui-theme/lib/components/select-input.tsx create mode 100644 themes/opentui-theme/lib/components/slider-input.tsx create mode 100644 themes/opentui-theme/lib/components/spinner-input.tsx create mode 100644 themes/opentui-theme/lib/components/stack.tsx create mode 100644 themes/opentui-theme/lib/components/tab-container.tsx create mode 100644 themes/opentui-theme/lib/components/table.tsx create mode 100644 themes/opentui-theme/lib/components/text-area.tsx create mode 100644 themes/opentui-theme/lib/components/text-input.tsx create mode 100644 themes/opentui-theme/lib/components/time-input.tsx create mode 100644 themes/opentui-theme/lib/components/utilities.tsx create mode 100644 themes/opentui-theme/lib/index.ts create mode 100644 themes/opentui-theme/lib/style.css create mode 100644 themes/opentui-theme/lib/theme.ts create mode 100644 themes/opentui-theme/package.json create mode 100644 themes/opentui-theme/tsconfig.json create mode 100644 themes/opentui-theme/tsconfig.lib.json create mode 100644 themes/opentui-theme/tsconfig.node.json create mode 100644 themes/opentui-theme/vite.config.ts diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..284cc7f6 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,23 @@ +# @aidbox-forms/cli + +Terminal demo app for `@aidbox-forms/renderer` using `@aidbox-forms/opentui-theme`. + +## Run + +```bash +pnpm -C cli dev -- --questionnaire site/stories/questionnaire/samples/text-controls.json --output response.json +``` + +On submit, the app exits and writes a `QuestionnaireResponse` JSON to `--output`. +If `--output` is omitted, it prints the response JSON to stdout and exits. + +### Flags + +- `--questionnaire ` (required) +- `--initial-response ` +- `--terminology-server-url ` +- `--output ` + +## Notes + +- Attachment upload is not supported in TUI (TBD). diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 00000000..145a6957 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,31 @@ +{ + "name": "@aidbox-forms/cli", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "bun src/index.tsx", + "lint": "eslint .", + "test": "echo \"@aidbox-forms/cli: no tests defined\"" + }, + "dependencies": { + "@aidbox-forms/opentui-theme": "workspace:*", + "@aidbox-forms/renderer": "workspace:*", + "@lhncbc/ucum-lhc": "7.1.3", + "@opentui/core": "^0.1.72", + "@opentui/react": "^0.1.72", + "classnames": "^2.5.1", + "fhirpath": "^4.6.0", + "mobx": "^6.15.0", + "mobx-react-lite": "^4.1.1", + "mobx-utils": "^6.1.1", + "react": "^19.2.0" + }, + "devDependencies": { + "@types/bun": "^1.2.22", + "@types/fhir": "^0.0.41", + "@types/node": "^24.10.3", + "@types/react": "^19.2.2", + "typescript": "~5.9.3" + } +} diff --git a/cli/src/css.d.ts b/cli/src/css.d.ts new file mode 100644 index 00000000..cbe652db --- /dev/null +++ b/cli/src/css.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/cli/src/index.tsx b/cli/src/index.tsx new file mode 100644 index 00000000..4deb634a --- /dev/null +++ b/cli/src/index.tsx @@ -0,0 +1,259 @@ +import Renderer from "@aidbox-forms/renderer"; +import { + Provider as FocusProvider, + theme as opentuiTheme, +} from "@aidbox-forms/opentui-theme"; +import { createCliRenderer } from "@opentui/core"; +import { createRoot, useKeyboard, useRenderer } from "@opentui/react"; +import { readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; +import type { Questionnaire, QuestionnaireResponse } from "fhir/r5"; +import { useCallback, useEffect, useMemo } from "react"; + +type CliOptions = { + questionnairePath: string; + initialResponsePath: string | undefined; + terminologyServerUrl: string | undefined; + outputPath: string | undefined; +}; + +type CompletionState = + | { status: "submit"; response: QuestionnaireResponse } + | { status: "exit" }; + +type CompletionHandler = (state: CompletionState) => void; + +type Deferred = { + promise: Promise; + resolve: (value: T) => void; + reject: (reason?: unknown) => void; +}; + +function createDeferred(): Deferred { + let resolve: ((value: T) => void) | undefined; + let reject: ((reason?: unknown) => void) | undefined; + + const promise = new Promise((resolveFunction, rejectFunction) => { + resolve = resolveFunction; + reject = rejectFunction; + }); + + if (!resolve || !reject) { + throw new Error("Failed to create deferred promise."); + } + + return { promise, resolve, reject }; +} + +function usage(): string { + return `Usage: + pnpm -C cli dev -- --questionnaire [--initial-response ] [--terminology-server-url ] [--output ] + +Flags: + --questionnaire Path to Questionnaire JSON (required) + --initial-response Path to QuestionnaireResponse JSON + --terminology-server-url FHIR terminology server base URL + --output Write response JSON to this file on submit +`; +} + +function findAttachmentItems(items: unknown): boolean { + if (!Array.isArray(items)) return false; + + for (const entry of items) { + if (!entry || typeof entry !== "object") continue; + + const item = entry as { type?: unknown; item?: unknown }; + + if (item.type === "attachment") { + return true; + } + + if (findAttachmentItems(item.item)) { + return true; + } + } + + return false; +} + +async function readJsonFile(filePath: string): Promise { + const text = await readFile(filePath, "utf8"); + return JSON.parse(text) as T; +} + +function stringifyPretty(value: unknown): string { + return JSON.stringify(value, undefined, 2); +} + +function App({ + questionnaire, + initialResponse, + terminologyServerUrl, + hasAttachments, + onComplete, +}: { + questionnaire: Questionnaire; + initialResponse: QuestionnaireResponse | undefined; + terminologyServerUrl: string | undefined; + hasAttachments: boolean; + onComplete: CompletionHandler; +}) { + const renderer = useRenderer(); + + useEffect(() => { + if (!hasAttachments) return; + + renderer.console.show(); + console.warn( + "This Questionnaire contains attachment items. Upload is not supported in the TUI (TBD).", + ); + }, [hasAttachments, renderer]); + + useKeyboard((key) => { + if (key.eventType !== "press") return; + + if (key.name === "escape") { + key.preventDefault(); + key.stopPropagation(); + onComplete({ status: "exit" }); + } + }); + + const onSubmit = useCallback( + (response: QuestionnaireResponse) => { + onComplete({ status: "submit", response }); + }, + [onComplete], + ); + + const header = useMemo(() => { + return ( + + + Aidbox Forms TUI (OpenTUI) + + Tab navigate • Ctrl+S submit • Esc quit + + ); + }, []); + + return ( + + + {header} + + + + ); +} + +const moduleFilePath = fileURLToPath(import.meta.url); +const moduleDirectoryPath = path.dirname(moduleFilePath); +const workspaceRootPath = path.resolve(moduleDirectoryPath, "..", ".."); +if (process.cwd() !== workspaceRootPath) { + process.chdir(workspaceRootPath); +} + +const { values } = parseArgs({ + options: { + questionnaire: { type: "string" }, + "initial-response": { type: "string" }, + "terminology-server-url": { type: "string" }, + output: { type: "string" }, + help: { type: "boolean" }, + }, + allowPositionals: false, +}); + +if (values.help) { + console.log(usage()); +} else if (values.questionnaire) { + const options: CliOptions = { + questionnairePath: path.resolve(process.cwd(), values.questionnaire), + initialResponsePath: values["initial-response"] + ? path.resolve(process.cwd(), values["initial-response"]) + : undefined, + terminologyServerUrl: values["terminology-server-url"], + outputPath: values.output + ? path.resolve(process.cwd(), values.output) + : undefined, + }; + + const questionnaire = await readJsonFile( + options.questionnairePath, + ); + const initialResponse = options.initialResponsePath + ? await readJsonFile(options.initialResponsePath) + : undefined; + + const hasAttachments = findAttachmentItems(questionnaire.item); + + if (hasAttachments) { + console.warn( + "Warning: attachment items detected; upload is not supported in the TUI (TBD).", + ); + } + + const renderer = await createCliRenderer(); + + const completion = createDeferred(); + let isCompleted = false; + + const completeOnce: CompletionHandler = (state) => { + if (isCompleted) return; + isCompleted = true; + completion.resolve(state); + }; + + renderer.once("destroy", () => { + completeOnce({ status: "exit" }); + }); + + const root = createRoot(renderer); + root.render( + , + ); + + const completionState = await completion.promise; + + renderer.destroy(); + + if (completionState.status === "submit") { + const serialized = stringifyPretty(completionState.response); + + try { + if (options.outputPath) { + await writeFile(options.outputPath, serialized, "utf8"); + } else { + process.stdout.write(`${serialized}\n`); + } + + process.exitCode = 0; + } catch (error) { + console.error(error); + process.exitCode = 1; + } + } else { + process.exitCode = 0; + } +} else { + console.error("Missing required flag: --questionnaire"); + console.log(usage()); + process.exitCode = 1; +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 00000000..305253b7 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "jsxImportSource": "@opentui/react", + "baseUrl": "..", + "paths": { + "@aidbox-forms/theme": ["packages/theme/lib"], + "@aidbox-forms/antd-theme": ["themes/antd-theme/lib"], + "@aidbox-forms/mantine-theme": ["themes/mantine-theme/lib"], + "@aidbox-forms/hs-theme": ["themes/hs-theme/lib"], + "@aidbox-forms/nshuk-theme": ["themes/nshuk-theme/lib"], + "@aidbox-forms/opentui-theme": ["themes/opentui-theme/lib"], + "@aidbox-forms/renderer": ["packages/renderer/lib"], + "@aidbox-forms/renderer/*": ["packages/renderer/lib/*"] + } + }, + "include": ["src/**/*"] +} diff --git a/package.json b/package.json index 17361b3b..8dd84678 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,11 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", + "@opentui/react": "^0.1.72", "@types/node": "^25.0.7", + "@types/react": "^19.2.2", "eslint": "^9.39.2", + "react": "^19.2.0", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-storybook": "^10.1.11", diff --git a/packages/renderer/lib/ui/theme.tsx b/packages/renderer/lib/ui/theme.tsx index 2a72cca8..1c6bf627 100644 --- a/packages/renderer/lib/ui/theme.tsx +++ b/packages/renderer/lib/ui/theme.tsx @@ -1,16 +1,26 @@ /* eslint-disable react-refresh/only-export-components */ -import { theme } from "@aidbox-forms/hs-theme"; import type { Theme } from "@aidbox-forms/theme"; import { createContext, type PropsWithChildren, useContext } from "react"; -const ThemeContext = createContext(theme); +const fallbackTheme = {} as Theme; + +let defaultTheme: Theme = fallbackTheme; + +try { + const module = await import("@aidbox-forms/hs-theme"); + defaultTheme = module.theme; +} catch { + defaultTheme = fallbackTheme; +} + +const ThemeContext = createContext(defaultTheme); export function ThemeProvider({ - theme: providedTheme = theme, + theme, children, }: PropsWithChildren<{ theme?: Theme | undefined }>) { return ( - + {children} ); diff --git a/packages/renderer/tsconfig.json b/packages/renderer/tsconfig.json index b74c46c0..7adb38dd 100644 --- a/packages/renderer/tsconfig.json +++ b/packages/renderer/tsconfig.json @@ -1,4 +1,17 @@ { + "compilerOptions": { + "baseUrl": "../..", + "paths": { + "@aidbox-forms/theme": ["packages/theme/lib"], + "@aidbox-forms/antd-theme": ["themes/antd-theme/lib"], + "@aidbox-forms/mantine-theme": ["themes/mantine-theme/lib"], + "@aidbox-forms/hs-theme": ["themes/hs-theme/lib"], + "@aidbox-forms/nshuk-theme": ["themes/nshuk-theme/lib"], + "@aidbox-forms/opentui-theme": ["themes/opentui-theme/lib"], + "@aidbox-forms/renderer": ["packages/renderer/lib"], + "@aidbox-forms/renderer/*": ["packages/renderer/lib/*"] + } + }, "files": [], "references": [ { "path": "./tsconfig.lib.json" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 741ee864..f3013ebe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,15 @@ importers: '@eslint/js': specifier: ^9.39.2 version: 9.39.2 + '@opentui/react': + specifier: ^0.1.72 + version: 0.1.72(react-devtools-core@7.0.1)(react@19.2.3)(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10)(ws@8.19.0) '@types/node': specifier: ^25.0.7 version: 25.0.7 + '@types/react': + specifier: ^19.2.2 + version: 19.2.8 eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -50,6 +56,9 @@ importers: prettier: specifier: ^3.7.4 version: 3.7.4 + react: + specifier: 19.2.3 + version: 19.2.3 typescript: specifier: ~5.9.3 version: 5.9.3 @@ -57,6 +66,58 @@ importers: specifier: ^8.53.0 version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + cli: + dependencies: + '@aidbox-forms/opentui-theme': + specifier: workspace:* + version: link:../themes/opentui-theme + '@aidbox-forms/renderer': + specifier: workspace:* + version: link:../packages/renderer + '@lhncbc/ucum-lhc': + specifier: 7.1.3 + version: 7.1.3(patch_hash=3179673f53a760a3c5663751809c2249ed644eea25ed87626d45d753f6c0b11c) + '@opentui/core': + specifier: ^0.1.72 + version: 0.1.72(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/react': + specifier: ^0.1.72 + version: 0.1.72(react-devtools-core@7.0.1)(react@19.2.3)(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10)(ws@8.19.0) + classnames: + specifier: ^2.5.1 + version: 2.5.1 + fhirpath: + specifier: ^4.6.0 + version: 4.8.2 + mobx: + specifier: ^6.15.0 + version: 6.15.0 + mobx-react-lite: + specifier: ^4.1.1 + version: 4.1.1(mobx@6.15.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + mobx-utils: + specifier: ^6.1.1 + version: 6.1.1(mobx@6.15.0) + react: + specifier: 19.2.3 + version: 19.2.3 + devDependencies: + '@types/bun': + specifier: ^1.2.22 + version: 1.3.6 + '@types/fhir': + specifier: ^0.0.41 + version: 0.0.41 + '@types/node': + specifier: ^24.10.3 + version: 24.10.8 + '@types/react': + specifier: ^19.2.2 + version: 19.2.8 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + packages/renderer: dependencies: '@aidbox-forms/nshuk-theme': @@ -417,6 +478,40 @@ importers: specifier: ^4.5.4 version: 4.5.4(@types/node@25.0.7)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + themes/opentui-theme: + dependencies: + '@aidbox-forms/theme': + specifier: workspace:* + version: link:../../packages/theme + '@opentui/core': + specifier: ^0.1.72 + version: 0.1.72(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/react': + specifier: ^0.1.72 + version: 0.1.72(react-devtools-core@7.0.1)(react@19.2.3)(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10)(ws@8.19.0) + devDependencies: + '@types/bun': + specifier: ^1.2.22 + version: 1.3.6 + '@types/react': + specifier: ^19.2.2 + version: 19.2.8 + '@vitejs/plugin-react': + specifier: ^5.1.0 + version: 5.1.2(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + react: + specifier: 19.2.3 + version: 19.2.3 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.1.12 + version: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(@types/node@25.0.7)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + packages: '@acemir/cssom@0.9.31': @@ -715,6 +810,9 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@dimforge/rapier2d-simd-compat@0.17.3': + resolution: {integrity: sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg==} + '@emotion/hash@0.8.0': resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} @@ -1141,6 +1239,118 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jimp/core@1.6.0': + resolution: {integrity: sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==} + engines: {node: '>=18'} + + '@jimp/diff@1.6.0': + resolution: {integrity: sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw==} + engines: {node: '>=18'} + + '@jimp/file-ops@1.6.0': + resolution: {integrity: sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ==} + engines: {node: '>=18'} + + '@jimp/js-bmp@1.6.0': + resolution: {integrity: sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw==} + engines: {node: '>=18'} + + '@jimp/js-gif@1.6.0': + resolution: {integrity: sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g==} + engines: {node: '>=18'} + + '@jimp/js-jpeg@1.6.0': + resolution: {integrity: sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA==} + engines: {node: '>=18'} + + '@jimp/js-png@1.6.0': + resolution: {integrity: sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg==} + engines: {node: '>=18'} + + '@jimp/js-tiff@1.6.0': + resolution: {integrity: sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw==} + engines: {node: '>=18'} + + '@jimp/plugin-blit@1.6.0': + resolution: {integrity: sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA==} + engines: {node: '>=18'} + + '@jimp/plugin-blur@1.6.0': + resolution: {integrity: sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw==} + engines: {node: '>=18'} + + '@jimp/plugin-circle@1.6.0': + resolution: {integrity: sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw==} + engines: {node: '>=18'} + + '@jimp/plugin-color@1.6.0': + resolution: {integrity: sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA==} + engines: {node: '>=18'} + + '@jimp/plugin-contain@1.6.0': + resolution: {integrity: sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ==} + engines: {node: '>=18'} + + '@jimp/plugin-cover@1.6.0': + resolution: {integrity: sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA==} + engines: {node: '>=18'} + + '@jimp/plugin-crop@1.6.0': + resolution: {integrity: sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang==} + engines: {node: '>=18'} + + '@jimp/plugin-displace@1.6.0': + resolution: {integrity: sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q==} + engines: {node: '>=18'} + + '@jimp/plugin-dither@1.6.0': + resolution: {integrity: sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ==} + engines: {node: '>=18'} + + '@jimp/plugin-fisheye@1.6.0': + resolution: {integrity: sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA==} + engines: {node: '>=18'} + + '@jimp/plugin-flip@1.6.0': + resolution: {integrity: sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg==} + engines: {node: '>=18'} + + '@jimp/plugin-hash@1.6.0': + resolution: {integrity: sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q==} + engines: {node: '>=18'} + + '@jimp/plugin-mask@1.6.0': + resolution: {integrity: sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA==} + engines: {node: '>=18'} + + '@jimp/plugin-print@1.6.0': + resolution: {integrity: sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A==} + engines: {node: '>=18'} + + '@jimp/plugin-quantize@1.6.0': + resolution: {integrity: sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg==} + engines: {node: '>=18'} + + '@jimp/plugin-resize@1.6.0': + resolution: {integrity: sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==} + engines: {node: '>=18'} + + '@jimp/plugin-rotate@1.6.0': + resolution: {integrity: sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw==} + engines: {node: '>=18'} + + '@jimp/plugin-threshold@1.6.0': + resolution: {integrity: sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w==} + engines: {node: '>=18'} + + '@jimp/types@1.6.0': + resolution: {integrity: sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg==} + engines: {node: '>=18'} + + '@jimp/utils@1.6.0': + resolution: {integrity: sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==} + engines: {node: '>=18'} + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.3': resolution: {integrity: sha512-9TGZuAX+liGkNKkwuo3FYJu7gHWT0vkBcf7GkOe7s7fmC19XwH/4u5u7sDIFrMooe558ORcmuBvBz7Ur5PlbHw==} peerDependencies: @@ -1216,6 +1426,48 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@opentui/core-darwin-arm64@0.1.72': + resolution: {integrity: sha512-RoU48kOrhLZYDBiXaDu1LXS2bwRdlJlFle8eUQiqJjLRbMIY34J/srBuL0JnAS3qKW4J34NepUQa0l0/S43Q3w==} + cpu: [arm64] + os: [darwin] + + '@opentui/core-darwin-x64@0.1.72': + resolution: {integrity: sha512-hHUQw8i2LWPToRW1rjAiRqmNf34iJPS9ve9CJDygvFs5JOqUxN5yrfLfKfE+1bQjfFDHnpqW1HUk96iLhkPj8Q==} + cpu: [x64] + os: [darwin] + + '@opentui/core-linux-arm64@0.1.72': + resolution: {integrity: sha512-63yml0OQ8tVa0JuDF9lBAWiChX6Q+iDO7lKv7c2n0352n/WyPr3iAgq4uSoH49HXuKeAXY/VwHGjvPzjXD/SDA==} + cpu: [arm64] + os: [linux] + + '@opentui/core-linux-x64@0.1.72': + resolution: {integrity: sha512-51veiQXNLvzDsFzsEvt71uK7WhiRe2DnvlJSGBSe6aRRHHxjCFYHzYi7t6bitJqtDTUj+EaMPbH81oZ6xy7tyg==} + cpu: [x64] + os: [linux] + + '@opentui/core-win32-arm64@0.1.72': + resolution: {integrity: sha512-1Ep6OcaYTy1RlLOln+LNN7DL1iNyLwLjG2M8aO0pVJKFvxeD5P7rdRzY065E4uhkHeJIHuduUqxvUjD0dyuwbw==} + cpu: [arm64] + os: [win32] + + '@opentui/core-win32-x64@0.1.72': + resolution: {integrity: sha512-5QUv91UkOINlkEaPky3kaxmJvshcJMBAX7LZtIroduaKBGpWRA1aogNhPZzp+30WkvgOU7aOtUktAZuFXb9WdQ==} + cpu: [x64] + os: [win32] + + '@opentui/core@0.1.72': + resolution: {integrity: sha512-l4WQzubBJ80Q0n77Lxuodjwwm8qj/sOa7IXxEAzzDDXY/7bsIhdSpVhRTt+KevBRlok5J+w/KMKYr8UzkA4/hA==} + peerDependencies: + web-tree-sitter: 0.25.10 + + '@opentui/react@0.1.72': + resolution: {integrity: sha512-pL1qeByRTamjA3mCfbwagaaw7mRXKDSXQDPYO14s65dILzZ6k+qZlsa6rLHfTNQ3dDb5zKJyVDdyn5z59Pv4Pg==} + peerDependencies: + react: 19.2.3 + react-devtools-core: ^7.0.1 + ws: ^8.18.0 + '@rc-component/async-validator@5.1.0': resolution: {integrity: sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==} engines: {node: '>=14.x'} @@ -1850,6 +2102,9 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -1868,6 +2123,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bun@1.3.6': + resolution: {integrity: sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1892,9 +2150,15 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/node@16.9.1': + resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} + '@types/node@20.19.28': resolution: {integrity: sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==} + '@types/node@24.10.8': + resolution: {integrity: sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ==} + '@types/node@25.0.7': resolution: {integrity: sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==} @@ -2061,6 +2325,9 @@ packages: '@vue/shared@3.5.25': resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + '@webgpu/types@0.1.69': + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + '@wyw-in-js/processor-utils@0.6.0': resolution: {integrity: sha512-5YAZMUmF+S2HaqheKfew6ybbYBMnF10PjIgI7ieyuFxCohyqJNF4xdo6oHftv2z5Z4vCQ0OZHtDOQyDImBYwmg==} engines: {node: '>=16.0.0'} @@ -2087,6 +2354,10 @@ packages: peerDependencies: vite: '>=3.2.7' + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2163,6 +2434,9 @@ packages: resolution: {integrity: sha512-qNy2odgsa0skmNMCuxzXhM4M8J1YDaPv3TI+vCdnOAanu0N982wBrSqziDKRDctEZLZy9VffqIZXc0UGjjSP/g==} engines: {node: '>=14'} + any-base@1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -2184,9 +2458,16 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + await-to-js@3.0.0: + resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} + engines: {node: '>=6.0.0'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.6: resolution: {integrity: sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==} hasBin: true @@ -2194,6 +2475,9 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bmp-ts@1.0.9: + resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2209,10 +2493,44 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builtin-modules@5.0.0: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} + bun-ffi-structs@0.1.2: + resolution: {integrity: sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w==} + peerDependencies: + typescript: ^5 + + bun-types@1.3.6: + resolution: {integrity: sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ==} + + bun-webgpu-darwin-arm64@0.1.4: + resolution: {integrity: sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg==} + cpu: [arm64] + os: [darwin] + + bun-webgpu-darwin-x64@0.1.4: + resolution: {integrity: sha512-X+PjwJUWenUmdQBP8EtdItMyieQ6Nlpn+BH518oaouDiSnWj5+b0Y7DNDZJq7Ezom4EaxmqL/uGYZK3aCQ7CXg==} + cpu: [x64] + os: [darwin] + + bun-webgpu-linux-x64@0.1.4: + resolution: {integrity: sha512-zMLs2YIGB+/jxrYFXaFhVKX/GBt05UTF45lc9srcHc9JXGjEj+12CIo1CHLTAWatXMTqt0Jsu6ukWEoWVT/ayA==} + cpu: [x64] + os: [linux] + + bun-webgpu-win32-x64@0.1.4: + resolution: {integrity: sha512-Z5yAK28xrcm8Wb5k7TZ8FJKpOI/r+aVCRdlHYAqI2SDJFN3nD4mJs900X6kNVmG/xFzb5yOuKVYWGg+6ZXWbyA==} + cpu: [x64] + os: [win32] + + bun-webgpu@0.1.4: + resolution: {integrity: sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -2563,9 +2881,20 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2608,6 +2937,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2658,6 +2991,9 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + gifwrap@0.10.1: + resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -2731,6 +3067,9 @@ packages: engines: {node: '>=18'} hasBin: true + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2739,6 +3078,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + image-q@4.0.0: + resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2834,6 +3176,10 @@ packages: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} + jimp@1.6.0: + resolution: {integrity: sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==} + engines: {node: '>=18'} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -2841,6 +3187,9 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3040,6 +3389,11 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -3125,6 +3479,9 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -3148,10 +3505,22 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + + parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + + parse-bmfont-xml@1.1.6: + resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -3188,6 +3557,10 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3204,16 +3577,34 @@ packages: engines: {node: '>=0.10'} hasBin: true + pixelmatch@5.3.0: + resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} + hasBin: true + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + planck@1.4.2: + resolution: {integrity: sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew==} + engines: {node: '>=14.0'} + peerDependencies: + stage-js: ^1.0.0-alpha.12 + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + pngjs@6.0.0: + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} + engines: {node: '>=12.13.0'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -3234,6 +3625,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3245,6 +3640,9 @@ packages: resolution: {integrity: sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==} engines: {node: '>= 0.10.0'} + react-devtools-core@7.0.1: + resolution: {integrity: sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw==} + react-docgen-typescript@2.4.0: resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} peerDependencies: @@ -3274,6 +3672,12 @@ packages: react: 19.2.3 react-dom: 19.2.3 + react-reconciler@0.32.0: + resolution: {integrity: sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: 19.2.3 + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -3321,6 +3725,14 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -3369,6 +3781,9 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + sax@1.1.6: resolution: {integrity: sha512-8zci48uUQyfqynGDSkUMD7FCJB96hwLnlZOXlgs1l3TX+LW27t3psSWKUxC0fxVgA86i8tL4NwGcY1h/6t3ESg==} @@ -3376,6 +3791,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -3404,6 +3822,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3411,6 +3833,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-xml-to-json@1.2.3: + resolution: {integrity: sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==} + engines: {node: '>=20.12.2'} + slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} @@ -3433,6 +3859,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stage-js@1.0.0-alpha.17: + resolution: {integrity: sha512-AzlMO+t51v6cFvKZ+Oe9DJnL1OXEH5s9bEy6di5aOrUpcP7PCzI/wIeXF0u3zg0L89gwnceoKxrLId0ZpYnNXw==} + engines: {node: '>=18.0'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -3480,6 +3910,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3504,6 +3937,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} @@ -3532,6 +3969,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + three@0.177.0: + resolution: {integrity: sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} @@ -3542,6 +3982,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -3573,6 +4016,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -3705,6 +4152,9 @@ packages: peerDependencies: react: 19.2.3 + utif2@4.1.0: + resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3798,6 +4248,14 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + web-tree-sitter@0.25.10: + resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==} + peerDependencies: + '@types/emscripten': ^1.40.0 + peerDependenciesMeta: + '@types/emscripten': + optional: true + webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -3843,6 +4301,18 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -3875,6 +4345,17 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -3896,12 +4377,18 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} @@ -4310,6 +4797,9 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@dimforge/rapier2d-simd-compat@0.17.3': + optional: true + '@emotion/hash@0.8.0': {} '@emotion/is-prop-valid@1.4.0': @@ -4580,6 +5070,195 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jimp/core@1.6.0': + dependencies: + '@jimp/file-ops': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + await-to-js: 3.0.0 + exif-parser: 0.1.12 + file-type: 16.5.4 + mime: 3.0.0 + + '@jimp/diff@1.6.0': + dependencies: + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + pixelmatch: 5.3.0 + + '@jimp/file-ops@1.6.0': {} + + '@jimp/js-bmp@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + bmp-ts: 1.0.9 + + '@jimp/js-gif@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + gifwrap: 0.10.1 + omggif: 1.0.10 + + '@jimp/js-jpeg@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + jpeg-js: 0.4.4 + + '@jimp/js-png@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + pngjs: 7.0.0 + + '@jimp/js-tiff@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + utif2: 4.1.0 + + '@jimp/plugin-blit@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-blur@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/utils': 1.6.0 + + '@jimp/plugin-circle@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-color@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + tinycolor2: 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-contain@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-cover@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-crop@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-displace@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-dither@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + + '@jimp/plugin-fisheye@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-flip@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-hash@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/js-bmp': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/js-tiff': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + any-base: 1.1.0 + + '@jimp/plugin-mask@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-print@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/types': 1.6.0 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.6 + simple-xml-to-json: 1.2.3 + zod: 3.25.76 + + '@jimp/plugin-quantize@1.6.0': + dependencies: + image-q: 4.0.0 + zod: 3.25.76 + + '@jimp/plugin-resize@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-rotate@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-threshold@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-hash': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/types@1.6.0': + dependencies: + zod: 3.25.76 + + '@jimp/utils@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + tinycolor2: 1.6.0 + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.3(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': dependencies: glob: 11.1.0 @@ -4714,6 +5393,58 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@opentui/core-darwin-arm64@0.1.72': + optional: true + + '@opentui/core-darwin-x64@0.1.72': + optional: true + + '@opentui/core-linux-arm64@0.1.72': + optional: true + + '@opentui/core-linux-x64@0.1.72': + optional: true + + '@opentui/core-win32-arm64@0.1.72': + optional: true + + '@opentui/core-win32-x64@0.1.72': + optional: true + + '@opentui/core@0.1.72(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10)': + dependencies: + bun-ffi-structs: 0.1.2(typescript@5.9.3) + diff: 8.0.2 + jimp: 1.6.0 + web-tree-sitter: 0.25.10 + yoga-layout: 3.2.1 + optionalDependencies: + '@dimforge/rapier2d-simd-compat': 0.17.3 + '@opentui/core-darwin-arm64': 0.1.72 + '@opentui/core-darwin-x64': 0.1.72 + '@opentui/core-linux-arm64': 0.1.72 + '@opentui/core-linux-x64': 0.1.72 + '@opentui/core-win32-arm64': 0.1.72 + '@opentui/core-win32-x64': 0.1.72 + bun-webgpu: 0.1.4 + planck: 1.4.2(stage-js@1.0.0-alpha.17) + three: 0.177.0 + transitivePeerDependencies: + - stage-js + - typescript + + '@opentui/react@0.1.72(react-devtools-core@7.0.1)(react@19.2.3)(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10)(ws@8.19.0)': + dependencies: + '@opentui/core': 0.1.72(stage-js@1.0.0-alpha.17)(typescript@5.9.3)(web-tree-sitter@0.25.10) + react: 19.2.3 + react-devtools-core: 7.0.1 + react-reconciler: 0.32.0(react@19.2.3) + ws: 8.19.0 + transitivePeerDependencies: + - stage-js + - typescript + - web-tree-sitter + '@rc-component/async-validator@5.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -5377,6 +6108,8 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@tokenizer/token@0.3.0': {} + '@types/argparse@1.0.38': {} '@types/aria-query@5.0.4': {} @@ -5402,6 +6135,10 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/bun@1.3.6': + dependencies: + bun-types: 1.3.6 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5421,10 +6158,16 @@ snapshots: '@types/mdx@2.0.13': {} + '@types/node@16.9.1': {} + '@types/node@20.19.28': dependencies: undici-types: 6.21.0 + '@types/node@24.10.8': + dependencies: + undici-types: 7.16.0 + '@types/node@25.0.7': dependencies: undici-types: 7.16.0 @@ -5443,7 +6186,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.28 + '@types/node': 25.0.7 '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: @@ -5662,6 +6405,9 @@ snapshots: '@vue/shared@3.5.25': {} + '@webgpu/types@0.1.69': + optional: true + '@wyw-in-js/processor-utils@0.6.0': dependencies: '@babel/generator': 7.28.5 @@ -5725,6 +6471,10 @@ snapshots: - typescript - utf-8-validate + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5839,6 +6589,8 @@ snapshots: antlr4@4.9.3: {} + any-base@1.1.0: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -5857,14 +6609,20 @@ snapshots: dependencies: tslib: 2.8.1 + await-to-js@3.0.0: {} + balanced-match@1.0.2: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.9.6: {} bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 + bmp-ts@1.0.9: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5886,8 +6644,43 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.2(browserslist@4.28.1) + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + builtin-modules@5.0.0: {} + bun-ffi-structs@0.1.2(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + bun-types@1.3.6: + dependencies: + '@types/node': 24.10.8 + + bun-webgpu-darwin-arm64@0.1.4: + optional: true + + bun-webgpu-darwin-x64@0.1.4: + optional: true + + bun-webgpu-linux-x64@0.1.4: + optional: true + + bun-webgpu-win32-x64@0.1.4: + optional: true + + bun-webgpu@0.1.4: + dependencies: + '@webgpu/types': 0.1.69 + optionalDependencies: + bun-webgpu-darwin-arm64: 0.1.4 + bun-webgpu-darwin-x64: 0.1.4 + bun-webgpu-linux-x64: 0.1.4 + bun-webgpu-win32-x64: 0.1.4 + optional: true + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -6272,8 +7065,14 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + eventemitter3@5.0.1: {} + events@3.3.0: {} + + exif-parser@0.1.12: {} + expect-type@1.3.0: {} exsolve@1.0.8: {} @@ -6305,6 +7104,12 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 6.3.0 + token-types: 4.2.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -6347,6 +7152,11 @@ snapshots: get-nonce@1.0.1: {} + gifwrap@0.10.1: + dependencies: + image-q: 4.0.0 + omggif: 1.0.10 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -6423,10 +7233,16 @@ snapshots: husky@9.1.7: {} + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} + image-q@4.0.0: + dependencies: + '@types/node': 16.9.1 + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -6496,10 +7312,42 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 + jimp@1.6.0: + dependencies: + '@jimp/core': 1.6.0 + '@jimp/diff': 1.6.0 + '@jimp/js-bmp': 1.6.0 + '@jimp/js-gif': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/js-tiff': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/plugin-blur': 1.6.0 + '@jimp/plugin-circle': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-contain': 1.6.0 + '@jimp/plugin-cover': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-displace': 1.6.0 + '@jimp/plugin-dither': 1.6.0 + '@jimp/plugin-fisheye': 1.6.0 + '@jimp/plugin-flip': 1.6.0 + '@jimp/plugin-hash': 1.6.0 + '@jimp/plugin-mask': 1.6.0 + '@jimp/plugin-print': 1.6.0 + '@jimp/plugin-quantize': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/plugin-rotate': 1.6.0 + '@jimp/plugin-threshold': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + jiti@2.6.1: {} jju@1.4.0: {} + jpeg-js@0.4.4: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -6696,6 +7544,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime@3.0.0: {} + mimic-function@5.0.1: {} min-indent@1.0.1: {} @@ -6759,6 +7609,8 @@ snapshots: obug@2.1.1: {} + omggif@1.0.10: {} + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -6789,10 +7641,21 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-bmfont-ascii@1.0.6: {} + + parse-bmfont-binary@1.0.6: {} + + parse-bmfont-xml@1.1.6: + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.5.0 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.28.6 @@ -6823,6 +7686,8 @@ snapshots: pathval@2.0.1: {} + peek-readable@4.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -6831,6 +7696,10 @@ snapshots: pidtree@0.6.0: {} + pixelmatch@5.3.0: + dependencies: + pngjs: 6.0.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -6843,8 +7712,17 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + planck@1.4.2(stage-js@1.0.0-alpha.17): + dependencies: + stage-js: 1.0.0-alpha.17 + optional: true + pluralize@8.0.0: {} + pngjs@6.0.0: {} + + pngjs@7.0.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -6863,6 +7741,8 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + punycode@2.3.1: {} quansync@0.2.11: {} @@ -6873,6 +7753,14 @@ snapshots: kind-of: 6.0.3 math-random: 1.0.4 + react-devtools-core@7.0.1: + dependencies: + shell-quote: 1.8.3 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + react-docgen-typescript@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -6910,6 +7798,11 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + react-reconciler@0.32.0(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.26.0 + react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3): @@ -6960,6 +7853,18 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -7031,12 +7936,16 @@ snapshots: safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + sax@1.1.6: {} saxes@6.0.0: dependencies: xmlchars: 2.2.0 + scheduler@0.26.0: {} + scheduler@0.27.0: {} scroll-into-view-if-needed@3.1.0: @@ -7057,10 +7966,14 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} + simple-xml-to-json@1.2.3: {} + slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -7076,6 +7989,9 @@ snapshots: stackback@0.0.2: {} + stage-js@1.0.0-alpha.17: + optional: true + std-env@3.10.0: {} storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): @@ -7143,6 +8059,10 @@ snapshots: dependencies: safe-buffer: 5.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -7161,6 +8081,11 @@ snapshots: strip-json-comments@3.1.1: {} + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + stylis@4.3.6: {} supports-color@7.2.0: @@ -7181,12 +8106,17 @@ snapshots: tapable@2.3.0: {} + three@0.177.0: + optional: true + throttle-debounce@5.0.2: {} tiny-invariant@1.3.3: {} tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -7210,6 +8140,11 @@ snapshots: dependencies: is-number: 7.0.0 + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + tough-cookie@6.0.0: dependencies: tldts: 7.0.19 @@ -7320,6 +8255,10 @@ snapshots: dependencies: react: 19.2.3 + utif2@4.1.0: + dependencies: + pako: 1.0.11 + util-deprecate@1.0.2: {} vite-plugin-dts@4.5.4(@types/node@25.0.7)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): @@ -7401,6 +8340,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + web-tree-sitter@0.25.10: {} + webidl-conversions@8.0.1: {} webpack-virtual-modules@0.6.2: {} @@ -7443,6 +8384,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + ws@7.5.10: {} + ws@8.18.3: {} ws@8.19.0: {} @@ -7453,6 +8396,15 @@ snapshots: xml-name-validator@5.0.0: {} + xml-parse-from-string@1.0.1: {} + + xml2js@0.5.0: + dependencies: + sax: 1.1.6 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + xmlchars@2.2.0: {} xmldoc@0.4.0: @@ -7467,8 +8419,12 @@ snapshots: yocto-queue@0.1.0: {} + yoga-layout@3.2.1: {} + zod-validation-error@4.0.2(zod@4.1.13): dependencies: zod: 4.1.13 + zod@3.25.76: {} + zod@4.1.13: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 422bc42b..1b4b95b5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - packages/* - themes/* - site + - cli patchedDependencies: "@lhncbc/ucum-lhc@7.1.3": patches/@lhncbc__ucum-lhc@7.1.3.patch diff --git a/site/stories/questionnaire/questionnaire.stories.tsx b/site/stories/questionnaire/questionnaire.stories.tsx index a9aa7740..4255da21 100644 --- a/site/stories/questionnaire/questionnaire.stories.tsx +++ b/site/stories/questionnaire/questionnaire.stories.tsx @@ -12,6 +12,7 @@ import { import answerConstraint from "./samples/answer-constraint-examples.json" with { type: "json" }; import answerExpression from "./samples/answer-expression.json" with { type: "json" }; import answerOptions from "./samples/answer-options.json" with { type: "json" }; +import answerOptionsChoice from "./samples/answer-options-choice.json" with { type: "json" }; import answerValueSet from "./samples/answer-valueset.json" with { type: "json" }; import booleanGating from "./samples/boolean-gating.json" with { type: "json" }; import expressionCalculated from "./samples/expression-calculated.json" with { type: "json" }; @@ -159,6 +160,10 @@ export const AnswerOptions = makeStory( "Answer options", answerOptions as Questionnaire, ); +export const AnswerOptionsChoice = makeStory( + "Simple answer options", + answerOptionsChoice as Questionnaire, +); export const AnswerExpressions = makeStory( "Answer expressions", answerExpression as Questionnaire, diff --git a/site/stories/questionnaire/samples/answer-options-choice.json b/site/stories/questionnaire/samples/answer-options-choice.json new file mode 100644 index 00000000..39e91ec3 --- /dev/null +++ b/site/stories/questionnaire/samples/answer-options-choice.json @@ -0,0 +1,79 @@ +{ + "resourceType": "Questionnaire", + "id": "answer-options-choice", + "status": "active", + "title": "Answer Options (Simple)", + "description": "Minimal questionnaire showcasing answerOption with itemControl.", + "item": [ + { + "linkId": "contact-method", + "type": "string", + "text": "Preferred contact method", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "answerOption": [ + { "valueString": "Email" }, + { "valueString": "Phone" }, + { "valueString": "SMS" } + ] + }, + { + "linkId": "favorite-fruit", + "type": "string", + "text": "Favorite fruit", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down" + } + ] + } + } + ], + "answerOption": [ + { "valueString": "Apple" }, + { "valueString": "Banana" }, + { "valueString": "Pear" } + ] + }, + { + "linkId": "hobbies", + "type": "string", + "text": "Select your hobbies", + "repeats": true, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "check-box" + } + ] + } + } + ], + "answerOption": [ + { "valueString": "Reading" }, + { "valueString": "Music" }, + { "valueString": "Sports" } + ] + } + ] +} diff --git a/themes/hs-theme/tsconfig.json b/themes/hs-theme/tsconfig.json index b74c46c0..7adb38dd 100644 --- a/themes/hs-theme/tsconfig.json +++ b/themes/hs-theme/tsconfig.json @@ -1,4 +1,17 @@ { + "compilerOptions": { + "baseUrl": "../..", + "paths": { + "@aidbox-forms/theme": ["packages/theme/lib"], + "@aidbox-forms/antd-theme": ["themes/antd-theme/lib"], + "@aidbox-forms/mantine-theme": ["themes/mantine-theme/lib"], + "@aidbox-forms/hs-theme": ["themes/hs-theme/lib"], + "@aidbox-forms/nshuk-theme": ["themes/nshuk-theme/lib"], + "@aidbox-forms/opentui-theme": ["themes/opentui-theme/lib"], + "@aidbox-forms/renderer": ["packages/renderer/lib"], + "@aidbox-forms/renderer/*": ["packages/renderer/lib/*"] + } + }, "files": [], "references": [ { "path": "./tsconfig.lib.json" }, diff --git a/themes/opentui-theme/README.md b/themes/opentui-theme/README.md new file mode 100644 index 00000000..af914203 --- /dev/null +++ b/themes/opentui-theme/README.md @@ -0,0 +1,33 @@ +# @aidbox-forms/opentui-theme + +OpenTUI (`@opentui/react`) theme for `@aidbox-forms/renderer`. + +This theme is intended for terminal UI demos and CLIs. + +## Usage + +```ts +import Renderer from "@aidbox-forms/renderer"; +import { Provider as FocusProvider, theme as opentuiTheme } from "@aidbox-forms/opentui-theme"; + +export function App({ questionnaire }) { + return ( + + + + ); +} +``` + +The `FocusProvider` is required so inputs can receive focus and Tab/Shift+Tab navigation works. + +## Notes + +- Attachments: upload is not supported in terminal (TBD). `FileInput` is display-only and never calls `onChange`. + +## TBD + +- Proper table/grid layout for `Table` (current implementation is a readable vertical layout). +- Richer widgets for number spinner/slider (current implementation is an input-based fallback). +- Better accessibility mapping for `aria*` props (no native ARIA in TUI). +- Theme-level styling configuration. diff --git a/themes/opentui-theme/lib/components/action-button.tsx b/themes/opentui-theme/lib/components/action-button.tsx new file mode 100644 index 00000000..aa4a488b --- /dev/null +++ b/themes/opentui-theme/lib/components/action-button.tsx @@ -0,0 +1,42 @@ +import { TextAttributes } from "@opentui/core"; +import { useKeyboard } from "@opentui/react"; +import { useCallback } from "react"; +import { useFocusable } from "./focus-provider.tsx"; + +export function ActionButton({ + id, + label, + onClick, + disabled, +}: { + id: string; + label: string; + onClick: () => void; + disabled?: boolean | undefined; +}) { + const enabled = disabled !== true; + const { focused } = useFocusable(id, enabled); + + const handleActivate = useCallback(() => { + if (!enabled) return; + onClick(); + }, [enabled, onClick]); + + useKeyboard((key) => { + if (!focused) return; + if (key.eventType !== "press") return; + + if (key.name === "return" || key.name === "enter" || key.name === "space") { + key.preventDefault(); + key.stopPropagation(); + handleActivate(); + } + }); + + return ( + + {focused ? ">" : " "} + [{label}] + + ); +} diff --git a/themes/opentui-theme/lib/components/answer-list.tsx b/themes/opentui-theme/lib/components/answer-list.tsx new file mode 100644 index 00000000..73f63c54 --- /dev/null +++ b/themes/opentui-theme/lib/components/answer-list.tsx @@ -0,0 +1,27 @@ +import { Children, useId } from "react"; +import type { AnswerListProperties } from "@aidbox-forms/theme"; +import { ActionButton } from "./action-button.tsx"; + +export function AnswerList({ + children, + onAdd, + canAdd, + addLabel, +}: AnswerListProperties) { + const items = Children.toArray(children); + const addId = useId(); + + return ( + + {items.length > 0 ? {items} : undefined} + {onAdd ? ( + + ) : undefined} + + ); +} diff --git a/themes/opentui-theme/lib/components/answer-scaffold.tsx b/themes/opentui-theme/lib/components/answer-scaffold.tsx new file mode 100644 index 00000000..f3f5e8a4 --- /dev/null +++ b/themes/opentui-theme/lib/components/answer-scaffold.tsx @@ -0,0 +1,36 @@ +import { useId } from "react"; +import type { AnswerScaffoldProperties } from "@aidbox-forms/theme"; +import { ActionButton } from "./action-button.tsx"; + +export function AnswerScaffold({ + control, + onRemove, + canRemove, + errors, + children, +}: AnswerScaffoldProperties) { + const removeId = useId(); + + return ( + + + {control} + {onRemove ? ( + + ) : undefined} + + + {children || errors ? ( + + {children} + {errors} + + ) : undefined} + + ); +} diff --git a/themes/opentui-theme/lib/components/checkbox-list.tsx b/themes/opentui-theme/lib/components/checkbox-list.tsx new file mode 100644 index 00000000..6d83ab87 --- /dev/null +++ b/themes/opentui-theme/lib/components/checkbox-list.tsx @@ -0,0 +1,68 @@ +import type { CheckboxListProperties } from "@aidbox-forms/theme"; +import { InlineText } from "./utilities.tsx"; +import { Checkbox } from "./checkbox.tsx"; + +export function CheckboxList({ + options, + selectedOptions, + onSelect, + onDeselect, + specifyOtherOption, + customOptionForm, + id, + disabled, + isLoading = false, +}: CheckboxListProperties) { + const enabled = disabled !== true && !isLoading; + + const displayOptions = specifyOtherOption + ? [...options, specifyOtherOption] + : options; + const selectedByToken = new Map( + selectedOptions.map((option) => [option.token, option]), + ); + const specifyOtherToken = specifyOtherOption?.token; + const isCustomActive = Boolean(customOptionForm && specifyOtherToken); + + return ( + + + {displayOptions.map((option) => { + const optionId = `${id}-${option.token}-option`; + const selectedOption = selectedByToken.get(option.token); + const isSpecifyOther = option.token === specifyOtherToken; + const checked = isSpecifyOther + ? isCustomActive || Boolean(selectedOption) + : Boolean(selectedOption); + + const optionDisabled = + !enabled || + (option.disabled === true && !(isSpecifyOther && isCustomActive)); + + return ( + + { + if (checked) { + onDeselect(option.token); + } else { + onSelect(option.token); + } + }} + /> + {selectedOption?.errors} + + ); + })} + + + {isLoading ? Loading… : undefined} + {customOptionForm} + + ); +} diff --git a/themes/opentui-theme/lib/components/checkbox.tsx b/themes/opentui-theme/lib/components/checkbox.tsx new file mode 100644 index 00000000..2ef2d89a --- /dev/null +++ b/themes/opentui-theme/lib/components/checkbox.tsx @@ -0,0 +1,47 @@ +import { TextAttributes } from "@opentui/core"; +import { useKeyboard } from "@opentui/react"; +import type { CheckboxProperties } from "@aidbox-forms/theme"; +import { useCallback } from "react"; +import { useFocusable } from "./focus-provider.tsx"; +import { InlineText } from "./utilities.tsx"; + +export function Checkbox({ + id, + checked, + onChange, + disabled, + label, +}: CheckboxProperties) { + const enabled = disabled !== true; + const { focused } = useFocusable(id, enabled); + + const toggle = useCallback(() => { + if (!enabled) return; + onChange(); + }, [enabled, onChange]); + + useKeyboard((key) => { + if (!focused) return; + if (key.eventType !== "press") return; + + if (key.name === "space" || key.name === "return" || key.name === "enter") { + key.preventDefault(); + key.stopPropagation(); + toggle(); + } + }); + + const labelContent = label ? ( + {label} + ) : undefined; + + return ( + + {focused ? ">" : " "} + + {checked ? "[x]" : "[ ]"} + + {labelContent} + + ); +} diff --git a/themes/opentui-theme/lib/components/custom-option-form.tsx b/themes/opentui-theme/lib/components/custom-option-form.tsx new file mode 100644 index 00000000..d7cd5e2e --- /dev/null +++ b/themes/opentui-theme/lib/components/custom-option-form.tsx @@ -0,0 +1,34 @@ +import type { CustomOptionFormProperties } from "@aidbox-forms/theme"; +import { useId } from "react"; +import { ActionButton } from "./action-button.tsx"; + +export function CustomOptionForm({ + content, + errors, + submit, + cancel, +}: CustomOptionFormProperties) { + const submitId = useId(); + const cancelId = useId(); + + return ( + + {content} + {errors} + + + + + + ); +} diff --git a/themes/opentui-theme/lib/components/date-input.tsx b/themes/opentui-theme/lib/components/date-input.tsx new file mode 100644 index 00000000..6541bdef --- /dev/null +++ b/themes/opentui-theme/lib/components/date-input.tsx @@ -0,0 +1,31 @@ +import type { DateInputProperties } from "@aidbox-forms/theme"; +import { useFocusable } from "./focus-provider.tsx"; + +export function DateInput({ + id, + value, + onChange, + disabled, + placeholder, +}: DateInputProperties) { + const enabled = disabled !== true; + const { focused, focusNext } = useFocusable(id, enabled); + + return ( + + { + if (!enabled) return; + onChange(next); + }} + onSubmit={() => { + if (!enabled) return; + focusNext(); + }} + /> + + ); +} diff --git a/themes/opentui-theme/lib/components/date-time-input.tsx b/themes/opentui-theme/lib/components/date-time-input.tsx new file mode 100644 index 00000000..e65afb04 --- /dev/null +++ b/themes/opentui-theme/lib/components/date-time-input.tsx @@ -0,0 +1,31 @@ +import type { DateTimeInputProperties } from "@aidbox-forms/theme"; +import { useFocusable } from "./focus-provider.tsx"; + +export function DateTimeInput({ + id, + value, + onChange, + disabled, + placeholder, +}: DateTimeInputProperties) { + const enabled = disabled !== true; + const { focused, focusNext } = useFocusable(id, enabled); + + return ( + + { + if (!enabled) return; + onChange(next); + }} + onSubmit={() => { + if (!enabled) return; + focusNext(); + }} + /> + + ); +} diff --git a/themes/opentui-theme/lib/components/display-renderer.tsx b/themes/opentui-theme/lib/components/display-renderer.tsx new file mode 100644 index 00000000..af7e98c9 --- /dev/null +++ b/themes/opentui-theme/lib/components/display-renderer.tsx @@ -0,0 +1,12 @@ +import type { DisplayRendererProperties } from "@aidbox-forms/theme"; + +export function DisplayRenderer({ + linkId, + children, +}: DisplayRendererProperties) { + return ( + + {children} + + ); +} diff --git a/themes/opentui-theme/lib/components/error-summary.tsx b/themes/opentui-theme/lib/components/error-summary.tsx new file mode 100644 index 00000000..22d9d530 --- /dev/null +++ b/themes/opentui-theme/lib/components/error-summary.tsx @@ -0,0 +1,33 @@ +import { TextAttributes } from "@opentui/core"; +import type { ErrorSummaryProperties } from "@aidbox-forms/theme"; + +export function ErrorSummary({ + id, + title = "There is a problem", + description, + items, +}: ErrorSummaryProperties) { + if (items.length === 0) return; + + const containerProperties = id ? { id } : {}; + + return ( + + + {title} + + {description ? ( + {description} + ) : undefined} + + {items.map((item, index) => ( + - {item.message} + ))} + + + ); +} diff --git a/themes/opentui-theme/lib/components/errors.tsx b/themes/opentui-theme/lib/components/errors.tsx new file mode 100644 index 00000000..a8052c7a --- /dev/null +++ b/themes/opentui-theme/lib/components/errors.tsx @@ -0,0 +1,20 @@ +import { TextAttributes } from "@opentui/core"; +import type { ErrorsProperties } from "@aidbox-forms/theme"; + +export function Errors({ id, messages }: ErrorsProperties) { + if (messages.length === 0) return; + + return ( + + {messages.map((message, index) => ( + + - {message} + + ))} + + ); +} diff --git a/themes/opentui-theme/lib/components/file-input.tsx b/themes/opentui-theme/lib/components/file-input.tsx new file mode 100644 index 00000000..b176ba87 --- /dev/null +++ b/themes/opentui-theme/lib/components/file-input.tsx @@ -0,0 +1,28 @@ +import type { FileInputProperties } from "@aidbox-forms/theme"; +import { InlineText } from "./utilities.tsx"; + +export function FileInput({ value }: FileInputProperties) { + if (!value) { + return ( + + No file selected (attachments unsupported in TUI) + + ); + } + + return ( + + + Attachment: {value.title ?? value.url ?? "(untitled)"} + + {value.contentType ? ( + {value.contentType} + ) : undefined} + {typeof value.size === "number" ? ( + {value.size} bytes + ) : undefined} + {value.url ? {value.url} : undefined} + Attachment upload is TBD for TUI. + + ); +} diff --git a/themes/opentui-theme/lib/components/flyover.tsx b/themes/opentui-theme/lib/components/flyover.tsx new file mode 100644 index 00000000..2a297a8d --- /dev/null +++ b/themes/opentui-theme/lib/components/flyover.tsx @@ -0,0 +1,6 @@ +import type { FlyoverProperties } from "@aidbox-forms/theme"; +import { InlineText } from "./utilities.tsx"; + +export function Flyover({ children }: FlyoverProperties) { + return {children}; +} diff --git a/themes/opentui-theme/lib/components/focus-provider.tsx b/themes/opentui-theme/lib/components/focus-provider.tsx new file mode 100644 index 00000000..af483350 --- /dev/null +++ b/themes/opentui-theme/lib/components/focus-provider.tsx @@ -0,0 +1,183 @@ +import { parseKeypress } from "@opentui/core"; +import { useRenderer } from "@opentui/react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; + +type FocusState = { + ids: string[]; + activeId: string | undefined; +}; + +type FocusContextValue = { + state: FocusState; + register: (id: string) => void; + unregister: (id: string) => void; + setActive: (id: string) => void; + focusNext: () => void; + focusPrev: () => void; +}; + +const FocusContext = createContext(undefined); + +export function Provider({ children }: { children: ReactNode }) { + const [state, setState] = useState({ + ids: [], + activeId: undefined, + }); + + const register = useCallback((id: string) => { + setState((previous) => { + if (previous.ids.includes(id)) return previous; + + const ids = [...previous.ids, id]; + const activeId = previous.activeId ?? id; + return { ids, activeId }; + }); + }, []); + + const unregister = useCallback((id: string) => { + setState((previous) => { + if (!previous.ids.includes(id)) return previous; + + const ids = previous.ids.filter((entry) => entry !== id); + const nextActiveId = + previous.activeId === id + ? ids.at(0) + : previous.activeId && ids.includes(previous.activeId) + ? previous.activeId + : ids.at(0); + + return { ids, activeId: nextActiveId }; + }); + }, []); + + const setActive = useCallback((id: string) => { + setState((previous) => { + if (!previous.ids.includes(id) || previous.activeId === id) { + return previous; + } + + return { ...previous, activeId: id }; + }); + }, []); + + const focusNext = useCallback(() => { + setState((previous) => { + if (previous.ids.length === 0) return previous; + + if (previous.activeId === undefined) { + return { ...previous, activeId: previous.ids[0] }; + } + + const index = previous.ids.indexOf(previous.activeId); + const nextIndex = index === -1 ? 0 : (index + 1) % previous.ids.length; + return { ...previous, activeId: previous.ids[nextIndex] }; + }); + }, []); + + const focusPrevious = useCallback(() => { + setState((previous) => { + if (previous.ids.length === 0) return previous; + + if (previous.activeId === undefined) { + return { ...previous, activeId: previous.ids[0] }; + } + + const index = previous.ids.indexOf(previous.activeId); + const previousIndex = + index === -1 + ? 0 + : (index - 1 + previous.ids.length) % previous.ids.length; + + return { ...previous, activeId: previous.ids[previousIndex] }; + }); + }, []); + + const renderer = useRenderer(); + + useEffect(() => { + const handler = (sequence: string): boolean => { + const key = parseKeypress(sequence, { useKittyKeyboard: true }); + if (!key) return false; + + if (key.name !== "tab") return false; + if (key.ctrl || key.meta || key.option) return false; + + if (key.eventType !== "release") { + if (key.shift) { + focusPrevious(); + } else { + focusNext(); + } + } + + return true; + }; + + renderer.prependInputHandler(handler); + + return () => { + renderer.removeInputHandler(handler); + }; + }, [focusNext, focusPrevious, renderer]); + + const value = useMemo(() => { + return { + state, + register, + unregister, + setActive, + focusNext, + focusPrev: focusPrevious, + }; + }, [focusNext, focusPrevious, register, setActive, state, unregister]); + + return ( + {children} + ); +} + +export function useFocusable( + id: string, + enabled: boolean, +): { + focused: boolean; + focus: () => void; + focusNext: () => void; + focusPrev: () => void; +} { + const context = useContext(FocusContext); + + const register = context?.register; + const unregister = context?.unregister; + + useEffect(() => { + if (!register || !unregister || !enabled) return; + + register(id); + return () => { + unregister(id); + }; + }, [enabled, id, register, unregister]); + + const focus = useCallback(() => { + if (!context) return; + if (!enabled) return; + + context.setActive(id); + }, [context, enabled, id]); + + return { + focused: Boolean(context && enabled && context.state.activeId === id), + focus, + focusNext: context?.focusNext ?? (() => {}), + focusPrev: context?.focusPrev ?? (() => {}), + }; +} diff --git a/themes/opentui-theme/lib/components/form.tsx b/themes/opentui-theme/lib/components/form.tsx new file mode 100644 index 00000000..4e6ee38d --- /dev/null +++ b/themes/opentui-theme/lib/components/form.tsx @@ -0,0 +1,122 @@ +import { useKeyboard } from "@opentui/react"; +import { TextAttributes } from "@opentui/core"; +import { useId } from "react"; +import type { FormPagination, FormProperties } from "@aidbox-forms/theme"; +import { ActionButton } from "./action-button.tsx"; +import { InlineText } from "./utilities.tsx"; + +function isCtrlShortcut( + key: { name: string; ctrl: boolean; meta: boolean; option: boolean }, + name: string, +): boolean { + return key.ctrl && !key.meta && !key.option && key.name === name; +} + +function PaginationControls({ pagination }: { pagination: FormPagination }) { + const previousId = useId(); + const nextId = useId(); + + return ( + + + + Page {pagination.current} / {pagination.total} + + + + ); +} + +export function Form({ + onSubmit, + onCancel, + children, + pagination, + title, + description, + errors, + before, + after, +}: FormProperties) { + const submitId = useId(); + const cancelId = useId(); + + useKeyboard((key) => { + if (key.eventType !== "press") return; + + if (isCtrlShortcut(key, "s")) { + if (onSubmit) { + key.preventDefault(); + key.stopPropagation(); + onSubmit(); + } + return; + } + + if (isCtrlShortcut(key, "r")) { + if (onCancel) { + key.preventDefault(); + key.stopPropagation(); + onCancel(); + } + return; + } + + if (pagination && isCtrlShortcut(key, "p") && !pagination.disabledPrev) { + key.preventDefault(); + key.stopPropagation(); + pagination.onPrev(); + return; + } + + if (pagination && isCtrlShortcut(key, "n") && !pagination.disabledNext) { + key.preventDefault(); + key.stopPropagation(); + pagination.onNext(); + } + }); + + return ( + + {title ? ( + {title} + ) : undefined} + {description ? {description} : undefined} + + {errors} + {before} + {children} + {after} + + + {pagination ? ( + + ) : undefined} + + + {onSubmit ? ( + + ) : undefined} + {onCancel ? ( + + ) : undefined} + + + + Tab/Shift+Tab navigate • Ctrl+S submit • Ctrl+R reset + {pagination ? " • Ctrl+P/Ctrl+N page" : ""} + + + + ); +} diff --git a/themes/opentui-theme/lib/components/group-list.tsx b/themes/opentui-theme/lib/components/group-list.tsx new file mode 100644 index 00000000..edac010c --- /dev/null +++ b/themes/opentui-theme/lib/components/group-list.tsx @@ -0,0 +1,29 @@ +import { useId } from "react"; +import type { GroupListProperties } from "@aidbox-forms/theme"; +import { ActionButton } from "./action-button.tsx"; + +export function GroupList({ + linkId, + header, + children, + onAdd, + canAdd, + addLabel, +}: GroupListProperties) { + const addId = useId(); + + return ( + + {header} + {children} + {onAdd ? ( + + ) : undefined} + + ); +} diff --git a/themes/opentui-theme/lib/components/group-scaffold.tsx b/themes/opentui-theme/lib/components/group-scaffold.tsx new file mode 100644 index 00000000..39182f8f --- /dev/null +++ b/themes/opentui-theme/lib/components/group-scaffold.tsx @@ -0,0 +1,32 @@ +import { useId } from "react"; +import type { GroupScaffoldProperties } from "@aidbox-forms/theme"; +import { ActionButton } from "./action-button.tsx"; + +export function GroupScaffold({ + header, + children, + errors, + onRemove, + canRemove, + removeLabel, +}: GroupScaffoldProperties) { + const removeId = useId(); + + return ( + + + {header} + {onRemove ? ( + + ) : undefined} + + {children} + {errors} + + ); +} diff --git a/themes/opentui-theme/lib/components/help.tsx b/themes/opentui-theme/lib/components/help.tsx new file mode 100644 index 00000000..373799c2 --- /dev/null +++ b/themes/opentui-theme/lib/components/help.tsx @@ -0,0 +1,6 @@ +import type { HelpProperties } from "@aidbox-forms/theme"; +import { InlineText } from "./utilities.tsx"; + +export function Help({ children }: HelpProperties) { + return {children}; +} diff --git a/themes/opentui-theme/lib/components/input-group.tsx b/themes/opentui-theme/lib/components/input-group.tsx new file mode 100644 index 00000000..bd324041 --- /dev/null +++ b/themes/opentui-theme/lib/components/input-group.tsx @@ -0,0 +1,25 @@ +import type { InputGroupProperties } from "@aidbox-forms/theme"; +import { Children } from "react"; + +export function InputGroup({ + children, + layout, + weights, +}: InputGroupProperties) { + const items = Children.toArray(children); + + const flexDirection = layout === "grid" ? "row" : "row"; + + return ( + + {items.map((child, index) => ( + + {child} + + ))} + + ); +} diff --git a/themes/opentui-theme/lib/components/label.tsx b/themes/opentui-theme/lib/components/label.tsx new file mode 100644 index 00000000..c48d98ca --- /dev/null +++ b/themes/opentui-theme/lib/components/label.tsx @@ -0,0 +1,59 @@ +import { TextAttributes } from "@opentui/core"; +import type { LabelProperties } from "@aidbox-forms/theme"; +import { toPlainText } from "./utilities.tsx"; + +function isTextLike(value: unknown): value is string | number | boolean { + return ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ); +} + +export function Label({ + prefix, + children, + id, + required, + help, + legal, + flyover, + as = "label", +}: LabelProperties) { + const emphasize = as !== "text"; + const attributes = emphasize ? TextAttributes.BOLD : 0; + + const prefixText = prefix ? toPlainText(prefix) : ""; + + const childIsText = isTextLike(children); + const childText = childIsText ? String(children) : ""; + + return ( + + + {prefixText ? ( + {prefixText} + ) : undefined} + + {childIsText ? ( + + {childText} + {required ? " *" : ""} + + ) : ( + + {children} + + )} + + {!childIsText && required ? ( + * + ) : undefined} + + + {help} + {legal} + {flyover} + + ); +} diff --git a/themes/opentui-theme/lib/components/legal.tsx b/themes/opentui-theme/lib/components/legal.tsx new file mode 100644 index 00000000..8469ff11 --- /dev/null +++ b/themes/opentui-theme/lib/components/legal.tsx @@ -0,0 +1,6 @@ +import type { LegalProperties } from "@aidbox-forms/theme"; +import { InlineText } from "./utilities.tsx"; + +export function Legal({ children }: LegalProperties) { + return {children}; +} diff --git a/themes/opentui-theme/lib/components/link.tsx b/themes/opentui-theme/lib/components/link.tsx new file mode 100644 index 00000000..5d2a21ae --- /dev/null +++ b/themes/opentui-theme/lib/components/link.tsx @@ -0,0 +1,12 @@ +import type { LinkProperties } from "@aidbox-forms/theme"; +import { toPlainText } from "./utilities.tsx"; + +export function Link({ href, children }: LinkProperties) { + const label = toPlainText(children); + + return ( + + {label} ({href}) + + ); +} diff --git a/themes/opentui-theme/lib/components/multi-select-input.tsx b/themes/opentui-theme/lib/components/multi-select-input.tsx new file mode 100644 index 00000000..8a960479 --- /dev/null +++ b/themes/opentui-theme/lib/components/multi-select-input.tsx @@ -0,0 +1,100 @@ +import type { MultiSelectInputProperties } from "@aidbox-forms/theme"; +import { InlineText } from "./utilities.tsx"; +import { Checkbox } from "./checkbox.tsx"; +import { useFocusable } from "./focus-provider.tsx"; +import { useState } from "react"; + +export function MultiSelectInput({ + options, + selectedOptions, + onSelect, + onDeselect, + onSearch, + specifyOtherOption, + customOptionForm, + id, + disabled, + isLoading = false, + placeholder, +}: MultiSelectInputProperties) { + const enabled = disabled !== true && !isLoading; + + const searchId = `${id}-search`; + const searchFocusable = useFocusable( + searchId, + enabled && onSearch != undefined, + ); + const [searchQuery, setSearchQuery] = useState(""); + + const displayOptions = specifyOtherOption + ? [...options, specifyOtherOption] + : options; + const selectedByToken = new Map( + selectedOptions.map((option) => [option.token, option]), + ); + const specifyOtherToken = specifyOtherOption?.token; + const isCustomActive = Boolean(customOptionForm && specifyOtherToken); + + return ( + + {onSearch ? ( + + { + setSearchQuery(next); + if (!enabled) return; + onSearch(next); + }} + onSubmit={() => { + if (!enabled) return; + searchFocusable.focusNext(); + }} + /> + + ) : undefined} + + + {displayOptions.map((option) => { + const optionId = `${id}-${option.token}-option`; + const selectedOption = selectedByToken.get(option.token); + const isSpecifyOther = option.token === specifyOtherToken; + const checked = isSpecifyOther + ? isCustomActive || Boolean(selectedOption) + : Boolean(selectedOption); + + const optionDisabled = + !enabled || + (option.disabled === true && !(isSpecifyOther && isCustomActive)); + + return ( + + { + if (checked) { + onDeselect(option.token); + } else { + onSelect(option.token); + } + }} + /> + {selectedOption?.errors} + + ); + })} + + + {isLoading ? Loading… : undefined} + {customOptionForm} + + ); +} diff --git a/themes/opentui-theme/lib/components/number-input.tsx b/themes/opentui-theme/lib/components/number-input.tsx new file mode 100644 index 00000000..a6cf9805 --- /dev/null +++ b/themes/opentui-theme/lib/components/number-input.tsx @@ -0,0 +1,83 @@ +import type { NumberInputProperties } from "@aidbox-forms/theme"; +import type { InputRenderable } from "@opentui/core"; +import { useEffect, useRef } from "react"; +import { useFocusable } from "./focus-provider.tsx"; +import { clamp } from "./utilities.tsx"; + +export function NumberInput({ + id, + value, + onChange, + disabled, + placeholder, + min, + max, + unitLabel, +}: NumberInputProperties) { + const enabled = disabled !== true; + const { focused, focusNext } = useFocusable(id, enabled); + + const valueString = value === undefined ? "" : String(value); + const inputReference = useRef(null); + const isInitializedReference = useRef(false); + + useEffect(() => { + const input = inputReference.current; + if (!input) return; + + if (!isInitializedReference.current) { + input.value = valueString; + isInitializedReference.current = true; + return; + } + + if (focused) return; + + if (input.value !== valueString) { + input.value = valueString; + } + }, [focused, valueString]); + + return ( + + + { + if (!enabled) return; + + const trimmed = next.trim(); + if (trimmed.length === 0) { + onChange(); + return; + } + + const parsed = Number(trimmed); + if (Number.isNaN(parsed)) { + onChange(); + return; + } + + const clampedValue = clamp(parsed, min ?? parsed, max ?? parsed); + onChange(clampedValue); + }} + onSubmit={() => { + if (!enabled) return; + focusNext(); + }} + /> + + {unitLabel ? {unitLabel} : undefined} + + ); +} diff --git a/themes/opentui-theme/lib/components/options-loading.tsx b/themes/opentui-theme/lib/components/options-loading.tsx new file mode 100644 index 00000000..13f6c6b5 --- /dev/null +++ b/themes/opentui-theme/lib/components/options-loading.tsx @@ -0,0 +1,6 @@ +import type { OptionsLoadingProperties } from "@aidbox-forms/theme"; +import { InlineText } from "./utilities.tsx"; + +export function OptionsLoading({ isLoading }: OptionsLoadingProperties) { + return isLoading ? Loading options… : undefined; +} diff --git a/themes/opentui-theme/lib/components/question-scaffold.tsx b/themes/opentui-theme/lib/components/question-scaffold.tsx new file mode 100644 index 00000000..affb9e37 --- /dev/null +++ b/themes/opentui-theme/lib/components/question-scaffold.tsx @@ -0,0 +1,18 @@ +import type { QuestionScaffoldProperties } from "@aidbox-forms/theme"; + +export function QuestionScaffold({ + linkId, + header, + children, + errors, +}: QuestionScaffoldProperties) { + return ( + + {header} + + {children} + + {errors} + + ); +} diff --git a/themes/opentui-theme/lib/components/radio-button-list.tsx b/themes/opentui-theme/lib/components/radio-button-list.tsx new file mode 100644 index 00000000..4381dd24 --- /dev/null +++ b/themes/opentui-theme/lib/components/radio-button-list.tsx @@ -0,0 +1,107 @@ +import type { RadioButtonListProperties } from "@aidbox-forms/theme"; +import type { SelectOption } from "@opentui/core"; +import { useMemo } from "react"; +import { useFocusable } from "./focus-provider.tsx"; +import { InlineText, toPlainText } from "./utilities.tsx"; + +export function RadioButtonList({ + options, + selectedOption, + onChange, + specifyOtherOption, + customOptionForm, + id, + disabled, + isLoading = false, +}: RadioButtonListProperties) { + const enabled = disabled !== true && !isLoading; + const { focused } = useFocusable(id, enabled); + + const fullOptions = useMemo(() => { + const base = specifyOtherOption + ? [...options, specifyOtherOption] + : [...options]; + const tokens = new Set(base.map((option) => option.token)); + + const activeToken = + customOptionForm && specifyOtherOption + ? specifyOtherOption.token + : selectedOption?.token; + + if (activeToken && !tokens.has(activeToken)) { + base.unshift({ + token: activeToken, + label: selectedOption?.label ?? activeToken, + disabled: true, + }); + } + + return base; + }, [customOptionForm, options, selectedOption, specifyOtherOption]); + + const activeToken = + customOptionForm && specifyOtherOption + ? specifyOtherOption.token + : selectedOption?.token; + + const selectedIndex = + activeToken == undefined + ? 0 + : 1 + fullOptions.findIndex((option) => option.token === activeToken); + + const selectOptions = useMemo(() => { + const none: SelectOption = { + name: "Select an option", + description: "", + value: undefined, + }; + + return [ + none, + ...fullOptions.map((option) => ({ + name: toPlainText(option.label) || option.token, + description: option.disabled === true ? "(disabled)" : "", + value: option.token, + })), + ]; + }, [fullOptions]); + + return ( + + + { + setSearchQuery(next); + if (!enabled) return; + onSearch(next); + }} + onSubmit={() => { + if (!enabled) return; + searchFocusable.focusNext(); + }} + /> + + ) : undefined} + + + { + if (!enabled) return; + + const trimmed = next.trim(); + if (trimmed.length === 0) { + onChange(); + return; + } + + const parsed = Number(trimmed); + if (Number.isNaN(parsed)) { + onChange(); + return; + } + + const clampedValue = clamp(parsed, min ?? parsed, max ?? parsed); + onChange(clampedValue); + }} + onSubmit={() => { + if (!enabled) return; + focusNext(); + }} + /> + + {unitLabel ? {unitLabel} : undefined} + + + ); +} diff --git a/themes/opentui-theme/lib/components/spinner-input.tsx b/themes/opentui-theme/lib/components/spinner-input.tsx new file mode 100644 index 00000000..2364fda6 --- /dev/null +++ b/themes/opentui-theme/lib/components/spinner-input.tsx @@ -0,0 +1,83 @@ +import type { SpinnerInputProperties } from "@aidbox-forms/theme"; +import type { InputRenderable } from "@opentui/core"; +import { useEffect, useRef } from "react"; +import { useFocusable } from "./focus-provider.tsx"; +import { clamp } from "./utilities.tsx"; + +export function SpinnerInput({ + id, + value, + onChange, + disabled, + placeholder, + min, + max, + unitLabel, +}: SpinnerInputProperties) { + const enabled = disabled !== true; + const { focused, focusNext } = useFocusable(id, enabled); + + const valueString = value === undefined ? "" : String(value); + const inputReference = useRef(null); + const isInitializedReference = useRef(false); + + useEffect(() => { + const input = inputReference.current; + if (!input) return; + + if (!isInitializedReference.current) { + input.value = valueString; + isInitializedReference.current = true; + return; + } + + if (focused) return; + + if (input.value !== valueString) { + input.value = valueString; + } + }, [focused, valueString]); + + return ( + + + { + if (!enabled) return; + + const trimmed = next.trim(); + if (trimmed.length === 0) { + onChange(); + return; + } + + const parsed = Number(trimmed); + if (Number.isNaN(parsed)) { + onChange(); + return; + } + + const clampedValue = clamp(parsed, min ?? parsed, max ?? parsed); + onChange(clampedValue); + }} + onSubmit={() => { + if (!enabled) return; + focusNext(); + }} + /> + + {unitLabel ? {unitLabel} : undefined} + + ); +} diff --git a/themes/opentui-theme/lib/components/stack.tsx b/themes/opentui-theme/lib/components/stack.tsx new file mode 100644 index 00000000..59113d0d --- /dev/null +++ b/themes/opentui-theme/lib/components/stack.tsx @@ -0,0 +1,9 @@ +import type { StackProperties } from "@aidbox-forms/theme"; + +export function Stack({ children }: StackProperties) { + return ( + + {children} + + ); +} diff --git a/themes/opentui-theme/lib/components/tab-container.tsx b/themes/opentui-theme/lib/components/tab-container.tsx new file mode 100644 index 00000000..8b8a7474 --- /dev/null +++ b/themes/opentui-theme/lib/components/tab-container.tsx @@ -0,0 +1,51 @@ +import type { TabContainerProperties } from "@aidbox-forms/theme"; +import type { SelectOption } from "@opentui/core"; +import { useMemo } from "react"; +import { useFocusable } from "./focus-provider.tsx"; +import { toPlainText } from "./utilities.tsx"; + +export function TabContainer({ + header, + items, + value, + onChange, + errors, + linkId, +}: TabContainerProperties) { + const controlId = `${linkId}-tabs`; + const { focused } = useFocusable(controlId, true); + + const options = useMemo(() => { + return items.map((item) => ({ + name: toPlainText(item.label) || item.token, + description: "", + value: item.token, + })); + }, [items]); + + const selectedIndex = Math.min( + Math.max(value, 0), + Math.max(items.length - 1, 0), + ); + + return ( + + {header} + +