diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100755 index 0000000..da0407c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100755 index 0000000..4ef52ee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +permissions: + contents: read + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup Go + uses: actions/setup-go@v7 + with: + go-version: 1.26 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + version: 12.6.0 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: make install + + - name: Lint + run: make lint + + - name: Build + run: make build + + - name: Tests + run: make tests diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..11f7a88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.tools/ +bin/ +vendor/ +node_modules/ +build/ +.idea/ +.vscode/ +coverage.txt +coverage.out +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.db +*.db-journal +*.mmdb +*.test +*.out +.env + +example/react/dist/ +example/react/node_modules/ + +react-sdk/ui-transport/dist/ +react-sdk/ui-transport/node_modules/ + +react-sdk/ui-core/dist/ +react-sdk/ui-core/node_modules/ + +react-sdk/ui-react/node_modules/ +react-sdk/ui-react/dist/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100755 index 0000000..ae15bad --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,264 @@ +version: "2" + +run: + go: "1.26" + timeout: 5m + tests: true + issues-exit-code: 1 + modules-download-mode: readonly + allow-parallel-runners: true + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + new: false + fix: true + +output: + formats: + text: + print-linter-name: true + print-issued-lines: true + colors: true + +formatters: + settings: + gofmt: + simplify: true + rewrite-rules: + - pattern: 'interface{}' + replacement: 'any' + - pattern: 'a[b:len(a)]' + replacement: 'a[b:]' + + exclusions: + paths: + - vendors/ + + enable: + - gofmt + - goimports + - gofumpt + +linters: + settings: + staticcheck: + checks: + - all + - -S1023 + - -ST1000 + - -ST1003 + - -ST1020 + gosec: + excludes: + - G104 + - G115 + - G301 + - G304 + - G306 + - G501 + - G505 + tagliatelle: + case: + rules: + mcp: snake + json: snake # JSON: snake_case (user_id) + yaml: snake # YAML: snake_case + xml: camel # XML: camelCase + mapstructure: snake # mapstructure: snake_case + env: upperSnake # ENV: UPPER_SNAKE_CASE + varnamelen: + min-name-length: 2 # Минимальная длина имени + max-distance: 5 # i, j, k допустимы в scope <= 5 строк + ignore-names: + - err # err — идиоматично + - ok # ok — идиоматично + - id # id — часто используется + - db # db — часто используется + - tx # tx — транзакция + - wg # wg — WaitGroup + - mu # mu — mutex + - rw # rw — RWMutex + - ch # ch — channel + - fn # fn — function + - sb # sb — strings.Builder + - ctx # ctx — context + - q # q — querier + - r # r — repo / reader + - s # s — strategy / service + - f # f — filter + - a # a — left operand (сравнение) + - b # b — right operand (сравнение) + ignore-type-assert-ok: true # Игнорировать v, ok := x.(T) + ignore-map-index-ok: true # Игнорировать v, ok := m[k] + ignore-chan-recv-ok: true # Игнорировать v, ok := <-ch + ignore-decls: + - i int # for i := ... + - j int # вложенные циклы + - n int # количество + - t testing.T # тесты + - b testing.B # бенчмарки + - r *http.Request # HTTP handler + - w http.ResponseWriter # HTTP handler + - c *gin.Context # Gin context + - c echo.Context # Echo context + - s *Server # конструктор Server + - m *metrics # конструктор metrics + revive: + severity: warning + rules: + # ------------------------------------------------------------------------- + # Предотвращение багов + # ------------------------------------------------------------------------- + - name: atomic # Проверяет правильное использование sync/atomic + - name: range-val-in-closure # Захват переменной цикла в замыкании + - name: range-val-address # Взятие адреса переменной цикла + - name: unreachable-code # Недостижимый код после return/panic + - name: unchecked-type-assertion # Type assertion без проверки ok + - name: datarace # Потенциальные data races + - name: identical-branches # Одинаковые ветки if/else + - name: defer # Проблемы с defer (в циклах, результат) + - name: call-to-gc # Явные вызовы runtime.GC() + - name: waitgroup-by-value # WaitGroup передан по значению + + # ------------------------------------------------------------------------- + # Обработка ошибок — Go proverb: "Don't just check errors, handle them gracefully" + # ------------------------------------------------------------------------- + - name: error-strings # Ошибки не должны начинаться с большой буквы + - name: error-return # error должен быть последним возвращаемым значением + - name: errorf # Использовать fmt.Errorf вместо errors.New + fmt.Sprintf + - name: unhandled-error # Необработанные ошибки + arguments: + - "fmt.Print" + - "fmt.Printf" + - "fmt.Println" + + # ------------------------------------------------------------------------- + # Сложность — Go proverb: "Clear is better than clever" + # ------------------------------------------------------------------------- + - name: cognitive-complexity + arguments: [50] # Cognitive complexity <= 15 + - name: cyclomatic + arguments: [50] # Cyclomatic complexity <= 10 + - name: function-result-limit + arguments: [3] # Максимум 3 возвращаемых значения + - name: argument-limit + arguments: [5] # Максимум 5 аргументов функции + + # ------------------------------------------------------------------------- + # Чистота кода — Go proverb: "A little copying is better than a little dependency" + # ------------------------------------------------------------------------- + - name: indent-error-flow # if err != nil { return } вместо else + - name: early-return # Ранний возврат вместо вложенности + - name: superfluous-else # Лишний else после return + - name: if-return # Упрощение if/return + - name: empty-block # Пустые блоки кода + - name: unnecessary-stmt # Ненужные операторы + - name: redundant-import-alias # import pkg "pkg" — лишний алиас + - name: confusing-results # Запутанные возвращаемые значения + - name: bool-literal-in-expr # if x == true → if x + - name: constant-logical-expr # Константные логические выражения + - name: modifies-parameter # Модификация параметров функции + - name: modifies-value-receiver # Модификация value receiver (бесполезно) + - name: redefines-builtin-id # Переопределение встроенных идентификаторов + - name: string-of-int # string(int) — частая ошибка + - name: time-equal # time.Time сравнение через == + - name: unconditional-recursion # Безусловная рекурсия (бесконечный цикл) + - name: useless-break # break в конце case (Go делает это автоматически) + + # ------------------------------------------------------------------------- + # Хорошие практики — Go proverbs + # ------------------------------------------------------------------------- + - name: context-as-argument # context.Context первым аргументом + - name: context-keys-type # Ключи контекста должны быть типизированы + - name: var-declaration # var x = 1 → x := 1 + - name: blank-imports # Запрет blank imports кроме main/test + - name: dot-imports # Запрет dot imports + - name: unexported-return # Публичная функция возвращает приватный тип + - name: exported # Экспортируемые идентификаторы должны быть задокументированы + arguments: + - "checkPrivateReceivers" + - "disableStutteringCheck" + + # ------------------------------------------------------------------------- + # Именование — Go proverb: "Good naming is like good coding: concise" + # ------------------------------------------------------------------------- + - name: var-naming + arguments: + - [ + "ID", + "URL", + "API", + "HTTP", + "JSON", + "XML", + "DB", + "SQL", + "UUID", + "UID", + "GUID", + "TTL", + "TCP", + "UDP", + "IP", + "RPC", + "QPS", + "EOF", + ] + - name: package-comments # Пакеты должны иметь комментарии + - name: receiver-naming # Имена receiver (r, s, c, не this/self) + exclusions: + paths: + - vendors/ + default: none + enable: + - govet + - errcheck + - misspell + - gocyclo + - ineffassign + - unparam + - unused + - prealloc + - durationcheck + - staticcheck + - makezero + - nilerr + - errorlint + - bodyclose + - gosec + - nilerr + - nilnesserr + - nilnil + - bidichk + - contextcheck + - fatcontext + - makezero + - forcetypeassert + - unconvert + - copyloopvar + - prealloc + - perfsprint + - gocritic + - goconst + - mnd + - revive + - predeclared + - reassign + - recvcheck + - asciicheck + - importas + - durationcheck + - tparallel + - thelper + - usetesting + - musttag + - errchkjson + - tagalign + - usestdlibvars + - nestif + - mirror + - whitespace + - decorder + - nonamedreturns + - inamedparam \ No newline at end of file diff --git a/.lic.yaml b/.lic.yaml new file mode 100755 index 0000000..ee29af9 --- /dev/null +++ b/.lic.yaml @@ -0,0 +1,3 @@ +author: "Mikhail Knyazhev " +lic_short: "BSD 3-Clause" +lic_file: LICENSE \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..4112b2c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules/ +**/dist/ +**/coverage/ +pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..04e09e4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6a5408f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# Repository instructions + +## Project map + +- `go-sdk/ui` contains the Go protocol types, builders, and validation. +- `go-sdk/mcp` adapts UI manifests and views to MCP resources. +- `react-sdk/ui-core`, `ui-transport`, and `ui-react` contain the TypeScript SDK packages. +- `example/go` and `example/react` are working examples; `schemas/` contains protocol JSON Schemas. + +Keep the protocol transport-independent. When changing protocol fields or behavior, check the Go schema and validation, React schema and validation, JSON Schemas, fixtures, and relevant examples for consistency. + +## Commands + +Run commands from the repository root: + +| Command | Purpose | +| --- | --- | +| `make install` | Install frozen pnpm workspace dependencies, install `goppy@latest`, and run `goppy setup-lib` | +| `make lint` | Run pnpm lint and formatting, then `goppy lint` | +| `make tests` | Run pnpm tests, then `goppy test` | +| `make build` | Build the three npm packages, run pnpm typecheck, then `goppy build --arch=amd64` | +| `make license` | Run `goppy license` | +| `make pre-commit` | Run install, license, lint, build, and tests in sequence | +| `make examples-dev` | Start the Vite React examples | +| `make npm-publish` | Run the full build, then publish the three `@osspkg` packages publicly | + +For focused TypeScript workflows, use `pnpm --filter typecheck`, `test`, or `build` where that script exists. Package names and scripts are defined in each workspace `package.json`. + +`make install` installs `goppy@latest` and runs `goppy setup-lib`. `make lint` runs `pnpm format`, which rewrites files before checking formatting, and the Go lint workflow may also update generated or formatted files. Inspect `git status` before running these workflows when preserving a worktree matters. `make pre-commit` runs install, license, lint, build, and tests in that order; there is no `make ci` target. + +`make tests` does not build npm packages. Run `make build` first in a clean checkout so package exports in `dist` exist. `npm-publish` runs the full build first and publishes publicly. Never run a publish command unless the user explicitly asked for a release. + +## Change guidance + +- Preserve the Go and TypeScript representations of the protocol in sync, including layout behavior for nested nodes. +- Keep JSON Schema documents and valid/invalid fixtures aligned with runtime validation. +- Prefer extending the existing SDK APIs and examples over introducing duplicate abstractions. +- Do not commit, publish, or run broad mutating workflows unless the user requests it. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f55727 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, Mikhail Knyazhev + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..8cfcb68 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ + +SHELL=/bin/bash + + +.PHONY: install +install: + pnpm install --frozen-lockfile + go install go.osspkg.com/goppy/v3/cmd/goppy@latest + goppy setup-lib + +.PHONY: lint +lint: + pnpm lint + pnpm format + pnpm format:check + goppy lint + +.PHONY: license +license: + goppy license + +.PHONY: build +build: + pnpm --filter @osspkg/ui-core build + pnpm --filter @osspkg/ui-transport build + pnpm --filter @osspkg/ui-react build + pnpm typecheck + goppy build --arch=amd64 + +.PHONY: tests +tests: + pnpm test + goppy test + +.PHONY: pre-commit +pre-commit: install license lint build tests + +.PHONY: examples-dev +examples-dev: + pnpm --filter @osspkg/ui-examples dev + +.PHONY: npm-version-patch +npm-version-patch: + cd react-sdk/ui-core && npm version patch --no-git-tag-version + cd react-sdk/ui-transport && npm version patch --no-git-tag-version + cd react-sdk/ui-react && npm version patch --no-git-tag-version + git add . && git commit -m "update lib version" + +.PHONY: npm-publish +npm-publish: build npm-version-patch + pnpm --filter @osspkg/ui-core publish --access public + pnpm --filter @osspkg/ui-transport publish --access public + pnpm --filter @osspkg/ui-react publish --access public diff --git a/README.md b/README.md index b8a6e43..fb9c8a4 100644 --- a/README.md +++ b/README.md @@ -1 +1,174 @@ -# go-ui \ No newline at end of file +# go-ui + +[![Go Reference](https://pkg.go.dev/badge/go.osspkg.com/ui/go-sdk/ui.svg)](https://pkg.go.dev/go.osspkg.com/ui/go-sdk/ui) + +`go-ui` is a declarative UI protocol and SDK for describing plugin interfaces as JSON schemas. The Go SDK builds and validates manifests and views; the React SDK validates those schemas and renders them with registered HTML and shadcn components. The UI model is independent of the transport, with an MCP adapter available in `go-sdk/mcp`. + +## Features + +- Declarative manifests, views, regions, nodes, layouts, data sources, actions, and effects. +- Go builders for composing and validating UI schemas. +- React packages for schema validation, transport, and rendering. +- Default HTML and shadcn component registries, with support for custom components. +- Child layout using `row`, `cols`, and `offset` in a 12-column grid. +- JSON Schemas for protocol objects in [`schemas/`](schemas/). + +## Repository layout + +| Path | Description | +| -------------------------------------------------- | ----------------------------------------------------------------- | +| [`go-sdk/ui`](go-sdk/ui) | Go schema types, builders, validation, and component definitions | +| [`go-sdk/mcp`](go-sdk/mcp) | Adapter that registers the UI manifest and views as MCP resources | +| [`react-sdk/ui-core`](react-sdk/ui-core) | TypeScript protocol types, validation, and runtime helpers | +| [`react-sdk/ui-transport`](react-sdk/ui-transport) | HTTP, WebSocket, and RPC transport helpers | +| [`react-sdk/ui-react`](react-sdk/ui-react) | React renderer, registries, and default components | +| [`example/go`](example/go) | Go examples for an HTML form and dashboard | +| [`example/react`](example/react) | Vite app with interactive React examples | +| [`schemas`](schemas) | JSON Schema documents for protocol entities | + +## Requirements + +- Go 1.26.8 or newer. +- Node.js and pnpm 12.6.0, as declared by the workspace package manifest. + +## Development + +Run these commands from the repository root. `make install` installs the pnpm workspace from the lockfile and installs/configures `goppy` for Go workflows. + +| Command | What it runs | +| --- | --- | +| `make install` | Frozen pnpm dependency install, `goppy@latest` installation, and `goppy setup-lib` | +| `make lint` | pnpm lint, formatting, formatting check, and `goppy lint` | +| `make tests` | pnpm tests and `goppy test` | +| `make build` | Build the three npm packages, run pnpm typecheck, and run `goppy build --arch=amd64` | +| `make license` | Run `goppy license` | +| `make pre-commit` | Run install, license, lint, build, and tests | +| `make examples-dev` | Start the React example app with Vite | + +`make lint` runs `pnpm format`, which can rewrite files. The Go lint and setup workflows can also update generated files. Review `git status` after running them. + +`make tests` does not build the npm packages. In a clean checkout, run `make build` before `make tests` so package exports in `dist` exist. `make npm-publish` runs the full build before publishing the packages publicly. + +## Go usage + +The root Go module is `go.osspkg.com/ui`. Add it to a Go module with: + +```bash +go get go.osspkg.com/ui +``` + +This example builds a profile view with a card and individually positioned child nodes: + +```go +package main + +import "go.osspkg.com/ui/go-sdk/ui" + +func newProfileApp() (*ui.App, error) { + app := ui.New( + ui.AppID("profile"), + ui.AppTitle("Profile"), + ui.AppVersion("1.0.0"), + ) + view := ui.View( + "profile.edit", + ui.Title("Edit profile"), + ui.Content( + ui.HTML(ui.HTMLArticle, "profile-card").Row(1).Cols(8).Offset(2).Children( + ui.HTML(ui.HTMLH1, "heading").Row(1).Cols(12).Prop("text", "Edit profile"), + ui.HTML(ui.HTMLInput, "name").Row(2).Cols(8).Offset(2).Prop("placeholder", "Ada Lovelace"), + ), + ), + ) + if err := app.AddView(view); err != nil { + return nil, err + } + return app, nil +} +``` + +To expose the manifest and views as MCP resources, register the app with the MCP adapter: + +```go +import ( + goMCP "go.osspkg.com/mcp" + "go.osspkg.com/ui/go-sdk/ui" + uiMCP "go.osspkg.com/ui/go-sdk/mcp" +) + +func registerUI(app *ui.App) error { + server, err := goMCP.New( + goMCP.ServerInfo{Name: "profile", Version: "1.0.0"}, + uiMCP.Capabilities(), + ) + if err != nil { + return err + } + return uiMCP.Register(server, app) +} +``` + +The adapter publishes resources; a host that uses the React runtime must separately handle `ui.get`, `data.call`, and `ui.action`. See the [dashboard example](example/go/dashboard/dashboard.go) for sources, actions, and effects. + +## React usage + +Install the packages used by your application: + +```bash +pnpm add @osspkg/ui-core @osspkg/ui-react +``` + +Register the built-in component sets, load the package stylesheet, and render a `ViewSchema`: + +```tsx +import { + createComponentRegistry, + registerDefaultHTMLComponents, + registerDefaultShadcnComponents, + UIProvider, + ViewRenderer, +} from "@osspkg/ui-react"; +import type { ViewSchema } from "@osspkg/ui-core"; +import "@osspkg/ui-react/styles.css"; + +const components = createComponentRegistry(); +registerDefaultHTMLComponents(components); +registerDefaultShadcnComponents(components); + +const schema: ViewSchema = { + protocolVersion: "1.0", + id: "welcome", + title: "Welcome", + regions: { + "top-header": [], + "left-panel": [], + "right-panel": [], + bottom: [], + content: [ + { + id: "welcome-card", + component: "card", + layout: { row: 1, cols: 8, offset: 2 }, + children: [ + { id: "title", component: "h1", layout: { row: 1, cols: 12 }, props: { text: "Welcome" } }, + { id: "body", component: "p", layout: { row: 2, cols: 8, offset: 2 }, props: { text: "Rendered from a schema." } }, + ], + }, + ], + }, +}; + +export function WelcomeView() { + return ( + + + + ); +} +``` + +Nodes with children create their own 12-column grid. Set a child's `layout` to control its row, width (`cols`), and horizontal offset; nodes without a layout use the available row. The [interactive example](example/react/README.md) demonstrates forms, default registries, and nested layouts. + +## License + +This project is licensed under the [BSD 3-Clause License](LICENSE). diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..7326b0c --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,60 @@ +import { FlatCompat } from "@eslint/eslintrc"; +import js from "@eslint/js"; +import globals from "globals"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import tseslint from "typescript-eslint"; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default tseslint.config( + { + ignores: ["**/dist/**", "**/node_modules/**", "**/coverage/**", "**/*.d.ts"], + }, + ...compat.extends("airbnb", "airbnb/hooks"), + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + settings: { + react: { version: "19.0" }, + }, + rules: { + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "react/jsx-filename-extension": ["error", { extensions: [".jsx", ".tsx"] }], + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "react/require-default-props": "off", + "react/destructuring-assignment": "off", + "react/jsx-props-no-spreading": "off", + "react/no-array-index-key": "off", + "import/prefer-default-export": "off", + "no-nested-ternary": "off", + "consistent-return": "off", + "import/no-extraneous-dependencies": "off", + "import/no-unresolved": "off", + "import/extensions": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": "off", + "no-restricted-syntax": "off", + "no-await-in-loop": "off", + "no-continue": "off", + "no-void": "off", + "lines-between-class-members": "off", + "class-methods-use-this": "off", + "no-bitwise": "off", + "no-plusplus": "off", + "no-shadow": "off", + "no-param-reassign": "off", + }, + }, + ...compat.extends("prettier"), +); diff --git a/example/go/dashboard/dashboard.go b/example/go/dashboard/dashboard.go new file mode 100644 index 0000000..1b57396 --- /dev/null +++ b/example/go/dashboard/dashboard.go @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package dashboard demonstrates shadcn components, grid placement, and data sources. +package dashboard + +import "go.osspkg.com/ui/go-sdk/ui" + +const ( + metricsCacheTTL = 30 + headerTitleCols = 9 + refreshButtonCols = 3 + refreshButtonPos = 9 + metricCardCols = 6 + latencyCardOffset = 6 + eventTableCols = 12 + eventTableRow = 2 +) + +// NewApp builds a dashboard whose metrics source is loaded when the view opens. +func NewApp() (*ui.App, error) { + app := ui.New( + ui.AppID("metrics"), + ui.AppTitle("Metrics"), + ui.AppVersion("1.0.0"), + ) + view := ui.View( + "metrics.dashboard", + ui.Title("Metrics dashboard"), + ui.Source("metrics", ui.Tool("metrics.summary").OnMount().TTL(metricsCacheTTL)), + ui.TopHeader( + ui.HTML(ui.HTMLH1, "title").Row(1).Cols(headerTitleCols).Prop( + "text", + "Metrics dashboard", + ), + ui.Shadcn( + ui.ShadcnButton, + "refresh", + ).Row(1).Cols(refreshButtonCols).Offset(refreshButtonPos).Prop("text", "Refresh").On( + "click", + ui.ActionRef("metrics.refresh"), + ), + ), + ui.Content( + ui.Shadcn(ui.ShadcnCard, "requests-card").Row(1).Cols(metricCardCols).Children( + ui.HTML(ui.HTMLH2, "requests-heading").Prop("text", "Requests"), + ui.Shadcn(ui.ShadcnTypography, "requests-value").Prop( + "text", + ui.SourceRef("metrics.requests"), + ), + ), + ui.Shadcn( + ui.ShadcnCard, + "latency-card", + ).Row(1).Cols(metricCardCols).Offset(latencyCardOffset).Children( + ui.HTML(ui.HTMLH2, "latency-heading").Prop("text", "Latency"), + ui.Shadcn(ui.ShadcnTypography, "latency-value").Prop( + "text", + ui.SourceRef("metrics.latency"), + ), + ), + ui.DataTable("recent-events").Row(eventTableRow).Cols(eventTableCols).Prop( + "rows", + ui.SourceRef("metrics.events"), + ).Prop("loading", ui.SourceRef("metrics.$loading")).Prop( + "error", + ui.SourceRef("metrics.$error"), + ), + ), + ).Action( + "metrics.refresh", + ui.CallTool("metrics.refresh").Then( + ui.RefreshSource("metrics"), + ui.ToastSuccess("Metrics refreshed"), + ), + ) + if err := app.AddView(view); err != nil { + return nil, err + } + return app, nil +} diff --git a/example/go/dashboard/dashboard_test.go b/example/go/dashboard/dashboard_test.go new file mode 100644 index 0000000..00e8fb5 --- /dev/null +++ b/example/go/dashboard/dashboard_test.go @@ -0,0 +1,20 @@ +package dashboard + +import "testing" + +func TestUnit_NewApp(t *testing.T) { + app, err := NewApp() + if err != nil { + t.Fatal(err) + } + if err := app.Manifest().Validate(); err != nil { + t.Fatal(err) + } + view, ok := app.View("metrics.dashboard") + if !ok { + t.Fatal("dashboard view is missing") + } + if len(view.Regions.Content) != 3 { + t.Fatalf("content nodes = %d, want 3", len(view.Regions.Content)) + } +} diff --git a/example/go/htmlform/htmlform.go b/example/go/htmlform/htmlform.go new file mode 100644 index 0000000..5133ec3 --- /dev/null +++ b/example/go/htmlform/htmlform.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package htmlform demonstrates a declarative form built from safe HTML tags. +package htmlform + +import "go.osspkg.com/ui/go-sdk/ui" + +const ( + profileCardCols = 8 + profileCardOffset = 2 +) + +// NewApp builds a profile form with native HTML elements and a local action. +func NewApp() (*ui.App, error) { + app := ui.New( + ui.AppID("profile"), + ui.AppTitle("Profile"), + ui.AppVersion("1.0.0"), + ) + view := ui.View( + "profile.edit", + ui.Title("Profile"), + ui.State( + map[string]any{ + "profile": map[string]any{"name": ""}, + "submitted": false, + }, + ), + ui.Content( + ui.HTML( + ui.HTMLArticle, + "profile-card", + ).Row(1).Cols(profileCardCols).Offset(profileCardOffset).Children( + ui.HTML(ui.HTMLH1, "title").Prop("text", "Edit profile"), + ui.HTML(ui.HTMLP, "hint").Prop( + "text", + "Changes are saved when the form is submitted.", + ), + ui.HTML(ui.HTMLForm, "profile-form").On( + "submit", + ui.ActionRef("profile.submit"), + ).Children( + ui.HTML(ui.HTMLLabel, "name-label").Prop("text", "Name"), + ui.HTML(ui.HTMLInput, "name").Prop( + "name", + "name", + ).Prop( + "value", + ui.StateRef("profile.name"), + ).Prop("placeholder", "Ada Lovelace"), + ui.HTML(ui.HTMLButton, "save").Prop( + "type", + "submit", + ).Prop("text", "Save profile"), + ), + ), + ), + ).Action( + "profile.submit", + ui.SetState("submitted", true).Then(ui.ToastSuccess("Profile saved")), + ) + if err := app.AddView(view); err != nil { + return nil, err + } + return app, nil +} diff --git a/example/go/htmlform/htmlform_test.go b/example/go/htmlform/htmlform_test.go new file mode 100644 index 0000000..d8aa3c2 --- /dev/null +++ b/example/go/htmlform/htmlform_test.go @@ -0,0 +1,16 @@ +package htmlform + +import "testing" + +func TestUnit_NewApp(t *testing.T) { + app, err := NewApp() + if err != nil { + t.Fatal(err) + } + if err := app.Manifest().Validate(); err != nil { + t.Fatal(err) + } + if _, ok := app.View("profile.edit"); !ok { + t.Fatal("profile view is missing") + } +} diff --git a/example/react/README.md b/example/react/README.md new file mode 100644 index 0000000..dc96429 --- /dev/null +++ b/example/react/README.md @@ -0,0 +1,40 @@ +# React examples + +These TypeScript entrypoints are intended for a React application that has +`@osspkg/ui-react` and `@osspkg/ui-core` installed. Each imports the packaged +Tailwind v4 stylesheet and uses only the public API. + +The directory is a private workspace package, so React and its JSX runtime are +installed for the examples themselves. Check it with: + +```bash +pnpm --filter @osspkg/ui-examples typecheck +``` + +To browse the examples locally, run this from the repository root: + +```bash +pnpm --filter @osspkg/ui-examples dev +``` + +Vite prints the local address (normally http://localhost:5173). Use the +buttons at the top of the page to switch between the examples. + +- [`basic.tsx`](basic.tsx) renders a static schema. +- [`html-form.tsx`](html-form.tsx) demonstrates native HTML, local state, and a + submit action. +- [`dashboard.tsx`](dashboard.tsx) demonstrates shadcn defaults and the + 12-column `row`/`cols`/`offset` layout. + +Each node with children renders them in their own 12-column grid. A child node +can use `layout` to choose its row, width, and offset within that parent. Nodes +without a layout fill the available row. + +Render one exported component from an application entrypoint, for example: + +```tsx +import { createRoot } from "react-dom/client"; +import { DashboardExample } from "./dashboard"; + +createRoot(document.getElementById("root")!).render(); +``` diff --git a/example/react/basic.tsx b/example/react/basic.tsx new file mode 100644 index 0000000..e7495d6 --- /dev/null +++ b/example/react/basic.tsx @@ -0,0 +1,55 @@ +import type { ReactElement } from "react"; +import { + createComponentRegistry, + registerDefaultHTMLComponents, + registerDefaultShadcnComponents, + UIProvider, + ViewRenderer, +} from "@osspkg/ui-react"; +import type { ViewSchema } from "@osspkg/ui-core"; +import "@osspkg/ui-react/styles.css"; + +const components = createComponentRegistry(); +registerDefaultHTMLComponents(components); +registerDefaultShadcnComponents(components); + +const schema: ViewSchema = { + protocolVersion: "1.0", + id: "welcome", + title: "Welcome", + regions: { + "top-header": [], + "left-panel": [], + "right-panel": [], + bottom: [], + content: [ + { + id: "welcome-card", + component: "card", + layout: { row: 1, cols: 8, offset: 2 }, + children: [ + { + id: "title", + component: "h1", + layout: { row: 1, cols: 12 }, + props: { text: "Welcome" }, + }, + { + id: "body", + component: "p", + layout: { row: 2, cols: 8, offset: 2 }, + props: { text: "This view is rendered from a declarative schema." }, + }, + ], + }, + ], + }, +}; + +export function BasicExample(): ReactElement { + return ( + + + + ); +} diff --git a/example/react/dashboard.tsx b/example/react/dashboard.tsx new file mode 100644 index 0000000..1c6a3f6 --- /dev/null +++ b/example/react/dashboard.tsx @@ -0,0 +1,66 @@ +import type { ReactElement } from "react"; +import { + createComponentRegistry, + registerDefaultHTMLComponents, + registerDefaultShadcnComponents, + UIProvider, + ViewRenderer, +} from "@osspkg/ui-react"; +import type { ViewSchema } from "@osspkg/ui-core"; +import "@osspkg/ui-react/styles.css"; + +const components = createComponentRegistry(); +registerDefaultHTMLComponents(components); +registerDefaultShadcnComponents(components); + +const schema: ViewSchema = { + protocolVersion: "1.0", + id: "metrics.dashboard", + regions: { + "top-header": [ + { + id: "title", + component: "h1", + layout: { row: 1, cols: 12 }, + props: { text: "Metrics dashboard" }, + }, + ], + "left-panel": [], + "right-panel": [], + bottom: [], + content: [ + { + id: "requests", + component: "card", + layout: { row: 1, cols: 6 }, + props: { text: "Requests: 12,480" }, + }, + { + id: "latency", + component: "card", + layout: { row: 1, cols: 6, offset: 6 }, + props: { text: "p95 latency: 184 ms" }, + }, + { + id: "events", + component: "data-table", + layout: { row: 2, cols: 12 }, + props: { + "aria-label": "Recent events", + rows: [ + { event: "user.created", status: "success" }, + { event: "user.deleted", status: "success" }, + ], + }, + }, + ], + }, +}; + +export function DashboardExample(): ReactElement { + return ( + + + + ); +} diff --git a/example/react/favicon.svg b/example/react/favicon.svg new file mode 100644 index 0000000..0bff326 --- /dev/null +++ b/example/react/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/example/react/html-form.tsx b/example/react/html-form.tsx new file mode 100644 index 0000000..ab9b422 --- /dev/null +++ b/example/react/html-form.tsx @@ -0,0 +1,84 @@ +import type { ReactElement } from "react"; +import { + createComponentRegistry, + registerDefaultHTMLComponents, + registerDefaultShadcnComponents, + UIProvider, + ViewRenderer, +} from "@osspkg/ui-react"; +import type { ViewSchema } from "@osspkg/ui-core"; +import "@osspkg/ui-react/styles.css"; + +const components = createComponentRegistry(); +registerDefaultHTMLComponents(components); +registerDefaultShadcnComponents(components); + +const schema: ViewSchema = { + protocolVersion: "1.0", + id: "profile.edit", + state: { submitted: false }, + actions: { + save: { type: "set-state", path: "submitted", value: true }, + }, + regions: { + "top-header": [], + "left-panel": [], + "right-panel": [], + bottom: [], + content: [ + { + id: "form-card", + component: "article", + layout: { row: 1, cols: 8, offset: 2 }, + children: [ + { + id: "heading", + component: "h1", + layout: { row: 1, cols: 12 }, + props: { text: "Edit profile" }, + }, + { + id: "profile-form", + component: "form", + events: { submit: { action: "save" } }, + children: [ + { + id: "name-label", + component: "label", + layout: { row: 1, cols: 12 }, + props: { htmlFor: "profile-name", text: "Name" }, + }, + { + id: "name", + component: "input", + layout: { row: 2, cols: 8, offset: 2 }, + props: { id: "profile-name", name: "name", placeholder: "Ada Lovelace" }, + }, + { + id: "save", + component: "button", + layout: { row: 3, cols: 8, offset: 2 }, + props: { type: "submit", text: "Save profile" }, + }, + ], + }, + { + id: "confirmation", + component: "p", + when: { $eq: [{ $state: "submitted" }, true] }, + layout: { row: 4, cols: 8, offset: 2 }, + props: { role: "status", text: "Profile saved." }, + }, + ], + }, + ], + }, +}; + +export function HTMLFormExample(): ReactElement { + return ( + + + + ); +} diff --git a/example/react/index.html b/example/react/index.html new file mode 100644 index 0000000..ead9c12 --- /dev/null +++ b/example/react/index.html @@ -0,0 +1,18 @@ + + + + + + + + + UI React examples + + +
+ + + diff --git a/example/react/main.tsx b/example/react/main.tsx new file mode 100644 index 0000000..dabb8dc --- /dev/null +++ b/example/react/main.tsx @@ -0,0 +1,64 @@ +import { createRoot } from "react-dom/client"; +import { useState, type ComponentType, type ReactElement } from "react"; +import { BasicExample } from "./basic.js"; +import { DashboardExample } from "./dashboard.js"; +import { HTMLFormExample } from "./html-form.js"; +import "./preview.css"; + +const examples = { + Basic: BasicExample, + "HTML form": HTMLFormExample, + Dashboard: DashboardExample, +} satisfies Record; + +type ExampleName = keyof typeof examples; +const exampleNames = Object.keys(examples) as ExampleName[]; + +function ExamplesApp(): ReactElement { + const [name, setName] = useState("Basic"); + const Example = examples[name]; + + return ( +
+
+
+

@osspkg/ui-react

+

+ Interactive examples +

+

+ Preview declarative schemas rendered with the default HTML and shadcn component + registries. +

+
+ +
+ +
+
+
+ ); +} + +const rootElement = document.getElementById("root"); +if (!rootElement) throw new Error("Missing React root element"); + +createRoot(rootElement).render(); diff --git a/example/react/package.json b/example/react/package.json new file mode 100644 index 0000000..da4bfda --- /dev/null +++ b/example/react/package.json @@ -0,0 +1,25 @@ +{ + "name": "@osspkg/ui-examples", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "preview": "vite preview", + "typecheck": "tsc -p tsconfig.json" + }, + "dependencies": { + "@osspkg/ui-core": "workspace:*", + "@osspkg/ui-react": "workspace:*" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.3.3", + "typescript": "^5.9.0", + "vite": "^7.3.6" + } +} diff --git a/example/react/preview.css b/example/react/preview.css new file mode 100644 index 0000000..f999fb6 --- /dev/null +++ b/example/react/preview.css @@ -0,0 +1,93 @@ +@import "tailwindcss"; + +@source "./"; + +@theme { + --color-ui-background: oklch(1 0 0); + --color-ui-foreground: oklch(0.145 0 0); + --color-ui-card: oklch(1 0 0); + --color-ui-card-foreground: oklch(0.145 0 0); + --color-ui-primary: oklch(0.205 0 0); + --color-ui-primary-foreground: oklch(0.985 0 0); + --color-ui-muted: oklch(0.97 0 0); + --color-ui-muted-foreground: oklch(0.556 0 0); + --color-ui-border: oklch(0.922 0 0); + --color-ui-ring: oklch(0.708 0 0); +} + +:root { + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + color: #171717; + background: #f5f6f8; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; +} + +button, +input, +select, +textarea { + font: inherit; +} + +#root { + min-height: 100vh; +} + +button { + cursor: pointer; +} + +button[aria-pressed="true"] { + border-color: #171717; + background: #171717; + color: #fff; +} +button:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible { + outline: 3px solid #2563eb; + outline-offset: 3px; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} + +[data-ui-node] h1 { + margin: 0 0 1rem; + font-size: 1.5rem; + line-height: 1.3; + font-weight: 650; + letter-spacing: -0.025em; +} + +@media (max-width: 640px) { + [data-ui-node] { + grid-column: 1 / -1; + grid-row: auto !important; + } +} diff --git a/example/react/preview.css.d.ts b/example/react/preview.css.d.ts new file mode 100644 index 0000000..a9f6f08 --- /dev/null +++ b/example/react/preview.css.d.ts @@ -0,0 +1,3 @@ +declare const stylesheet: string; + +export default stylesheet; diff --git a/example/react/tsconfig.json b/example/react/tsconfig.json new file mode 100644 index 0000000..ade9ebf --- /dev/null +++ b/example/react/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "noEmit": true, + "noUncheckedSideEffectImports": true, + "baseUrl": "../..", + "paths": { + "@osspkg/ui-core": ["react-sdk/ui-core/src/index.ts"], + "@osspkg/ui-react": ["react-sdk/ui-react/src/index.ts"], + "@osspkg/ui-transport": ["react-sdk/ui-transport/src/index.ts"], + "@osspkg/ui-react/styles.css": ["react-sdk/ui-react/src/styles.css.d.ts"] + } + }, + "include": ["*.tsx"] +} diff --git a/example/react/vite.config.ts b/example/react/vite.config.ts new file mode 100644 index 0000000..337a06f --- /dev/null +++ b/example/react/vite.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath, URL } from "node:url"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +const source = (path: string) => fileURLToPath(new URL(path, import.meta.url)); + +export default defineConfig({ + plugins: [tailwindcss()], + resolve: { + alias: [ + { + find: "@osspkg/ui-react/styles.css", + replacement: source("../../react-sdk/ui-react/src/styles.css"), + }, + { find: "@osspkg/ui-core", replacement: source("../../react-sdk/ui-core/src/index.ts") }, + { find: "@osspkg/ui-react", replacement: source("../../react-sdk/ui-react/src/index.ts") }, + { + find: "@osspkg/ui-transport", + replacement: source("../../react-sdk/ui-transport/src/index.ts"), + }, + ], + }, +}); diff --git a/fixtures/invalid-layout.json b/fixtures/invalid-layout.json new file mode 100644 index 0000000..00cf088 --- /dev/null +++ b/fixtures/invalid-layout.json @@ -0,0 +1,13 @@ +{ + "protocolVersion": "1.0", + "id": "invalid", + "regions": { + "top-header": [], + "left-panel": [], + "content": [ + {"id": "broken", "component": "card", "layout": {"row": 1, "cols": 8, "offset": 5}} + ], + "right-panel": [], + "bottom": [] + } +} diff --git a/fixtures/valid-view.json b/fixtures/valid-view.json new file mode 100644 index 0000000..7eb17b8 --- /dev/null +++ b/fixtures/valid-view.json @@ -0,0 +1,32 @@ +{ + "protocolVersion": "1.0", + "id": "users.list", + "revision": "rev-1", + "title": "Users", + "state": {"filters": {"search": ""}}, + "sources": { + "users": { + "type": "tool", + "tool": "users.list", + "input": {"search": {"$state": "filters.search"}}, + "policy": "on-mount" + } + }, + "actions": { + "users.refresh": {"type": "refresh-source", "source": "users"} + }, + "regions": { + "top-header": [], + "left-panel": [], + "content": [ + { + "id": "users-table", + "component": "table", + "layout": {"row": 1, "cols": 12}, + "props": {"rows": {"$source": "users.items"}} + } + ], + "right-panel": [], + "bottom": [] + } +} diff --git a/go-sdk/mcp/register.go b/go-sdk/mcp/register.go new file mode 100644 index 0000000..c2e5c9a --- /dev/null +++ b/go-sdk/mcp/register.go @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package mcp registers declarative UI resources on a go-mcp server. +package mcp + +import ( + "encoding/json" + "errors" + "strings" + + ossmcp "go.osspkg.com/mcp" + + "go.osspkg.com/ui/go-sdk/ui" +) + +// Capabilities returns an MCP option that advertises the UI protocol version +// and the ui://manifest resource as an experimental osspkg.ui capability. +func Capabilities() ossmcp.Option { + return ossmcp.WithCapabilities( + map[string]any{ + "experimental": map[string]any{ + "osspkg.ui": map[string]any{ + "version": ui.ProtocolVersion, + "manifest": "ui://manifest", + }, + }, + }, + ) +} + +// Register validates app and registers its manifest and declared views as MCP +// resources on server. It returns an error for nil arguments, invalid app data, +// missing manifest views, or resource registration failures. Resources already +// registered before a failure are not rolled back. +func Register(server *ossmcp.Server, app *ui.App) error { + if server == nil || app == nil { + return errors.New("ui/mcp: server and app are required") + } + + manifest, err := app.ManifestJSON() + if err != nil { + return err + } + + if err = app.Manifest().Validate(); err != nil { + return err + } + + if err = server.RegisterResource( + ossmcp.Resource{ + URI: "ui://manifest", + Name: "UI Manifest", + Description: "UI entry points exposed by the application", + MIMEType: ui.UIMIMEType, + Text: string(manifest), + }, + ); err != nil { + return err + } + + for _, descriptor := range app.Manifest().Views { + view, ok := app.View(descriptor.ID) + if !ok { + return errors.New("ui/mcp: manifest view is missing") + } + + payload, err := json.Marshal(view) + if err != nil { + return err + } + + name := descriptor.Title + if strings.TrimSpace(name) == "" { + name = descriptor.ID + } + + if err := server.RegisterResource( + ossmcp.Resource{ + URI: descriptor.Schema, + Name: name, + MIMEType: ui.UIMIMEType, + Text: string(payload), + }, + ); err != nil { + return err + } + } + + return nil +} diff --git a/go-sdk/mcp/register_test.go b/go-sdk/mcp/register_test.go new file mode 100644 index 0000000..99862ff --- /dev/null +++ b/go-sdk/mcp/register_test.go @@ -0,0 +1,56 @@ +package mcp + +import ( + "testing" + + goMCP "go.osspkg.com/mcp" + + "go.osspkg.com/ui/go-sdk/ui" +) + +func TestUnit_Register(t *testing.T) { + server, err := goMCP.New( + goMCP.ServerInfo{Name: "users", Version: "1.0.0"}, + Capabilities(), + ) + if err != nil { + t.Fatal(err) + } + app := ui.New( + ui.AppID("users"), + ui.AppTitle("Users"), + ui.AppVersion("1.0.0"), + ) + if err := app.AddView( + ui.View( + "users.list", + ui.Title("Users"), + ), + ); err != nil { + t.Fatal(err) + } + if err := Register(server, app); err != nil { + t.Fatal(err) + } +} + +func TestUnit_RegisterViewWithoutTitle(t *testing.T) { + server, err := goMCP.New( + goMCP.ServerInfo{Name: "users", Version: "1.0.0"}, + Capabilities(), + ) + if err != nil { + t.Fatal(err) + } + app := ui.New( + ui.AppID("users"), + ui.AppTitle("Users"), + ui.AppVersion("1.0.0"), + ) + if err := app.AddView(ui.View("users.list")); err != nil { + t.Fatal(err) + } + if err := Register(server, app); err != nil { + t.Fatal(err) + } +} diff --git a/go-sdk/ui/builder.go b/go-sdk/ui/builder.go new file mode 100644 index 0000000..0eff0c7 --- /dev/null +++ b/go-sdk/ui/builder.go @@ -0,0 +1,497 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package ui + +import ( + "encoding/json" + "fmt" +) + +// App builds and stores declarative UI views for a plugin. +type App struct { + manifest Manifest + views map[string]ViewSchema +} + +// AppOption configures an App during construction. +type AppOption func(*App) + +// AppID sets the plugin identifier in the application manifest. +func AppID(value string) AppOption { + return func(app *App) { + app.manifest.Plugin.ID = value + } +} + +// AppTitle sets the plugin title in the application manifest. +func AppTitle(value string) AppOption { + return func(app *App) { + app.manifest.Plugin.Title = value + } +} + +// AppVersion sets the plugin version in the application manifest. +func AppVersion(value string) AppOption { + return func(app *App) { + app.manifest.Plugin.Version = value + } +} + +// New creates an application with the supplied manifest options. +func New(options ...AppOption) *App { + app := &App{ + manifest: Manifest{ + ProtocolVersion: ProtocolVersion, + Views: []UIViewDescriptor{}, + }, + views: make(map[string]ViewSchema), + } + + for _, option := range options { + option(app) + } + + return app +} + +// AddView validates and registers a snapshot of view in the application. +// It returns ErrInvalidSchema when the view ID is already registered. +func (app *App) AddView(view *ViewBuilder) error { + schema := view.Schema() + if err := schema.Validate(); err != nil { + return err + } + + if _, exists := app.views[schema.ID]; exists { + return ErrInvalidSchema + } + + snapshot, err := cloneViewSchema(schema) + if err != nil { + return fmt.Errorf("%w: copy view schema: %w", ErrInvalidSchema, err) + } + + app.views[schema.ID] = snapshot + title := schema.Title + if title == "" { + title = schema.ID + } + app.manifest.Views = append( + app.manifest.Views, + UIViewDescriptor{ + ID: schema.ID, + Title: title, + Schema: "ui://views/" + schema.ID, + }, + ) + + return nil +} + +// Manifest returns a copy of the application manifest. +func (app *App) Manifest() Manifest { + return cloneManifest(app.manifest) +} + +// ManifestJSON serializes the application manifest as JSON. It returns an +// error if the manifest contains a value that cannot be encoded. +func (app *App) ManifestJSON() ([]byte, error) { + return json.Marshal(app.Manifest()) +} + +// View returns a copy of the registered view with id. The boolean is false +// when no view is registered under that ID or the stored schema cannot be +// cloned. +func (app *App) View(id string) (ViewSchema, bool) { + view, ok := app.views[id] + if !ok { + return ViewSchema{}, false + } + + snapshot, err := cloneViewSchema(view) + if err != nil { + return ViewSchema{}, false + } + + return snapshot, true +} + +func cloneManifest(manifest Manifest) Manifest { + if manifest.Views != nil { + manifest.Views = append(make([]UIViewDescriptor, 0, len(manifest.Views)), manifest.Views...) + } + + if manifest.RequiredComponents != nil { + manifest.RequiredComponents = append( + make([]string, 0, len(manifest.RequiredComponents)), + manifest.RequiredComponents..., + ) + } + + return manifest +} + +func cloneViewSchema(schema ViewSchema) (ViewSchema, error) { + payload, err := json.Marshal(schema) + if err != nil { + return ViewSchema{}, err + } + + var clone ViewSchema + if err = json.Unmarshal(payload, &clone); err != nil { + return ViewSchema{}, err + } + + return clone, nil +} + +// ViewBuilder incrementally constructs a ViewSchema. +type ViewBuilder struct { + schema ViewSchema +} + +// ViewOption configures a view during construction. +type ViewOption func(*ViewSchema) + +// View creates a view builder with the supplied ID and options. +func View(id string, options ...ViewOption) *ViewBuilder { + builder := &ViewBuilder{ + schema: ViewSchema{ + ProtocolVersion: ProtocolVersion, + ID: id, + Regions: emptyRegions(), + Sources: map[string]DataSource{}, + Actions: map[string]Action{}, + }, + } + + for _, option := range options { + option(&builder.schema) + } + + return builder +} + +func emptyRegions() Regions { + return Regions{ + TopHeader: []Node{}, + LeftPanel: []Node{}, + Content: []Node{}, + RightPanel: []Node{}, + Bottom: []Node{}, + } +} + +// Title sets the view title. +func Title(value string) ViewOption { + return func(schema *ViewSchema) { + schema.Title = value + } +} + +// State sets the initial view state. +func State(value map[string]any) ViewOption { + return func(schema *ViewSchema) { + schema.State = value + } +} + +// TopHeader places nodes in the top header region. +func TopHeader(nodes ...*NodeBuilder) ViewOption { + return func(schema *ViewSchema) { + schema.Regions.TopHeader = buildNodes(nodes) + } +} + +// LeftPanel places nodes in the left panel region. +func LeftPanel(nodes ...*NodeBuilder) ViewOption { + return func(schema *ViewSchema) { + schema.Regions.LeftPanel = buildNodes(nodes) + } +} + +// Content places nodes in the content region. +func Content(nodes ...*NodeBuilder) ViewOption { + return func(schema *ViewSchema) { + schema.Regions.Content = buildNodes(nodes) + } +} + +// RightPanel places nodes in the right panel region. +func RightPanel(nodes ...*NodeBuilder) ViewOption { + return func(schema *ViewSchema) { + schema.Regions.RightPanel = buildNodes(nodes) + } +} + +// Bottom places nodes in the bottom region. +func Bottom(nodes ...*NodeBuilder) ViewOption { + return func(schema *ViewSchema) { + schema.Regions.Bottom = buildNodes(nodes) + } +} + +// Action adds or replaces the named action in the view. +func (view *ViewBuilder) Action(name string, action *ActionBuilder) *ViewBuilder { + view.schema.Actions[name] = action.action + return view +} + +// Schema returns the current view schema. The returned value shares its maps +// and slices with the builder; use it as a read-only snapshot unless you +// intend to change the builder's data. +func (view *ViewBuilder) Schema() ViewSchema { + return view.schema +} + +// Revision sets the revision identifier used to distinguish view versions. +func (view *ViewBuilder) Revision(value string) *ViewBuilder { + view.schema.Revision = value + return view +} + +// Source adds or replaces the named data source in the view. +func (view *ViewBuilder) Source(name string, source *SourceBuilder) *ViewBuilder { + view.schema.Sources[name] = source.source + return view +} + +// NodeBuilder incrementally constructs a UI node. +type NodeBuilder struct{ node Node } + +func node(component, id string) *NodeBuilder { + return &NodeBuilder{ + node: Node{ + Component: component, + ID: id, + Props: map[string]Value{}, + Events: map[string]EventHandler{}, + }, + } +} + +// Row sets the node's grid row. +func (node *NodeBuilder) Row(row int) *NodeBuilder { + node.ensureLayout() + node.node.Layout.Row = row + return node +} + +// Cols sets the node's grid column span. +func (node *NodeBuilder) Cols(cols int) *NodeBuilder { + node.ensureLayout() + node.node.Layout.Cols = cols + return node +} + +// Offset sets the node's grid column offset. +func (node *NodeBuilder) Offset(offset int) *NodeBuilder { + node.ensureLayout() + node.node.Layout.Offset = offset + return node +} + +// Prop sets a named node property. +func (node *NodeBuilder) Prop(name string, value any) *NodeBuilder { + node.node.Props[name] = toValue(value) + return node +} + +// On attaches an event handler to the node. +func (node *NodeBuilder) On(name string, handler EventHandler) *NodeBuilder { + node.node.Events[name] = handler + return node +} + +// Children sets the node's child nodes. +func (node *NodeBuilder) Children(children ...*NodeBuilder) *NodeBuilder { + node.node.Children = buildNodes(children) + return node +} + +// Slots sets the nodes in a named slot. +func (node *NodeBuilder) Slots(name string, children ...*NodeBuilder) *NodeBuilder { + if node.node.Slots == nil { + node.node.Slots = map[string][]Node{} + } + + node.node.Slots[name] = buildNodes(children) + + return node +} + +// When sets the conditional expression for the node. +func (node *NodeBuilder) When(value Value) *NodeBuilder { + node.node.When = &value + return node +} + +// Build returns the current node value. The returned node shares maps and +// slices with the builder; use it as a read-only snapshot unless you intend to +// change the builder's data. +func (node *NodeBuilder) Build() Node { + return node.node +} + +func (node *NodeBuilder) ensureLayout() { + if node.node.Layout == nil { + node.node.Layout = &Layout{} + } +} + +func buildNodes(nodes []*NodeBuilder) []Node { + result := make([]Node, 0, len(nodes)) + for _, n := range nodes { + result = append(result, n.Build()) + } + + return result +} + +// SourceBuilder incrementally constructs a data source. +type SourceBuilder struct{ source DataSource } + +// Source adds a named data source to a view. +func Source(name string, source *SourceBuilder) ViewOption { + return func(schema *ViewSchema) { + if schema.Sources == nil { + schema.Sources = map[string]DataSource{} + } + + schema.Sources[name] = source.source + } +} + +// Tool creates a tool-backed data source builder. +func Tool(name string) *SourceBuilder { + return &SourceBuilder{ + source: DataSource{ + Type: "tool", + Tool: name, + Policy: SourceManual, + Input: map[string]Value{}, + }, + } +} + +// Input adds an input value to the data source. +func (source *SourceBuilder) Input(name string, value any) *SourceBuilder { + source.source.Input[name] = toValue(value) + return source +} + +// OnMount configures the source to load when the view mounts. +func (source *SourceBuilder) OnMount() *SourceBuilder { + source.source.Policy = SourceOnMount + return source +} + +// TTL sets the source cache lifetime in seconds. A negative value is retained +// by the builder but causes view validation to fail. +func (source *SourceBuilder) TTL(value int64) *SourceBuilder { + source.source.Cache = &CachePolicy{TTL: value} + return source +} + +// ActionBuilder incrementally constructs an action. +type ActionBuilder struct{ action Action } + +// CallTool creates a tool-calling action builder. +func CallTool(name string) *ActionBuilder { + return &ActionBuilder{ + action: Action{ + Type: actionTypeTool, + Tool: name, + Input: map[string]Value{}, + }, + } +} + +// SetState creates an action that replaces a state path with value. +func SetState(path string, value any) *ActionBuilder { + return &ActionBuilder{ + action: Action{ + Type: actionTypeSetState, + Path: path, + Value: ptrValue(value), + }, + } +} + +// MergeState creates an action that merges value into a state path. +func MergeState(path string, value any) *ActionBuilder { + return &ActionBuilder{ + action: Action{ + Type: actionTypeMergeState, + Path: path, + Value: ptrValue(value), + }, + } +} + +// Input adds an input value to the action. +func (action *ActionBuilder) Input(name string, value any) *ActionBuilder { + action.action.Input[name] = toValue(value) + return action +} + +// Then appends effects to the action. +func (action *ActionBuilder) Then(effects ...Effect) *ActionBuilder { + action.action.Effects = append(action.action.Effects, effects...) + return action +} + +// ActionRef creates an event handler that invokes a named action. +func ActionRef(name string) EventHandler { + return EventHandler{Action: name} +} + +// Invalidate creates an effect that invalidates a data source. +func Invalidate(source string) Effect { + return Effect{ + Type: actionTypeInvalidate, + Source: source, + } +} + +// RefreshSource creates an effect that refreshes a data source. +func RefreshSource(source string) Effect { + return Effect{ + Type: actionTypeRefreshSource, + Source: source, + } +} + +// ToastSuccess creates a successful toast effect. +func ToastSuccess(message string) Effect { + return Effect{ + Type: "toast", + Variant: "success", + Message: message, + } +} + +// SetStateEffect creates an effect that replaces a state path. +func SetStateEffect(path string, value any) Effect { + return Effect{ + Type: "set-state", + Path: path, + Value: ptrValue(value), + } +} + +func toValue(value any) Value { + if typed, ok := value.(Value); ok { + return typed + } + + return Literal(value) +} + +func ptrValue(value any) *Value { + result := toValue(value) + return &result +} diff --git a/go-sdk/ui/builder_test.go b/go-sdk/ui/builder_test.go new file mode 100644 index 0000000..b90f268 --- /dev/null +++ b/go-sdk/ui/builder_test.go @@ -0,0 +1,100 @@ +package ui + +import ( + "encoding/json" + "testing" +) + +const usersListViewID = "users.list" + +func TestUnit_BuilderCreatesView(t *testing.T) { + view := View( + usersListViewID, + Title("Users"), + Source("users", Tool(usersListViewID).OnMount()), + Content( + DataTable("users-table").Row(1).Cols(12).Prop( + "rows", + SourceRef("users.items"), + ), + ), + ).Action( + "users.refresh", + CallTool("users.refresh").Then(Invalidate("users")), + ) + + app := New(AppID("users"), AppTitle("Users"), AppVersion("1.0.0")) + if err := app.AddView(view); err != nil { + t.Fatal(err) + } + manifest := app.Manifest() + if len(manifest.Views) != 1 || manifest.Views[0].ID != usersListViewID || manifest.Views[0].Title != "Users" { + t.Fatalf("manifest = %#v", manifest) + } + payload, err := json.Marshal(view.Schema()) + if err != nil { + t.Fatal(err) + } + var decoded struct { + Regions map[string]json.RawMessage `json:"regions"` + } + if err := json.Unmarshal(payload, &decoded); err != nil { + t.Fatal(err) + } + for _, region := range []string{ + "top-header", + "left-panel", + "content", + "right-panel", + "bottom", + } { + if string(decoded.Regions[region]) == "null" { + t.Fatalf("region %q serialized as null", region) + } + } +} + +func TestUnit_AppUsesViewIDWhenTitleIsEmpty(t *testing.T) { + app := New(AppID("users"), AppTitle("Users"), AppVersion("1.0.0")) + if err := app.AddView(View(usersListViewID)); err != nil { + t.Fatal(err) + } + if got := app.Manifest().Views[0].Title; got != usersListViewID { + t.Fatalf("manifest view title = %q, want users.list", got) + } +} + +func TestUnit_AppSnapshotsViews(t *testing.T) { + view := View(usersListViewID, Title("Users")).Action( + "initial", + CallTool("initial"), + ) + app := New(AppID("users")) + if err := app.AddView(view); err != nil { + t.Fatal(err) + } + + manifest := app.Manifest() + manifest.Views[0].ID = "changed" + if got := app.Manifest().Views[0].ID; got != usersListViewID { + t.Fatalf("stored manifest view id = %q, want users.list", got) + } + + view.Action("late", CallTool("late")) + returned, ok := app.View(usersListViewID) + if !ok { + t.Fatal("view is missing") + } + returned.Actions["returned"] = Action{Type: "tool", Tool: "returned"} + + stored, ok := app.View(usersListViewID) + if !ok { + t.Fatal("view is missing after mutation") + } + if len(stored.Actions) != 1 { + t.Fatalf("stored actions = %d, want 1", len(stored.Actions)) + } + if _, ok := stored.Actions["initial"]; !ok { + t.Fatal("initial action is missing") + } +} diff --git a/go-sdk/ui/html.go b/go-sdk/ui/html.go new file mode 100644 index 0000000..6366e59 --- /dev/null +++ b/go-sdk/ui/html.go @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package ui + +// HTMLTag is a safe, declarative HTML body-element name. Document, executable, +// and embedded-content elements are intentionally excluded. +type HTMLTag string + +// HTML constants identify safe HTML body elements supported by the protocol. +const ( + // HTMLA through HTMLWbr enumerate supported safe HTML body elements. + HTMLA HTMLTag = "a" + // HTMLAbbr is the safe HTML abbreviation element. + HTMLAbbr HTMLTag = "abbr" + // HTMLAddress is the safe HTML contact-information element. + HTMLAddress HTMLTag = "address" + // HTMLArea is the safe HTML area element. + HTMLArea HTMLTag = "area" + HTMLArticle HTMLTag = "article" + HTMLAside HTMLTag = "aside" + HTMLAudio HTMLTag = "audio" + HTMLB HTMLTag = "b" + HTMLBDI HTMLTag = "bdi" + HTMLBDO HTMLTag = "bdo" + HTMLBlockquote HTMLTag = "blockquote" + HTMLBr HTMLTag = "br" + HTMLButton HTMLTag = "button" + HTMLCanvas HTMLTag = "canvas" + HTMLCaption HTMLTag = "caption" + HTMLCite HTMLTag = "cite" + HTMLCode HTMLTag = "code" + HTMLCol HTMLTag = "col" + HTMLColgroup HTMLTag = "colgroup" + HTMLData HTMLTag = "data" + HTMLDatalist HTMLTag = "datalist" + HTMLDd HTMLTag = "dd" + HTMLDel HTMLTag = "del" + HTMLDetails HTMLTag = "details" + HTMLDfn HTMLTag = "dfn" + HTMLDialog HTMLTag = "dialog" + HTMLDiv HTMLTag = "div" + HTMLDl HTMLTag = "dl" + HTMLDt HTMLTag = "dt" + HTMLEm HTMLTag = "em" + HTMLFieldset HTMLTag = "fieldset" + HTMLFigcaption HTMLTag = "figcaption" + HTMLFigure HTMLTag = "figure" + HTMLFooter HTMLTag = "footer" + HTMLForm HTMLTag = "form" + HTMLH1 HTMLTag = "h1" + HTMLH2 HTMLTag = "h2" + HTMLH3 HTMLTag = "h3" + HTMLH4 HTMLTag = "h4" + HTMLH5 HTMLTag = "h5" + HTMLH6 HTMLTag = "h6" + HTMLHeader HTMLTag = "header" + HTMLHgroup HTMLTag = "hgroup" + HTMLHr HTMLTag = "hr" + HTMLI HTMLTag = "i" + HTMLImg HTMLTag = "img" + HTMLInput HTMLTag = "input" + HTMLIns HTMLTag = "ins" + HTMLKbd HTMLTag = "kbd" + HTMLLabel HTMLTag = "label" + HTMLLegend HTMLTag = "legend" + HTMLLi HTMLTag = "li" + HTMLMain HTMLTag = "main" + HTMLMap HTMLTag = "map" + HTMLMark HTMLTag = "mark" + HTMLMenu HTMLTag = "menu" + HTMLMeter HTMLTag = "meter" + HTMLNav HTMLTag = "nav" + HTMLOl HTMLTag = "ol" + HTMLOptgroup HTMLTag = "optgroup" + HTMLOption HTMLTag = "option" + HTMLOutput HTMLTag = "output" + HTMLP HTMLTag = "p" + HTMLPicture HTMLTag = "picture" + HTMLPre HTMLTag = "pre" + HTMLProgress HTMLTag = "progress" + HTMLQ HTMLTag = "q" + HTMLRp HTMLTag = "rp" + HTMLRt HTMLTag = "rt" + HTMLRuby HTMLTag = "ruby" + HTMLS HTMLTag = "s" + HTMLSamp HTMLTag = "samp" + HTMLSearch HTMLTag = "search" + HTMLSection HTMLTag = "section" + HTMLSelect HTMLTag = "select" + HTMLSmall HTMLTag = "small" + HTMLSource HTMLTag = "source" + HTMLSpan HTMLTag = "span" + HTMLStrong HTMLTag = "strong" + HTMLSub HTMLTag = "sub" + HTMLSummary HTMLTag = "summary" + HTMLSup HTMLTag = "sup" + HTMLTable HTMLTag = "table" + HTMLTbody HTMLTag = "tbody" + HTMLTd HTMLTag = "td" + HTMLTextarea HTMLTag = "textarea" + HTMLTfoot HTMLTag = "tfoot" + HTMLTh HTMLTag = "th" + HTMLThead HTMLTag = "thead" + HTMLTime HTMLTag = "time" + HTMLTr HTMLTag = "tr" + HTMLTrack HTMLTag = "track" + HTMLU HTMLTag = "u" + HTMLUl HTMLTag = "ul" + HTMLVar HTMLTag = "var" + HTMLVideo HTMLTag = "video" + HTMLWbr HTMLTag = "wbr" +) + +// HTML creates a node for a safe HTML body element. The host must register the +// matching React component and explicitly allow its props and events. +func HTML(tag HTMLTag, id string) *NodeBuilder { + return node(string(tag), id) +} diff --git a/go-sdk/ui/html_test.go b/go-sdk/ui/html_test.go new file mode 100644 index 0000000..812f4db --- /dev/null +++ b/go-sdk/ui/html_test.go @@ -0,0 +1,13 @@ +package ui + +import "testing" + +func TestUnit_HTMLBuilderProducesTagName(t *testing.T) { + for _, tag := range []HTMLTag{HTMLArticle, HTMLDiv, HTMLH1, HTMLInput, HTMLTable, HTMLVideo} { + t.Run(string(tag), func(t *testing.T) { + if got := HTML(tag, "node").Build().Component; got != string(tag) { + t.Fatalf("component = %q, want %q", got, tag) + } + }) + } +} diff --git a/go-sdk/ui/literal_validation.go b/go-sdk/ui/literal_validation.go new file mode 100644 index 0000000..b64d3c1 --- /dev/null +++ b/go-sdk/ui/literal_validation.go @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package ui + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" +) + +func validateLiteralData(data any, limits Limits, depth int) error { + encoded, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("%w: literal is not JSON-compatible: %w", ErrInvalidValue, err) + } + + decoder := json.NewDecoder(bytes.NewReader(encoded)) + decoder.UseNumber() + var decoded any + if err := decoder.Decode(&decoded); err != nil { + return fmt.Errorf("%w: literal: %w", ErrInvalidValue, err) + } + + return validateLiteralJSON(decoded, limits, depth) +} + +func validateLiteralJSON(value any, limits Limits, depth int) error { + if depth > limits.MaxExpressionDepth { + return errors.New("expression depth limit exceeded") + } + + switch typed := value.(type) { + case string: + if len(typed) > limits.MaxStringLength { + return errors.New("expression string limit exceeded") + } + case []any: + if len(typed) > limits.MaxArrayItems { + return errors.New("expression array limit exceeded") + } + for _, item := range typed { + if err := validateLiteralJSON(item, limits, depth+1); err != nil { + return err + } + } + case map[string]any: + if len(typed) > limits.MaxObjectKeys { + return errors.New("expression object limit exceeded") + } + for key, item := range typed { + if key == unsafePrototypeKey || key == unsafePrototypeProperty || key == unsafeConstructorKey { + return errors.New("unsafe object key") + } + if err := validateLiteralJSON(item, limits, depth+1); err != nil { + return err + } + } + } + + return nil +} diff --git a/go-sdk/ui/rpc.go b/go-sdk/ui/rpc.go new file mode 100644 index 0000000..6e88ec2 --- /dev/null +++ b/go-sdk/ui/rpc.go @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package ui + +// Host RPC method names used by the React runtime. The MCP adapter publishes +// UI resources; a host transport is responsible for exposing these methods. +const ( + RPCMethodGetView = "ui.get" + RPCMethodDataCall = "data.call" + RPCMethodAction = "ui.action" +) + +// GetViewRequest requests a view schema from a host UI backend. +type GetViewRequest struct { + Plugin string `json:"plugin,omitempty"` + View string `json:"view"` + Context any `json:"context,omitempty"` +} + +// GetViewResponse contains a view schema returned by a host UI backend. +type GetViewResponse struct { + Schema ViewSchema `json:"schema"` +} + +// DataCallRequest requests a data source operation from a host UI backend. +type DataCallRequest struct { + Plugin string `json:"plugin,omitempty"` + View string `json:"view"` + Source string `json:"source"` + Operation string `json:"operation"` + Input map[string]any `json:"input,omitempty"` +} + +// ActionRequest requests a declared view action from a host UI backend. +type ActionRequest struct { + Plugin string `json:"plugin,omitempty"` + View string `json:"view"` + Action string `json:"action"` + Input map[string]any `json:"input,omitempty"` +} diff --git a/go-sdk/ui/schema.go b/go-sdk/ui/schema.go new file mode 100644 index 0000000..0cb8d20 --- /dev/null +++ b/go-sdk/ui/schema.go @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +// Package ui defines the transport-independent declarative UI protocol. +package ui + +//go:generate easyjson + +import ( + "errors" + "fmt" + "strings" +) + +const ( + // ProtocolVersion is the supported declarative UI protocol version. + ProtocolVersion = "1.0" + // UIMIMEType is the MIME type used for declarative UI resources. + UIMIMEType = "application/vnd.osspkg.ui+json" +) + +var ( + // ErrInvalidSchema reports an invalid manifest or view schema. + ErrInvalidSchema = errors.New("invalid ui schema") + // ErrInvalidLayout reports an invalid node layout. + ErrInvalidLayout = errors.New("invalid ui layout") + // ErrInvalidValue reports an invalid declarative value. + ErrInvalidValue = errors.New("invalid ui value") +) + +// Manifest describes the application entry points exposed to a host. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type Manifest struct { + ProtocolVersion string `json:"protocolVersion"` + Plugin PluginManifest `json:"plugin"` + Views []UIViewDescriptor `json:"views"` + RequiredComponents []string `json:"requiredComponents,omitempty"` +} + +// PluginManifest identifies the plugin that owns the UI. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type PluginManifest struct { + ID string `json:"id"` + Title string `json:"title"` + Version string `json:"version"` + Description string `json:"description,omitempty"` +} + +// UIViewDescriptor identifies a view schema resource in a manifest. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type UIViewDescriptor struct { + ID string `json:"id"` + Title string `json:"title"` + Route string `json:"route,omitempty"` + Schema string `json:"schema"` +} + +// ViewSchema is the declarative layout, state, source, and action model for a view. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type ViewSchema struct { + ProtocolVersion string `json:"protocolVersion"` + ID string `json:"id"` + Revision string `json:"revision,omitempty"` + Title string `json:"title,omitempty"` + State map[string]any `json:"state,omitempty"` + Sources map[string]DataSource `json:"sources,omitempty"` + Actions map[string]Action `json:"actions,omitempty"` + Regions Regions `json:"regions"` +} + +// Regions groups view nodes into the host-supported layout regions. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type Regions struct { + TopHeader []Node `json:"top-header"` + LeftPanel []Node `json:"left-panel"` + Content []Node `json:"content"` + RightPanel []Node `json:"right-panel"` + Bottom []Node `json:"bottom"` +} + +// Layout places a node in a twelve-column row layout. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type Layout struct { + Row int `json:"row"` + Cols int `json:"cols,omitempty"` + Offset int `json:"offset,omitempty"` +} + +// Node describes one declarative UI component and its descendants. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type Node struct { + ID string `json:"id"` + Component string `json:"component"` + Layout *Layout `json:"layout,omitempty"` + Props map[string]Value `json:"props,omitempty"` + Events map[string]EventHandler `json:"events,omitempty"` + Children []Node `json:"children,omitempty"` + Slots map[string][]Node `json:"slots,omitempty"` + When *Value `json:"when,omitempty"` +} + +// EventHandler maps a UI event to an action or sequence of action steps. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type EventHandler struct { + Action string `json:"action,omitempty"` + Steps []ActionStep `json:"steps,omitempty"` +} + +// DataSource describes a host tool used to populate view data. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type DataSource struct { + Type string `json:"type"` + Tool string `json:"tool"` + Input map[string]Value `json:"input,omitempty"` + Policy SourcePolicy `json:"policy,omitempty"` + Cache *CachePolicy `json:"cache,omitempty"` + RefreshOn []string `json:"refreshOn,omitempty"` +} + +// SourcePolicy controls when a data source is fetched. +type SourcePolicy string + +const ( + // SourceManual fetches the source only when explicitly requested. + SourceManual SourcePolicy = "manual" + // SourceOnMount fetches the source when the view is mounted. + SourceOnMount SourcePolicy = "on-mount" +) + +// CachePolicy controls how long fetched source data may be reused. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type CachePolicy struct { + TTL int64 `json:"ttl,omitempty"` +} + +// Action describes a named operation available to the view. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type Action struct { + Type string `json:"type"` + Tool string `json:"tool,omitempty"` + Input map[string]Value `json:"input,omitempty"` + Path string `json:"path,omitempty"` + Value *Value `json:"value,omitempty"` + Source string `json:"source,omitempty"` + To string `json:"to,omitempty"` + Variant string `json:"variant,omitempty"` + Message string `json:"message,omitempty"` + Effects []Effect `json:"effects,omitempty"` +} + +// ActionStep is one step in an event action sequence. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type ActionStep struct { + Type string `json:"type"` + Action string `json:"action,omitempty"` + Input map[string]Value `json:"input,omitempty"` +} + +// Effect describes a client-side consequence of an action. +// +//easyjson:json +//nolint:recvcheck // easyjson generates value marshal and pointer unmarshal methods. +type Effect struct { + Type string `json:"type"` + Path string `json:"path,omitempty"` + Source string `json:"source,omitempty"` + Value *Value `json:"value,omitempty"` + BaseRevision string `json:"baseRevision,omitempty"` + Revision string `json:"revision,omitempty"` + To string `json:"to,omitempty"` + Variant string `json:"variant,omitempty"` + Message string `json:"message,omitempty"` +} + +// Validate checks the manifest against DefaultLimits. +func (m Manifest) Validate() error { + return m.ValidateWithLimits(DefaultLimits()) +} + +// ValidateWithLimits checks the manifest against limits. Any non-positive +// limit field is replaced with its DefaultLimits value. +func (m Manifest) ValidateWithLimits(limits Limits) error { + limits = withDefaultLimits(limits) + + if m.ProtocolVersion != ProtocolVersion { + return fmt.Errorf("%w: unsupported manifest protocol version %q", ErrInvalidSchema, m.ProtocolVersion) + } + + if !validName(m.Plugin.ID) || m.Plugin.Title == "" || m.Plugin.Version == "" { + return fmt.Errorf("%w: plugin id, title, and version are required", ErrInvalidSchema) + } + + if m.Views == nil { + return fmt.Errorf("%w: manifest views must be an array", ErrInvalidSchema) + } + + seen := make(map[string]struct{}, len(m.Views)) + for _, view := range m.Views { + if !validName(view.ID) || view.Title == "" || view.Schema == "" { + return fmt.Errorf("%w: view id, title, and schema are required", ErrInvalidSchema) + } + + if _, ok := seen[view.ID]; ok { + return fmt.Errorf("%w: duplicate view %q", ErrInvalidSchema, view.ID) + } + + seen[view.ID] = struct{}{} + } + + for _, component := range m.RequiredComponents { + if !validComponentName(component) { + return fmt.Errorf("%w: invalid required component %q", ErrInvalidSchema, component) + } + } + + return validateSerializedLimits(m, limits) +} + +func validName(name string) bool { + return name != "" && !strings.ContainsAny(name, "\r\n") +} diff --git a/go-sdk/ui/schema_easyjson.go b/go-sdk/ui/schema_easyjson.go new file mode 100644 index 0000000..14603b5 --- /dev/null +++ b/go-sdk/ui/schema_easyjson.go @@ -0,0 +1,2190 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package ui + +import ( + json "encoding/json" + + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi(in *jlexer.Lexer, out *ViewSchema) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "protocolVersion": + if in.IsNull() { + in.Skip() + } else { + out.ProtocolVersion = string(in.String()) + } + case "id": + if in.IsNull() { + in.Skip() + } else { + out.ID = string(in.String()) + } + case "revision": + if in.IsNull() { + in.Skip() + } else { + out.Revision = string(in.String()) + } + case "title": + if in.IsNull() { + in.Skip() + } else { + out.Title = string(in.String()) + } + case "state": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.State = make(map[string]interface{}) + } else { + out.State = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v1 interface{} + if m, ok := v1.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := v1.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + v1 = in.Interface() + } + (out.State)[key] = v1 + in.WantComma() + } + in.Delim('}') + } + case "sources": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Sources = make(map[string]DataSource) + } else { + out.Sources = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v2 DataSource + if in.IsNull() { + in.Skip() + } else { + (v2).UnmarshalEasyJSON(in) + } + (out.Sources)[key] = v2 + in.WantComma() + } + in.Delim('}') + } + case "actions": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Actions = make(map[string]Action) + } else { + out.Actions = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v3 Action + if in.IsNull() { + in.Skip() + } else { + (v3).UnmarshalEasyJSON(in) + } + (out.Actions)[key] = v3 + in.WantComma() + } + in.Delim('}') + } + case "regions": + if in.IsNull() { + in.Skip() + } else { + (out.Regions).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi(out *jwriter.Writer, in ViewSchema) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"protocolVersion\":" + out.RawString(prefix[1:]) + out.String(string(in.ProtocolVersion)) + } + { + const prefix string = ",\"id\":" + out.RawString(prefix) + out.String(string(in.ID)) + } + if in.Revision != "" { + const prefix string = ",\"revision\":" + out.RawString(prefix) + out.String(string(in.Revision)) + } + if in.Title != "" { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + if len(in.State) != 0 { + const prefix string = ",\"state\":" + out.RawString(prefix) + { + out.RawByte('{') + v4First := true + for v4Name, v4Value := range in.State { + if v4First { + v4First = false + } else { + out.RawByte(',') + } + out.String(string(v4Name)) + out.RawByte(':') + if m, ok := v4Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v4Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v4Value)) + } + } + out.RawByte('}') + } + } + if len(in.Sources) != 0 { + const prefix string = ",\"sources\":" + out.RawString(prefix) + { + out.RawByte('{') + v5First := true + for v5Name, v5Value := range in.Sources { + if v5First { + v5First = false + } else { + out.RawByte(',') + } + out.String(string(v5Name)) + out.RawByte(':') + (v5Value).MarshalEasyJSON(out) + } + out.RawByte('}') + } + } + if len(in.Actions) != 0 { + const prefix string = ",\"actions\":" + out.RawString(prefix) + { + out.RawByte('{') + v6First := true + for v6Name, v6Value := range in.Actions { + if v6First { + v6First = false + } else { + out.RawByte(',') + } + out.String(string(v6Name)) + out.RawByte(':') + (v6Value).MarshalEasyJSON(out) + } + out.RawByte('}') + } + } + { + const prefix string = ",\"regions\":" + out.RawString(prefix) + (in.Regions).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ViewSchema) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ViewSchema) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ViewSchema) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ViewSchema) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi1(in *jlexer.Lexer, out *UIViewDescriptor) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "id": + if in.IsNull() { + in.Skip() + } else { + out.ID = string(in.String()) + } + case "title": + if in.IsNull() { + in.Skip() + } else { + out.Title = string(in.String()) + } + case "route": + if in.IsNull() { + in.Skip() + } else { + out.Route = string(in.String()) + } + case "schema": + if in.IsNull() { + in.Skip() + } else { + out.Schema = string(in.String()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi1(out *jwriter.Writer, in UIViewDescriptor) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.String(string(in.ID)) + } + { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + if in.Route != "" { + const prefix string = ",\"route\":" + out.RawString(prefix) + out.String(string(in.Route)) + } + { + const prefix string = ",\"schema\":" + out.RawString(prefix) + out.String(string(in.Schema)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v UIViewDescriptor) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v UIViewDescriptor) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *UIViewDescriptor) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *UIViewDescriptor) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi1(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi2(in *jlexer.Lexer, out *Regions) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "top-header": + if in.IsNull() { + in.Skip() + out.TopHeader = nil + } else { + in.Delim('[') + if out.TopHeader == nil { + if !in.IsDelim(']') { + out.TopHeader = make([]Node, 0, 0) + } else { + out.TopHeader = []Node{} + } + } else { + out.TopHeader = (out.TopHeader)[:0] + } + for !in.IsDelim(']') { + var v7 Node + if in.IsNull() { + in.Skip() + } else { + (v7).UnmarshalEasyJSON(in) + } + out.TopHeader = append(out.TopHeader, v7) + in.WantComma() + } + in.Delim(']') + } + case "left-panel": + if in.IsNull() { + in.Skip() + out.LeftPanel = nil + } else { + in.Delim('[') + if out.LeftPanel == nil { + if !in.IsDelim(']') { + out.LeftPanel = make([]Node, 0, 0) + } else { + out.LeftPanel = []Node{} + } + } else { + out.LeftPanel = (out.LeftPanel)[:0] + } + for !in.IsDelim(']') { + var v8 Node + if in.IsNull() { + in.Skip() + } else { + (v8).UnmarshalEasyJSON(in) + } + out.LeftPanel = append(out.LeftPanel, v8) + in.WantComma() + } + in.Delim(']') + } + case "content": + if in.IsNull() { + in.Skip() + out.Content = nil + } else { + in.Delim('[') + if out.Content == nil { + if !in.IsDelim(']') { + out.Content = make([]Node, 0, 0) + } else { + out.Content = []Node{} + } + } else { + out.Content = (out.Content)[:0] + } + for !in.IsDelim(']') { + var v9 Node + if in.IsNull() { + in.Skip() + } else { + (v9).UnmarshalEasyJSON(in) + } + out.Content = append(out.Content, v9) + in.WantComma() + } + in.Delim(']') + } + case "right-panel": + if in.IsNull() { + in.Skip() + out.RightPanel = nil + } else { + in.Delim('[') + if out.RightPanel == nil { + if !in.IsDelim(']') { + out.RightPanel = make([]Node, 0, 0) + } else { + out.RightPanel = []Node{} + } + } else { + out.RightPanel = (out.RightPanel)[:0] + } + for !in.IsDelim(']') { + var v10 Node + if in.IsNull() { + in.Skip() + } else { + (v10).UnmarshalEasyJSON(in) + } + out.RightPanel = append(out.RightPanel, v10) + in.WantComma() + } + in.Delim(']') + } + case "bottom": + if in.IsNull() { + in.Skip() + out.Bottom = nil + } else { + in.Delim('[') + if out.Bottom == nil { + if !in.IsDelim(']') { + out.Bottom = make([]Node, 0, 0) + } else { + out.Bottom = []Node{} + } + } else { + out.Bottom = (out.Bottom)[:0] + } + for !in.IsDelim(']') { + var v11 Node + if in.IsNull() { + in.Skip() + } else { + (v11).UnmarshalEasyJSON(in) + } + out.Bottom = append(out.Bottom, v11) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi2(out *jwriter.Writer, in Regions) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"top-header\":" + out.RawString(prefix[1:]) + if in.TopHeader == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v12, v13 := range in.TopHeader { + if v12 > 0 { + out.RawByte(',') + } + (v13).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"left-panel\":" + out.RawString(prefix) + if in.LeftPanel == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v14, v15 := range in.LeftPanel { + if v14 > 0 { + out.RawByte(',') + } + (v15).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"content\":" + out.RawString(prefix) + if in.Content == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v16, v17 := range in.Content { + if v16 > 0 { + out.RawByte(',') + } + (v17).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"right-panel\":" + out.RawString(prefix) + if in.RightPanel == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v18, v19 := range in.RightPanel { + if v18 > 0 { + out.RawByte(',') + } + (v19).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"bottom\":" + out.RawString(prefix) + if in.Bottom == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v20, v21 := range in.Bottom { + if v20 > 0 { + out.RawByte(',') + } + (v21).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Regions) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Regions) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Regions) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Regions) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi2(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi3(in *jlexer.Lexer, out *PluginManifest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "id": + if in.IsNull() { + in.Skip() + } else { + out.ID = string(in.String()) + } + case "title": + if in.IsNull() { + in.Skip() + } else { + out.Title = string(in.String()) + } + case "version": + if in.IsNull() { + in.Skip() + } else { + out.Version = string(in.String()) + } + case "description": + if in.IsNull() { + in.Skip() + } else { + out.Description = string(in.String()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi3(out *jwriter.Writer, in PluginManifest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.String(string(in.ID)) + } + { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + { + const prefix string = ",\"version\":" + out.RawString(prefix) + out.String(string(in.Version)) + } + if in.Description != "" { + const prefix string = ",\"description\":" + out.RawString(prefix) + out.String(string(in.Description)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v PluginManifest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v PluginManifest) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *PluginManifest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *PluginManifest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi3(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi4(in *jlexer.Lexer, out *Node) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "id": + if in.IsNull() { + in.Skip() + } else { + out.ID = string(in.String()) + } + case "component": + if in.IsNull() { + in.Skip() + } else { + out.Component = string(in.String()) + } + case "layout": + if in.IsNull() { + in.Skip() + out.Layout = nil + } else { + if out.Layout == nil { + out.Layout = new(Layout) + } + if in.IsNull() { + in.Skip() + } else { + (*out.Layout).UnmarshalEasyJSON(in) + } + } + case "props": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Props = make(map[string]Value) + } else { + out.Props = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v22 Value + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((v22).UnmarshalJSON(data)) + } + } + (out.Props)[key] = v22 + in.WantComma() + } + in.Delim('}') + } + case "events": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Events = make(map[string]EventHandler) + } else { + out.Events = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v23 EventHandler + if in.IsNull() { + in.Skip() + } else { + (v23).UnmarshalEasyJSON(in) + } + (out.Events)[key] = v23 + in.WantComma() + } + in.Delim('}') + } + case "children": + if in.IsNull() { + in.Skip() + out.Children = nil + } else { + in.Delim('[') + if out.Children == nil { + if !in.IsDelim(']') { + out.Children = make([]Node, 0, 0) + } else { + out.Children = []Node{} + } + } else { + out.Children = (out.Children)[:0] + } + for !in.IsDelim(']') { + var v24 Node + if in.IsNull() { + in.Skip() + } else { + (v24).UnmarshalEasyJSON(in) + } + out.Children = append(out.Children, v24) + in.WantComma() + } + in.Delim(']') + } + case "slots": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Slots = make(map[string][]Node) + } else { + out.Slots = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v25 []Node + if in.IsNull() { + in.Skip() + v25 = nil + } else { + in.Delim('[') + if v25 == nil { + if !in.IsDelim(']') { + v25 = make([]Node, 0, 0) + } else { + v25 = []Node{} + } + } else { + v25 = (v25)[:0] + } + for !in.IsDelim(']') { + var v26 Node + if in.IsNull() { + in.Skip() + } else { + (v26).UnmarshalEasyJSON(in) + } + v25 = append(v25, v26) + in.WantComma() + } + in.Delim(']') + } + (out.Slots)[key] = v25 + in.WantComma() + } + in.Delim('}') + } + case "when": + if in.IsNull() { + in.Skip() + out.When = nil + } else { + if out.When == nil { + out.When = new(Value) + } + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((*out.When).UnmarshalJSON(data)) + } + } + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi4(out *jwriter.Writer, in Node) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.String(string(in.ID)) + } + { + const prefix string = ",\"component\":" + out.RawString(prefix) + out.String(string(in.Component)) + } + if in.Layout != nil { + const prefix string = ",\"layout\":" + out.RawString(prefix) + (*in.Layout).MarshalEasyJSON(out) + } + if len(in.Props) != 0 { + const prefix string = ",\"props\":" + out.RawString(prefix) + { + out.RawByte('{') + v27First := true + for v27Name, v27Value := range in.Props { + if v27First { + v27First = false + } else { + out.RawByte(',') + } + out.String(string(v27Name)) + out.RawByte(':') + out.Raw((v27Value).MarshalJSON()) + } + out.RawByte('}') + } + } + if len(in.Events) != 0 { + const prefix string = ",\"events\":" + out.RawString(prefix) + { + out.RawByte('{') + v28First := true + for v28Name, v28Value := range in.Events { + if v28First { + v28First = false + } else { + out.RawByte(',') + } + out.String(string(v28Name)) + out.RawByte(':') + (v28Value).MarshalEasyJSON(out) + } + out.RawByte('}') + } + } + if len(in.Children) != 0 { + const prefix string = ",\"children\":" + out.RawString(prefix) + { + out.RawByte('[') + for v29, v30 := range in.Children { + if v29 > 0 { + out.RawByte(',') + } + (v30).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + if len(in.Slots) != 0 { + const prefix string = ",\"slots\":" + out.RawString(prefix) + { + out.RawByte('{') + v31First := true + for v31Name, v31Value := range in.Slots { + if v31First { + v31First = false + } else { + out.RawByte(',') + } + out.String(string(v31Name)) + out.RawByte(':') + if v31Value == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v32, v33 := range v31Value { + if v32 > 0 { + out.RawByte(',') + } + (v33).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + out.RawByte('}') + } + } + if in.When != nil { + const prefix string = ",\"when\":" + out.RawString(prefix) + out.Raw((*in.When).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Node) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi4(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Node) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi4(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Node) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi4(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Node) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi4(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi5(in *jlexer.Lexer, out *Manifest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "protocolVersion": + if in.IsNull() { + in.Skip() + } else { + out.ProtocolVersion = string(in.String()) + } + case "plugin": + if in.IsNull() { + in.Skip() + } else { + (out.Plugin).UnmarshalEasyJSON(in) + } + case "views": + if in.IsNull() { + in.Skip() + out.Views = nil + } else { + in.Delim('[') + if out.Views == nil { + if !in.IsDelim(']') { + out.Views = make([]UIViewDescriptor, 0, 1) + } else { + out.Views = []UIViewDescriptor{} + } + } else { + out.Views = (out.Views)[:0] + } + for !in.IsDelim(']') { + var v34 UIViewDescriptor + if in.IsNull() { + in.Skip() + } else { + (v34).UnmarshalEasyJSON(in) + } + out.Views = append(out.Views, v34) + in.WantComma() + } + in.Delim(']') + } + case "requiredComponents": + if in.IsNull() { + in.Skip() + out.RequiredComponents = nil + } else { + in.Delim('[') + if out.RequiredComponents == nil { + if !in.IsDelim(']') { + out.RequiredComponents = make([]string, 0, 4) + } else { + out.RequiredComponents = []string{} + } + } else { + out.RequiredComponents = (out.RequiredComponents)[:0] + } + for !in.IsDelim(']') { + var v35 string + if in.IsNull() { + in.Skip() + } else { + v35 = string(in.String()) + } + out.RequiredComponents = append(out.RequiredComponents, v35) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi5(out *jwriter.Writer, in Manifest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"protocolVersion\":" + out.RawString(prefix[1:]) + out.String(string(in.ProtocolVersion)) + } + { + const prefix string = ",\"plugin\":" + out.RawString(prefix) + (in.Plugin).MarshalEasyJSON(out) + } + { + const prefix string = ",\"views\":" + out.RawString(prefix) + if in.Views == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v36, v37 := range in.Views { + if v36 > 0 { + out.RawByte(',') + } + (v37).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + if len(in.RequiredComponents) != 0 { + const prefix string = ",\"requiredComponents\":" + out.RawString(prefix) + { + out.RawByte('[') + for v38, v39 := range in.RequiredComponents { + if v38 > 0 { + out.RawByte(',') + } + out.String(string(v39)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Manifest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi5(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Manifest) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi5(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Manifest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi5(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Manifest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi5(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi6(in *jlexer.Lexer, out *Layout) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "row": + if in.IsNull() { + in.Skip() + } else { + out.Row = int(in.Int()) + } + case "cols": + if in.IsNull() { + in.Skip() + } else { + out.Cols = int(in.Int()) + } + case "offset": + if in.IsNull() { + in.Skip() + } else { + out.Offset = int(in.Int()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi6(out *jwriter.Writer, in Layout) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"row\":" + out.RawString(prefix[1:]) + out.Int(int(in.Row)) + } + if in.Cols != 0 { + const prefix string = ",\"cols\":" + out.RawString(prefix) + out.Int(int(in.Cols)) + } + if in.Offset != 0 { + const prefix string = ",\"offset\":" + out.RawString(prefix) + out.Int(int(in.Offset)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Layout) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi6(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Layout) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi6(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Layout) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi6(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Layout) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi6(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi7(in *jlexer.Lexer, out *EventHandler) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "action": + if in.IsNull() { + in.Skip() + } else { + out.Action = string(in.String()) + } + case "steps": + if in.IsNull() { + in.Skip() + out.Steps = nil + } else { + in.Delim('[') + if out.Steps == nil { + if !in.IsDelim(']') { + out.Steps = make([]ActionStep, 0, 1) + } else { + out.Steps = []ActionStep{} + } + } else { + out.Steps = (out.Steps)[:0] + } + for !in.IsDelim(']') { + var v40 ActionStep + if in.IsNull() { + in.Skip() + } else { + (v40).UnmarshalEasyJSON(in) + } + out.Steps = append(out.Steps, v40) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi7(out *jwriter.Writer, in EventHandler) { + out.RawByte('{') + first := true + _ = first + if in.Action != "" { + const prefix string = ",\"action\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.Action)) + } + if len(in.Steps) != 0 { + const prefix string = ",\"steps\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('[') + for v41, v42 := range in.Steps { + if v41 > 0 { + out.RawByte(',') + } + (v42).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v EventHandler) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi7(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v EventHandler) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi7(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *EventHandler) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi7(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *EventHandler) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi7(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi8(in *jlexer.Lexer, out *Effect) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "type": + if in.IsNull() { + in.Skip() + } else { + out.Type = string(in.String()) + } + case "path": + if in.IsNull() { + in.Skip() + } else { + out.Path = string(in.String()) + } + case "source": + if in.IsNull() { + in.Skip() + } else { + out.Source = string(in.String()) + } + case "value": + if in.IsNull() { + in.Skip() + out.Value = nil + } else { + if out.Value == nil { + out.Value = new(Value) + } + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((*out.Value).UnmarshalJSON(data)) + } + } + } + case "baseRevision": + if in.IsNull() { + in.Skip() + } else { + out.BaseRevision = string(in.String()) + } + case "revision": + if in.IsNull() { + in.Skip() + } else { + out.Revision = string(in.String()) + } + case "to": + if in.IsNull() { + in.Skip() + } else { + out.To = string(in.String()) + } + case "variant": + if in.IsNull() { + in.Skip() + } else { + out.Variant = string(in.String()) + } + case "message": + if in.IsNull() { + in.Skip() + } else { + out.Message = string(in.String()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi8(out *jwriter.Writer, in Effect) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + if in.Path != "" { + const prefix string = ",\"path\":" + out.RawString(prefix) + out.String(string(in.Path)) + } + if in.Source != "" { + const prefix string = ",\"source\":" + out.RawString(prefix) + out.String(string(in.Source)) + } + if in.Value != nil { + const prefix string = ",\"value\":" + out.RawString(prefix) + out.Raw((*in.Value).MarshalJSON()) + } + if in.BaseRevision != "" { + const prefix string = ",\"baseRevision\":" + out.RawString(prefix) + out.String(string(in.BaseRevision)) + } + if in.Revision != "" { + const prefix string = ",\"revision\":" + out.RawString(prefix) + out.String(string(in.Revision)) + } + if in.To != "" { + const prefix string = ",\"to\":" + out.RawString(prefix) + out.String(string(in.To)) + } + if in.Variant != "" { + const prefix string = ",\"variant\":" + out.RawString(prefix) + out.String(string(in.Variant)) + } + if in.Message != "" { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Effect) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi8(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Effect) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi8(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Effect) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi8(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Effect) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi8(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi9(in *jlexer.Lexer, out *DataSource) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "type": + if in.IsNull() { + in.Skip() + } else { + out.Type = string(in.String()) + } + case "tool": + if in.IsNull() { + in.Skip() + } else { + out.Tool = string(in.String()) + } + case "input": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Input = make(map[string]Value) + } else { + out.Input = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v43 Value + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((v43).UnmarshalJSON(data)) + } + } + (out.Input)[key] = v43 + in.WantComma() + } + in.Delim('}') + } + case "policy": + if in.IsNull() { + in.Skip() + } else { + out.Policy = SourcePolicy(in.String()) + } + case "cache": + if in.IsNull() { + in.Skip() + out.Cache = nil + } else { + if out.Cache == nil { + out.Cache = new(CachePolicy) + } + if in.IsNull() { + in.Skip() + } else { + (*out.Cache).UnmarshalEasyJSON(in) + } + } + case "refreshOn": + if in.IsNull() { + in.Skip() + out.RefreshOn = nil + } else { + in.Delim('[') + if out.RefreshOn == nil { + if !in.IsDelim(']') { + out.RefreshOn = make([]string, 0, 4) + } else { + out.RefreshOn = []string{} + } + } else { + out.RefreshOn = (out.RefreshOn)[:0] + } + for !in.IsDelim(']') { + var v44 string + if in.IsNull() { + in.Skip() + } else { + v44 = string(in.String()) + } + out.RefreshOn = append(out.RefreshOn, v44) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi9(out *jwriter.Writer, in DataSource) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + { + const prefix string = ",\"tool\":" + out.RawString(prefix) + out.String(string(in.Tool)) + } + if len(in.Input) != 0 { + const prefix string = ",\"input\":" + out.RawString(prefix) + { + out.RawByte('{') + v45First := true + for v45Name, v45Value := range in.Input { + if v45First { + v45First = false + } else { + out.RawByte(',') + } + out.String(string(v45Name)) + out.RawByte(':') + out.Raw((v45Value).MarshalJSON()) + } + out.RawByte('}') + } + } + if in.Policy != "" { + const prefix string = ",\"policy\":" + out.RawString(prefix) + out.String(string(in.Policy)) + } + if in.Cache != nil { + const prefix string = ",\"cache\":" + out.RawString(prefix) + (*in.Cache).MarshalEasyJSON(out) + } + if len(in.RefreshOn) != 0 { + const prefix string = ",\"refreshOn\":" + out.RawString(prefix) + { + out.RawByte('[') + for v46, v47 := range in.RefreshOn { + if v46 > 0 { + out.RawByte(',') + } + out.String(string(v47)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v DataSource) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi9(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v DataSource) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi9(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *DataSource) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi9(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *DataSource) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi9(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi10(in *jlexer.Lexer, out *CachePolicy) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "ttl": + if in.IsNull() { + in.Skip() + } else { + out.TTL = int64(in.Int64()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi10(out *jwriter.Writer, in CachePolicy) { + out.RawByte('{') + first := true + _ = first + if in.TTL != 0 { + const prefix string = ",\"ttl\":" + first = false + out.RawString(prefix[1:]) + out.Int64(int64(in.TTL)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v CachePolicy) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi10(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v CachePolicy) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi10(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *CachePolicy) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi10(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *CachePolicy) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi10(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi11(in *jlexer.Lexer, out *ActionStep) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "type": + if in.IsNull() { + in.Skip() + } else { + out.Type = string(in.String()) + } + case "action": + if in.IsNull() { + in.Skip() + } else { + out.Action = string(in.String()) + } + case "input": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Input = make(map[string]Value) + } else { + out.Input = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v48 Value + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((v48).UnmarshalJSON(data)) + } + } + (out.Input)[key] = v48 + in.WantComma() + } + in.Delim('}') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi11(out *jwriter.Writer, in ActionStep) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + if in.Action != "" { + const prefix string = ",\"action\":" + out.RawString(prefix) + out.String(string(in.Action)) + } + if len(in.Input) != 0 { + const prefix string = ",\"input\":" + out.RawString(prefix) + { + out.RawByte('{') + v49First := true + for v49Name, v49Value := range in.Input { + if v49First { + v49First = false + } else { + out.RawByte(',') + } + out.String(string(v49Name)) + out.RawByte(':') + out.Raw((v49Value).MarshalJSON()) + } + out.RawByte('}') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ActionStep) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi11(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ActionStep) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi11(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ActionStep) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi11(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ActionStep) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi11(l, v) +} +func easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi12(in *jlexer.Lexer, out *Action) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + switch key { + case "type": + if in.IsNull() { + in.Skip() + } else { + out.Type = string(in.String()) + } + case "tool": + if in.IsNull() { + in.Skip() + } else { + out.Tool = string(in.String()) + } + case "input": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Input = make(map[string]Value) + } else { + out.Input = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v50 Value + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((v50).UnmarshalJSON(data)) + } + } + (out.Input)[key] = v50 + in.WantComma() + } + in.Delim('}') + } + case "path": + if in.IsNull() { + in.Skip() + } else { + out.Path = string(in.String()) + } + case "value": + if in.IsNull() { + in.Skip() + out.Value = nil + } else { + if out.Value == nil { + out.Value = new(Value) + } + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((*out.Value).UnmarshalJSON(data)) + } + } + } + case "source": + if in.IsNull() { + in.Skip() + } else { + out.Source = string(in.String()) + } + case "to": + if in.IsNull() { + in.Skip() + } else { + out.To = string(in.String()) + } + case "variant": + if in.IsNull() { + in.Skip() + } else { + out.Variant = string(in.String()) + } + case "message": + if in.IsNull() { + in.Skip() + } else { + out.Message = string(in.String()) + } + case "effects": + if in.IsNull() { + in.Skip() + out.Effects = nil + } else { + in.Delim('[') + if out.Effects == nil { + if !in.IsDelim(']') { + out.Effects = make([]Effect, 0, 0) + } else { + out.Effects = []Effect{} + } + } else { + out.Effects = (out.Effects)[:0] + } + for !in.IsDelim(']') { + var v51 Effect + if in.IsNull() { + in.Skip() + } else { + (v51).UnmarshalEasyJSON(in) + } + out.Effects = append(out.Effects, v51) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi12(out *jwriter.Writer, in Action) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + if in.Tool != "" { + const prefix string = ",\"tool\":" + out.RawString(prefix) + out.String(string(in.Tool)) + } + if len(in.Input) != 0 { + const prefix string = ",\"input\":" + out.RawString(prefix) + { + out.RawByte('{') + v52First := true + for v52Name, v52Value := range in.Input { + if v52First { + v52First = false + } else { + out.RawByte(',') + } + out.String(string(v52Name)) + out.RawByte(':') + out.Raw((v52Value).MarshalJSON()) + } + out.RawByte('}') + } + } + if in.Path != "" { + const prefix string = ",\"path\":" + out.RawString(prefix) + out.String(string(in.Path)) + } + if in.Value != nil { + const prefix string = ",\"value\":" + out.RawString(prefix) + out.Raw((*in.Value).MarshalJSON()) + } + if in.Source != "" { + const prefix string = ",\"source\":" + out.RawString(prefix) + out.String(string(in.Source)) + } + if in.To != "" { + const prefix string = ",\"to\":" + out.RawString(prefix) + out.String(string(in.To)) + } + if in.Variant != "" { + const prefix string = ",\"variant\":" + out.RawString(prefix) + out.String(string(in.Variant)) + } + if in.Message != "" { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + if len(in.Effects) != 0 { + const prefix string = ",\"effects\":" + out.RawString(prefix) + { + out.RawByte('[') + for v53, v54 := range in.Effects { + if v53 > 0 { + out.RawByte(',') + } + (v54).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Action) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi12(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Action) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonCef4e921EncodeGoOsspkgComUiGoSdkUi12(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Action) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi12(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Action) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonCef4e921DecodeGoOsspkgComUiGoSdkUi12(l, v) +} diff --git a/go-sdk/ui/schema_test.go b/go-sdk/ui/schema_test.go new file mode 100644 index 0000000..7dd610a --- /dev/null +++ b/go-sdk/ui/schema_test.go @@ -0,0 +1,309 @@ +package ui + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "strings" + "testing" +) + +func TestUnit_ValueRoundTrip(t *testing.T) { + value := Expr("$eq", StateRef("status"), Literal("active")) + encoded, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var decoded Value + if err := json.Unmarshal(encoded, &decoded); err != nil { + t.Fatal(err) + } + if decoded.Kind != ValueExpr || decoded.Path != "$eq" { + t.Fatalf("decoded value = %#v", decoded) + } +} + +func TestUnit_ValueLiteralObjects(t *testing.T) { + for _, data := range []string{ + `{"rows":[{"id":1}]}`, + `{"disabled":null}`, + } { + t.Run(data, func(t *testing.T) { + var value Value + if err := json.Unmarshal([]byte(data), &value); err != nil { + t.Fatal(err) + } + if value.Kind != ValueLiteral { + t.Fatalf("value kind = %q, want %q", value.Kind, ValueLiteral) + } + encoded, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var want, got any + if err := json.Unmarshal([]byte(data), &want); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(encoded, &got); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("round-trip JSON differs: got %s, want %s", encoded, data) + } + }) + } +} + +func TestUnit_LayoutValidation(t *testing.T) { + tests := []struct { + name string + layout Layout + region string + want error + }{ + {name: "valid", layout: Layout{Row: 1, Cols: 8, Offset: 2}, region: "content"}, + {name: "overflow", layout: Layout{Row: 1, Cols: 8, Offset: 5}, region: "content", want: ErrInvalidLayout}, + {name: "side panel", layout: Layout{Row: 1, Cols: 1}, region: "left-panel", want: ErrInvalidLayout}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.layout.Validate(test.region) + if !errors.Is(err, test.want) { + t.Fatalf("Validate() error = %v, want %v", err, test.want) + } + }) + } +} + +func TestUnit_SchemaLimits(t *testing.T) { + view := ViewSchema{ + ProtocolVersion: ProtocolVersion, + ID: "users.list", + Regions: func() Regions { + regions := emptyRegions() + regions.Content = []Node{{ID: "title", Component: "text", Props: map[string]Value{"text": Literal("Users")}}} + return regions + }(), + } + tests := []struct { + name string + limits Limits + }{ + {name: "bytes", limits: Limits{MaxBytes: 64}}, + {name: "strings", limits: Limits{MaxStrings: 1}}, + {name: "string length", limits: Limits{MaxStringLength: 3}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := view.ValidateWithLimits(test.limits); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("ValidateWithLimits() error = %v, want ErrInvalidSchema", err) + } + }) + } +} + +func TestUnit_SemanticValidation(t *testing.T) { + tests := []struct { + name string + value func() error + want error + }{ + {name: "unsupported operator", value: func() error { return Expr("$unknown", Literal(true)).Validate() }, want: ErrInvalidValue}, + {name: "operator arity", value: func() error { return Expr("$eq", Literal(true)).Validate() }, want: ErrInvalidValue}, + {name: "unknown source reference", value: func() error { + regions := emptyRegions() + regions.Content = []Node{{ID: "n", Component: "text", Props: map[string]Value{"value": SourceRef("missing.items")}}} + return (ViewSchema{ProtocolVersion: ProtocolVersion, ID: "users.list", Regions: regions}).Validate() + }, want: ErrInvalidSchema}, + {name: "unknown effect source", value: func() error { + return (ViewSchema{ProtocolVersion: ProtocolVersion, ID: "users.list", Actions: map[string]Action{"save": {Type: "tool", Tool: "users.save", Effects: []Effect{{Type: "invalidate", Source: "missing"}}}}, Regions: emptyRegions()}).Validate() + }, want: ErrInvalidSchema}, + {name: "unknown action value source", value: func() error { + return (ViewSchema{ProtocolVersion: ProtocolVersion, ID: "users.list", Actions: map[string]Action{"save": {Type: "set-state", Path: "form", Value: ptrValue(SourceRef("missing.items"))}}, Regions: emptyRegions()}).Validate() + }, want: ErrInvalidSchema}, + {name: "module component name", value: func() error { + regions := emptyRegions() + regions.Content = []Node{{ID: "n", Component: "@scope/button"}} + return (ViewSchema{ProtocolVersion: ProtocolVersion, ID: "users.list", Regions: regions}).Validate() + }, want: ErrInvalidSchema}, + {name: "component name starts with hyphen", value: func() error { + regions := emptyRegions() + regions.Content = []Node{{ID: "n", Component: "-button"}} + return (ViewSchema{ProtocolVersion: ProtocolVersion, ID: "users.list", Regions: regions}).Validate() + }, want: ErrInvalidSchema}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.value(); !errors.Is(err, test.want) { + t.Fatalf("validation error = %v, want %v", err, test.want) + } + }) + } +} + +func TestUnit_DottedSourceReference(t *testing.T) { + regions := emptyRegions() + regions.Content = []Node{{ + ID: "orders-table", + Component: "table", + Props: map[string]Value{"rows": SourceRef("orders.list.items")}, + }} + view := ViewSchema{ + ProtocolVersion: ProtocolVersion, + ID: "orders", + Sources: map[string]DataSource{ + "orders.list": {Type: "tool", Tool: "orders.list"}, + }, + Regions: regions, + } + if err := view.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestUnit_SchemaRequiresAllRegionArrays(t *testing.T) { + view := ViewSchema{ProtocolVersion: ProtocolVersion, ID: "users.list", Regions: Regions{Content: []Node{}}} + if err := view.Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema", err) + } +} + +func TestUnit_SchemaExpressionDepthLimit(t *testing.T) { + value := Literal(true) + for range 3 { + value = Expr("$not", value) + } + regions := emptyRegions() + regions.Content = []Node{{ID: "condition", Component: "text", When: &value}} + view := ViewSchema{ProtocolVersion: ProtocolVersion, ID: "users.list", Regions: regions} + if err := view.ValidateWithLimits(Limits{MaxExpressionDepth: 2}); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("ValidateWithLimits() error = %v, want ErrInvalidSchema", err) + } +} + +func TestUnit_JSONFixtures(t *testing.T) { + valid, err := os.ReadFile("../../fixtures/valid-view.json") + if err != nil { + t.Fatal(err) + } + var view ViewSchema + if err := json.Unmarshal(valid, &view); err != nil { + t.Fatal(err) + } + if err := view.Validate(); err != nil { + t.Fatal(err) + } + encoded, err := json.Marshal(view) + if err != nil { + t.Fatal(err) + } + var want, got any + if err := json.Unmarshal(valid, &want); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(encoded, &got); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("round-trip JSON differs:\n got: %s\nwant: %s", encoded, valid) + } + + invalid, err := os.ReadFile("../../fixtures/invalid-layout.json") + if err != nil { + t.Fatal(err) + } + var broken ViewSchema + if err := json.Unmarshal(invalid, &broken); err != nil { + t.Fatal(err) + } + if err := broken.Validate(); !errors.Is(err, ErrInvalidLayout) { + t.Fatalf("invalid fixture error = %v, want ErrInvalidLayout", err) + } +} + +func TestUnit_ManifestValidationParity(t *testing.T) { + base := Manifest{ + ProtocolVersion: ProtocolVersion, + Plugin: PluginManifest{ID: "users", Title: "Users", Version: "1.0"}, + Views: []UIViewDescriptor{}, + } + + if err := (Manifest{ProtocolVersion: ProtocolVersion, Plugin: base.Plugin}).Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema for nil views", err) + } + + base.Views = []UIViewDescriptor{{ID: "users.list", Title: "Users", Schema: "ui://views/users.list"}} + invalidManifests := []struct { + name string + edit func(*Manifest) + }{ + {name: "empty plugin title", edit: func(m *Manifest) { m.Plugin.Title = "" }}, + {name: "empty plugin version", edit: func(m *Manifest) { m.Plugin.Version = "" }}, + {name: "empty view title", edit: func(m *Manifest) { m.Views[0].Title = "" }}, + } + for _, test := range invalidManifests { + t.Run(test.name, func(t *testing.T) { + manifest := base + manifest.Views = append([]UIViewDescriptor(nil), base.Views...) + test.edit(&manifest) + if err := manifest.Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema", err) + } + }) + } + + base.RequiredComponents = []string{"/button"} + if err := base.Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema for invalid component", err) + } + + base.RequiredComponents = nil + if err := base.ValidateWithLimits(Limits{MaxBytes: 1}); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("ValidateWithLimits() error = %v, want ErrInvalidSchema for byte limit", err) + } +} + +func TestUnit_ValueValidationParity(t *testing.T) { + viewWith := func(value Value) ViewSchema { + regions := emptyRegions() + regions.Content = []Node{{ID: "value", Component: "text", Props: map[string]Value{"value": value}}} + return ViewSchema{ProtocolVersion: ProtocolVersion, ID: "values", Regions: regions} + } + + tooManyItems := make([]any, DefaultLimits().MaxArrayItems+1) + if err := viewWith(Literal(tooManyItems)).Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema for array limit", err) + } + + tooManyKeys := make(map[string]any, DefaultLimits().MaxObjectKeys+1) + for index := 0; index <= DefaultLimits().MaxObjectKeys; index++ { + tooManyKeys[fmt.Sprintf("key-%d", index)] = true + } + if err := viewWith(Literal(tooManyKeys)).Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema for object limit", err) + } + + if err := viewWith(Literal(map[string]any{unsafePrototypeKey: true})).Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema for unsafe key", err) + } + + longPath := strings.Repeat("a", DefaultLimits().MaxPathLength+1) + if err := viewWith(StateRef(longPath)).Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema for path limit", err) + } + + if err := viewWith(Literal([]any{1, 2})).ValidateWithLimits(Limits{MaxArrayItems: 1}); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("ValidateWithLimits() error = %v, want ErrInvalidSchema for custom array limit", err) + } +} + +func FuzzValueUnmarshal(f *testing.F) { + f.Add([]byte(`{"$state":"form.email"}`)) + f.Add([]byte(`{"$eq":[1,2]}`)) + f.Fuzz(func(t *testing.T, data []byte) { + var value Value + _ = json.Unmarshal(data, &value) + }) +} diff --git a/go-sdk/ui/security_test.go b/go-sdk/ui/security_test.go new file mode 100644 index 0000000..fd4e3c9 --- /dev/null +++ b/go-sdk/ui/security_test.go @@ -0,0 +1,54 @@ +package ui + +import ( + "encoding/json" + "errors" + "strings" + "testing" +) + +func TestUnit_SecuritySchemaBoundaries(t *testing.T) { + t.Run( + "unsafe value path", func(t *testing.T) { + var value Value + if err := json.Unmarshal([]byte(`{"$state":"constructor.name"}`), &value); !errors.Is( + err, + ErrInvalidValue, + ) { + t.Fatalf("Unmarshal() error = %v, want ErrInvalidValue", err) + } + }, + ) + t.Run( + "oversized serialized schema", func(t *testing.T) { + view := ViewSchema{ + ProtocolVersion: ProtocolVersion, + ID: "safe", + Title: strings.Repeat("x", 128), + Regions: Regions{}, + } + if err := view.ValidateWithLimits(Limits{MaxBytes: 64}); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("ValidateWithLimits() error = %v, want ErrInvalidSchema", err) + } + }, + ) + t.Run( + "unsafe action path", func(t *testing.T) { + view := ViewSchema{ + ProtocolVersion: ProtocolVersion, + ID: "safe", + Actions: map[string]Action{ + "save": { + Type: "set-state", + Path: "__proto__.polluted", + Value: ptrValue("x"), + }, + }, + Regions: Regions{}, + } + if err := view.Validate(); !errors.Is(err, ErrInvalidSchema) { + t.Fatalf("Validate() error = %v, want ErrInvalidSchema", err) + } + }, + ) +} diff --git a/go-sdk/ui/shadcn.go b/go-sdk/ui/shadcn.go new file mode 100644 index 0000000..14f6f43 --- /dev/null +++ b/go-sdk/ui/shadcn.go @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package ui + +// ShadcnComponent is a host-owned component name from the official shadcn/ui +// catalog. Register a matching React implementation before rendering it. +type ShadcnComponent string + +// Shadcn constants identify host-owned shadcn/ui components. +const ( + // ShadcnAccordion through ShadcnTypography enumerate supported shadcn/ui components. + ShadcnAccordion ShadcnComponent = "accordion" + // ShadcnAlert identifies the alert component. + ShadcnAlert ShadcnComponent = "alert" + // ShadcnAlertDialog identifies the alert dialog component. + ShadcnAlertDialog ShadcnComponent = "alert-dialog" + // ShadcnAspectRatio identifies the aspect ratio component. + ShadcnAspectRatio ShadcnComponent = "aspect-ratio" + ShadcnAttachment ShadcnComponent = "attachment" + ShadcnAvatar ShadcnComponent = "avatar" + ShadcnBadge ShadcnComponent = "badge" + ShadcnBreadcrumb ShadcnComponent = "breadcrumb" + ShadcnBubble ShadcnComponent = "bubble" + ShadcnButton ShadcnComponent = "button" + ShadcnButtonGroup ShadcnComponent = "button-group" + ShadcnCalendar ShadcnComponent = "calendar" + ShadcnCard ShadcnComponent = "card" + ShadcnCarousel ShadcnComponent = "carousel" + ShadcnChart ShadcnComponent = "chart" + ShadcnCheckbox ShadcnComponent = "checkbox" + ShadcnCollapsible ShadcnComponent = "collapsible" + ShadcnCombobox ShadcnComponent = "combobox" + ShadcnCommand ShadcnComponent = "command" + ShadcnContextMenu ShadcnComponent = "context-menu" + ShadcnDataTable ShadcnComponent = "data-table" + ShadcnDatePicker ShadcnComponent = "date-picker" + ShadcnDialog ShadcnComponent = "dialog" + ShadcnDirection ShadcnComponent = "direction" + ShadcnDrawer ShadcnComponent = "drawer" + ShadcnDropdownMenu ShadcnComponent = "dropdown-menu" + ShadcnEmpty ShadcnComponent = "empty" + ShadcnField ShadcnComponent = "field" + ShadcnHoverCard ShadcnComponent = "hover-card" + ShadcnInput ShadcnComponent = "input" + ShadcnInputGroup ShadcnComponent = "input-group" + ShadcnInputOTP ShadcnComponent = "input-otp" + ShadcnItem ShadcnComponent = "item" + ShadcnKbd ShadcnComponent = "kbd" + ShadcnLabel ShadcnComponent = "label" + ShadcnMarker ShadcnComponent = "marker" + ShadcnMenubar ShadcnComponent = "menubar" + ShadcnMessage ShadcnComponent = "message" + ShadcnMessageScroller ShadcnComponent = "message-scroller" + ShadcnNativeSelect ShadcnComponent = "native-select" + ShadcnNavigationMenu ShadcnComponent = "navigation-menu" + ShadcnPagination ShadcnComponent = "pagination" + ShadcnPopover ShadcnComponent = "popover" + ShadcnProgress ShadcnComponent = "progress" + ShadcnQuestionnaire ShadcnComponent = "questionnaire" + ShadcnRadioGroup ShadcnComponent = "radio-group" + ShadcnResizable ShadcnComponent = "resizable" + ShadcnScrollArea ShadcnComponent = "scroll-area" + ShadcnSelect ShadcnComponent = "select" + ShadcnSeparator ShadcnComponent = "separator" + ShadcnSheet ShadcnComponent = "sheet" + ShadcnSidebar ShadcnComponent = "sidebar" + ShadcnSkeleton ShadcnComponent = "skeleton" + ShadcnSlider ShadcnComponent = "slider" + ShadcnSpinner ShadcnComponent = "spinner" + ShadcnSwitch ShadcnComponent = "switch" + ShadcnTable ShadcnComponent = "table" + ShadcnTabs ShadcnComponent = "tabs" + ShadcnTextarea ShadcnComponent = "textarea" + ShadcnToast ShadcnComponent = "toast" + ShadcnToggle ShadcnComponent = "toggle" + ShadcnToggleGroup ShadcnComponent = "toggle-group" + ShadcnTooltip ShadcnComponent = "tooltip" + ShadcnTypography ShadcnComponent = "typography" +) + +// Shadcn creates a node for a component from the shadcn/ui catalog. +func Shadcn(component ShadcnComponent, id string) *NodeBuilder { return node(string(component), id) } + +// Accordion creates an accordion node. +func Accordion(id string) *NodeBuilder { return Shadcn(ShadcnAccordion, id) } + +// Alert creates an alert node. +func Alert(id string) *NodeBuilder { return Shadcn(ShadcnAlert, id) } + +// AlertDialog creates an alert dialog node. +func AlertDialog(id string) *NodeBuilder { return Shadcn(ShadcnAlertDialog, id) } + +// AspectRatio creates an aspect ratio node. +func AspectRatio(id string) *NodeBuilder { return Shadcn(ShadcnAspectRatio, id) } + +// Attachment creates an attachment node. +func Attachment(id string) *NodeBuilder { return Shadcn(ShadcnAttachment, id) } + +// Avatar creates an avatar node. +func Avatar(id string) *NodeBuilder { return Shadcn(ShadcnAvatar, id) } + +// Badge creates a badge node. +func Badge(id string) *NodeBuilder { return Shadcn(ShadcnBadge, id) } + +// Breadcrumb creates a breadcrumb node. +func Breadcrumb(id string) *NodeBuilder { return Shadcn(ShadcnBreadcrumb, id) } + +// Bubble creates a bubble node. +func Bubble(id string) *NodeBuilder { return Shadcn(ShadcnBubble, id) } + +// ButtonGroup creates a button group node. +func ButtonGroup(id string) *NodeBuilder { return Shadcn(ShadcnButtonGroup, id) } + +// Calendar creates a calendar node. +func Calendar(id string) *NodeBuilder { return Shadcn(ShadcnCalendar, id) } + +// Carousel creates a carousel node. +func Carousel(id string) *NodeBuilder { return Shadcn(ShadcnCarousel, id) } + +// Chart creates a chart node. +func Chart(id string) *NodeBuilder { return Shadcn(ShadcnChart, id) } + +// Checkbox creates a checkbox node. +func Checkbox(id string) *NodeBuilder { return Shadcn(ShadcnCheckbox, id) } + +// Collapsible creates a collapsible node. +func Collapsible(id string) *NodeBuilder { return Shadcn(ShadcnCollapsible, id) } + +// Combobox creates a combobox node. +func Combobox(id string) *NodeBuilder { return Shadcn(ShadcnCombobox, id) } + +// Command creates a command node. +func Command(id string) *NodeBuilder { return Shadcn(ShadcnCommand, id) } + +// ContextMenu creates a context menu node. +func ContextMenu(id string) *NodeBuilder { return Shadcn(ShadcnContextMenu, id) } + +// DataTable creates a data table node. +func DataTable(id string) *NodeBuilder { return Shadcn(ShadcnDataTable, id) } + +// DatePicker creates a date picker node. +func DatePicker(id string) *NodeBuilder { return Shadcn(ShadcnDatePicker, id) } + +// Dialog creates a dialog node. +func Dialog(id string) *NodeBuilder { return Shadcn(ShadcnDialog, id) } + +// Direction creates a direction node. +func Direction(id string) *NodeBuilder { return Shadcn(ShadcnDirection, id) } + +// Drawer creates a drawer node. +func Drawer(id string) *NodeBuilder { return Shadcn(ShadcnDrawer, id) } + +// DropdownMenu creates a dropdown menu node. +func DropdownMenu(id string) *NodeBuilder { return Shadcn(ShadcnDropdownMenu, id) } + +// Empty creates an empty-state node. +func Empty(id string) *NodeBuilder { return Shadcn(ShadcnEmpty, id) } + +// Field creates a field node. +func Field(id string) *NodeBuilder { return Shadcn(ShadcnField, id) } + +// HoverCard creates a hover card node. +func HoverCard(id string) *NodeBuilder { return Shadcn(ShadcnHoverCard, id) } + +// Input creates an input node. +func Input(id string) *NodeBuilder { return Shadcn(ShadcnInput, id) } + +// InputGroup creates an input group node. +func InputGroup(id string) *NodeBuilder { return Shadcn(ShadcnInputGroup, id) } + +// InputOTP creates an input OTP node. +func InputOTP(id string) *NodeBuilder { return Shadcn(ShadcnInputOTP, id) } + +// Item creates an item node. +func Item(id string) *NodeBuilder { return Shadcn(ShadcnItem, id) } + +// Kbd creates a keyboard shortcut node. +func Kbd(id string) *NodeBuilder { return Shadcn(ShadcnKbd, id) } + +// Label creates a label node. +func Label(id string) *NodeBuilder { return Shadcn(ShadcnLabel, id) } + +// Marker creates a marker node. +func Marker(id string) *NodeBuilder { return Shadcn(ShadcnMarker, id) } + +// Menubar creates a menubar node. +func Menubar(id string) *NodeBuilder { return Shadcn(ShadcnMenubar, id) } + +// Message creates a message node. +func Message(id string) *NodeBuilder { return Shadcn(ShadcnMessage, id) } + +// MessageScroller creates a message scroller node. +func MessageScroller(id string) *NodeBuilder { return Shadcn(ShadcnMessageScroller, id) } + +// NativeSelect creates a native select node. +func NativeSelect(id string) *NodeBuilder { return Shadcn(ShadcnNativeSelect, id) } + +// NavigationMenu creates a navigation menu node. +func NavigationMenu(id string) *NodeBuilder { return Shadcn(ShadcnNavigationMenu, id) } + +// Pagination creates a pagination node. +func Pagination(id string) *NodeBuilder { return Shadcn(ShadcnPagination, id) } + +// Popover creates a popover node. +func Popover(id string) *NodeBuilder { return Shadcn(ShadcnPopover, id) } + +// Progress creates a progress node. +func Progress(id string) *NodeBuilder { return Shadcn(ShadcnProgress, id) } + +// Questionnaire creates a questionnaire node. +func Questionnaire(id string) *NodeBuilder { return Shadcn(ShadcnQuestionnaire, id) } + +// RadioGroup creates a radio group node. +func RadioGroup(id string) *NodeBuilder { return Shadcn(ShadcnRadioGroup, id) } + +// Resizable creates a resizable node. +func Resizable(id string) *NodeBuilder { return Shadcn(ShadcnResizable, id) } + +// ScrollArea creates a scroll area node. +func ScrollArea(id string) *NodeBuilder { return Shadcn(ShadcnScrollArea, id) } + +// Select creates a select node. +func Select(id string) *NodeBuilder { return Shadcn(ShadcnSelect, id) } + +// Separator creates a separator node. +func Separator(id string) *NodeBuilder { return Shadcn(ShadcnSeparator, id) } + +// Sheet creates a sheet node. +func Sheet(id string) *NodeBuilder { return Shadcn(ShadcnSheet, id) } + +// Sidebar creates a sidebar node. +func Sidebar(id string) *NodeBuilder { return Shadcn(ShadcnSidebar, id) } + +// Skeleton creates a skeleton node. +func Skeleton(id string) *NodeBuilder { return Shadcn(ShadcnSkeleton, id) } + +// Slider creates a slider node. +func Slider(id string) *NodeBuilder { return Shadcn(ShadcnSlider, id) } + +// Spinner creates a spinner node. +func Spinner(id string) *NodeBuilder { return Shadcn(ShadcnSpinner, id) } + +// Switch creates a switch node. +func Switch(id string) *NodeBuilder { return Shadcn(ShadcnSwitch, id) } + +// Tabs creates a tabs node. +func Tabs(id string) *NodeBuilder { return Shadcn(ShadcnTabs, id) } + +// Textarea creates a textarea node. +func Textarea(id string) *NodeBuilder { return Shadcn(ShadcnTextarea, id) } + +// Toast creates a toast node. +func Toast(id string) *NodeBuilder { return Shadcn(ShadcnToast, id) } + +// Toggle creates a toggle node. +func Toggle(id string) *NodeBuilder { return Shadcn(ShadcnToggle, id) } + +// ToggleGroup creates a toggle group node. +func ToggleGroup(id string) *NodeBuilder { return Shadcn(ShadcnToggleGroup, id) } + +// Tooltip creates a tooltip node. +func Tooltip(id string) *NodeBuilder { return Shadcn(ShadcnTooltip, id) } + +// Typography creates a typography node. +func Typography(id string) *NodeBuilder { return Shadcn(ShadcnTypography, id) } diff --git a/go-sdk/ui/shadcn_test.go b/go-sdk/ui/shadcn_test.go new file mode 100644 index 0000000..1633db2 --- /dev/null +++ b/go-sdk/ui/shadcn_test.go @@ -0,0 +1,90 @@ +package ui + +import "testing" + +func TestUnit_ShadcnBuildersProduceCatalogNames(t *testing.T) { + tests := []struct { + name string + component ShadcnComponent + }{ + {"accordion", ShadcnAccordion}, + {"alert", ShadcnAlert}, + {"alert dialog", ShadcnAlertDialog}, + {"aspect ratio", ShadcnAspectRatio}, + {"attachment", ShadcnAttachment}, + {"avatar", ShadcnAvatar}, + {"badge", ShadcnBadge}, + {"breadcrumb", ShadcnBreadcrumb}, + {"bubble", ShadcnBubble}, + {"button", ShadcnButton}, + {"button group", ShadcnButtonGroup}, + {"calendar", ShadcnCalendar}, + {"card", ShadcnCard}, + {"carousel", ShadcnCarousel}, + {"chart", ShadcnChart}, + {"checkbox", ShadcnCheckbox}, + {"collapsible", ShadcnCollapsible}, + {"combobox", ShadcnCombobox}, + {"command", ShadcnCommand}, + {"context menu", ShadcnContextMenu}, + {"data table", ShadcnDataTable}, + {"date picker", ShadcnDatePicker}, + {"dialog", ShadcnDialog}, + {"direction", ShadcnDirection}, + {"drawer", ShadcnDrawer}, + {"dropdown menu", ShadcnDropdownMenu}, + {"empty", ShadcnEmpty}, + {"field", ShadcnField}, + {"hover card", ShadcnHoverCard}, + {"input", ShadcnInput}, + {"input group", ShadcnInputGroup}, + {"input otp", ShadcnInputOTP}, + {"item", ShadcnItem}, + {"kbd", ShadcnKbd}, + {"label", ShadcnLabel}, + {"marker", ShadcnMarker}, + {"menubar", ShadcnMenubar}, + {"message", ShadcnMessage}, + {"message scroller", ShadcnMessageScroller}, + {"native select", ShadcnNativeSelect}, + {"navigation menu", ShadcnNavigationMenu}, + {"pagination", ShadcnPagination}, + {"popover", ShadcnPopover}, + {"progress", ShadcnProgress}, + {"questionnaire", ShadcnQuestionnaire}, + {"radio group", ShadcnRadioGroup}, + {"resizable", ShadcnResizable}, + {"scroll area", ShadcnScrollArea}, + {"select", ShadcnSelect}, + {"separator", ShadcnSeparator}, + {"sheet", ShadcnSheet}, + {"sidebar", ShadcnSidebar}, + {"skeleton", ShadcnSkeleton}, + {"slider", ShadcnSlider}, + {"spinner", ShadcnSpinner}, + {"switch", ShadcnSwitch}, + {"table", ShadcnTable}, + {"tabs", ShadcnTabs}, + {"textarea", ShadcnTextarea}, + {"toast", ShadcnToast}, + {"toggle", ShadcnToggle}, + {"toggle group", ShadcnToggleGroup}, + {"tooltip", ShadcnTooltip}, + {"typography", ShadcnTypography}, + } + for _, test := range tests { + t.Run( + test.name, func(t *testing.T) { + if got := Shadcn(test.component, "node").Build().Component; got != string(test.component) { + t.Fatalf("component = %q, want %q", got, test.component) + } + }, + ) + } +} + +func TestUnit_ShadcnConvenienceBuilder(t *testing.T) { + if got := AlertDialog("confirm").Build().Component; got != string(ShadcnAlertDialog) { + t.Fatalf("AlertDialog() component = %q, want %q", got, ShadcnAlertDialog) + } +} diff --git a/go-sdk/ui/validation.go b/go-sdk/ui/validation.go new file mode 100644 index 0000000..692de5f --- /dev/null +++ b/go-sdk/ui/validation.go @@ -0,0 +1,611 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package ui + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "strings" +) + +const ( + defaultMaxBytes = 1 << 20 + defaultMaxNodes = 1000 + defaultMaxDepth = 32 + defaultMaxPropsPerNode = 64 + defaultMaxActions = 128 + defaultMaxSources = 64 + defaultMaxStrings = 10000 + defaultMaxStringLength = 16 << 10 + defaultMaxExpressionDepth = 32 + defaultMaxArrayItems = 1000 + defaultMaxObjectKeys = 256 + defaultMaxPathLength = 1024 + + actionTypeTool = "tool" + actionTypeSetState = "set-state" + actionTypeMergeState = "merge-state" + actionTypeRefreshSource = "refresh-source" + actionTypeInvalidate = "invalidate" + actionStepTypeAction = "action" +) + +// Limits bounds the size and complexity of a manifest or view schema. Fields +// set to zero or less use the corresponding DefaultLimits value. +type Limits struct { + MaxBytes int + MaxNodes int + MaxDepth int + MaxPropsPerNode int + MaxActions int + MaxSources int + MaxStrings int + MaxStringLength int + MaxExpressionDepth int + MaxArrayItems int + MaxObjectKeys int + MaxPathLength int +} + +// DefaultLimits returns the default bounds used by schema validation. +func DefaultLimits() Limits { + return Limits{ + MaxBytes: defaultMaxBytes, + MaxNodes: defaultMaxNodes, + MaxDepth: defaultMaxDepth, + MaxPropsPerNode: defaultMaxPropsPerNode, + MaxActions: defaultMaxActions, + MaxSources: defaultMaxSources, + MaxStrings: defaultMaxStrings, + MaxStringLength: defaultMaxStringLength, + MaxExpressionDepth: defaultMaxExpressionDepth, + MaxArrayItems: defaultMaxArrayItems, + MaxObjectKeys: defaultMaxObjectKeys, + MaxPathLength: defaultMaxPathLength, + } +} + +// Validate checks the view schema against DefaultLimits. +func (v ViewSchema) Validate() error { + return v.ValidateWithLimits(DefaultLimits()) +} + +// ValidateWithLimits checks the view schema against limits. Any non-positive +// limit field is replaced with its DefaultLimits value. +func (v ViewSchema) ValidateWithLimits(limits Limits) error { + limits = withDefaultLimits(limits) + + if v.ProtocolVersion != ProtocolVersion { + return fmt.Errorf("%w: unsupported view protocol version %q", ErrInvalidSchema, v.ProtocolVersion) + } + + if !validName(v.ID) { + return fmt.Errorf("%w: view id is required", ErrInvalidSchema) + } + + if len(v.Actions) > limits.MaxActions || len(v.Sources) > limits.MaxSources { + return fmt.Errorf("%w: source or action limit exceeded", ErrInvalidSchema) + } + + if v.Regions.TopHeader == nil || + v.Regions.LeftPanel == nil || + v.Regions.Content == nil || + v.Regions.RightPanel == nil || + v.Regions.Bottom == nil { + return fmt.Errorf("%w: all regions must be arrays", ErrInvalidSchema) + } + + if err := validateSerializedLimits(v, limits); err != nil { + return err + } + + for name, source := range v.Sources { + if !validName(name) || source.Type != actionTypeTool || !validName(source.Tool) { + return fmt.Errorf("%w: invalid source %q", ErrInvalidSchema, name) + } + + if source.Policy != "" && source.Policy != SourceManual && source.Policy != SourceOnMount { + return fmt.Errorf("%w: source %q has unsupported policy", ErrInvalidSchema, name) + } + + if source.Cache != nil && source.Cache.TTL < 0 { + return fmt.Errorf("%w: source %q has negative cache ttl", ErrInvalidSchema, name) + } + + if err := validateValues(source.Input, limits, v); err != nil { + return fmt.Errorf("%w: source %q input: %w", ErrInvalidSchema, name, err) + } + + for _, action := range source.RefreshOn { + if _, ok := v.Actions[action]; !ok { + return fmt.Errorf("%w: source %q refresh action %q is not defined", ErrInvalidSchema, name, action) + } + } + } + + for name, action := range v.Actions { + if !validName(name) || !validActionType(action.Type) { + return fmt.Errorf("%w: invalid action %q", ErrInvalidSchema, name) + } + + if err := validateAction(action, limits, v); err != nil { + return fmt.Errorf("%w: action %q: %w", ErrInvalidSchema, name, err) + } + } + + context := validationContext{ + limits: limits, + view: v, + seen: make(map[string]struct{}), + } + + for region, list := range map[string][]Node{ + "top-header": v.Regions.TopHeader, + "left-panel": v.Regions.LeftPanel, + "content": v.Regions.Content, + "right-panel": v.Regions.RightPanel, + "bottom": v.Regions.Bottom, + } { + for _, n := range list { + if err := context.validateNode(n, region, 1); err != nil { + return err + } + } + } + + return nil +} + +func withDefaultLimits(limits Limits) Limits { + defaults := DefaultLimits() + + if limits.MaxBytes <= 0 { + limits.MaxBytes = defaults.MaxBytes + } + + if limits.MaxNodes <= 0 { + limits.MaxNodes = defaults.MaxNodes + } + + if limits.MaxDepth <= 0 { + limits.MaxDepth = defaults.MaxDepth + } + + if limits.MaxPropsPerNode <= 0 { + limits.MaxPropsPerNode = defaults.MaxPropsPerNode + } + + if limits.MaxActions <= 0 { + limits.MaxActions = defaults.MaxActions + } + + if limits.MaxSources <= 0 { + limits.MaxSources = defaults.MaxSources + } + + if limits.MaxStrings <= 0 { + limits.MaxStrings = defaults.MaxStrings + } + + if limits.MaxStringLength <= 0 { + limits.MaxStringLength = defaults.MaxStringLength + } + + if limits.MaxExpressionDepth <= 0 { + limits.MaxExpressionDepth = defaults.MaxExpressionDepth + } + + if limits.MaxArrayItems <= 0 { + limits.MaxArrayItems = defaults.MaxArrayItems + } + + if limits.MaxObjectKeys <= 0 { + limits.MaxObjectKeys = defaults.MaxObjectKeys + } + + if limits.MaxPathLength <= 0 { + limits.MaxPathLength = defaults.MaxPathLength + } + + return limits +} + +func validateSerializedLimits(schema any, limits Limits) error { + encoded, err := json.Marshal(schema) + if err != nil { + return fmt.Errorf("%w: serialize schema for limits: %w", ErrInvalidSchema, err) + } + + if len(encoded) > limits.MaxBytes { + return fmt.Errorf("%w: schema byte limit exceeded", ErrInvalidSchema) + } + + decoder := json.NewDecoder(bytes.NewReader(encoded)) + decoder.UseNumber() + var document any + + if err := decoder.Decode(&document); err != nil { + return fmt.Errorf("%w: inspect schema limits: %w", ErrInvalidSchema, err) + } + + count := 0 + if err := walkLimitValue(document, 0, limits, &count); err != nil { + return fmt.Errorf("%w: %w", ErrInvalidSchema, err) + } + + return nil +} + +func walkLimitValue(value any, depth int, limits Limits, count *int) error { + if depth > limits.MaxDepth { + return errors.New("schema depth limit exceeded") + } + + switch typed := value.(type) { + case string: + if len(typed) > limits.MaxStringLength { + return errors.New("string length limit exceeded") + } + + *count++ + if *count > limits.MaxStrings { + return errors.New("string count limit exceeded") + } + + case []any: + if len(typed) > limits.MaxArrayItems { + return errors.New("array item limit exceeded") + } + + for _, item := range typed { + if err := walkLimitValue(item, depth+1, limits, count); err != nil { + return err + } + } + + case map[string]any: + if len(typed) > limits.MaxObjectKeys { + return errors.New("object key limit exceeded") + } + + for key, item := range typed { + if key == unsafePrototypeKey || key == unsafePrototypeProperty || key == unsafeConstructorKey { + return errors.New("unsafe object key") + } + + if err := walkLimitValue(key, depth+1, limits, count); err != nil { + return err + } + + if err := walkLimitValue(item, depth+1, limits, count); err != nil { + return err + } + } + + default: + } + + return nil +} + +type validationContext struct { + limits Limits + view ViewSchema + seen map[string]struct{} + nodes int +} + +func (context *validationContext) validateNode(node Node, region string, depth int) error { + context.nodes++ + if context.nodes > context.limits.MaxNodes || depth > context.limits.MaxDepth { + return fmt.Errorf("%w: node limit exceeded", ErrInvalidSchema) + } + + if !validName(node.ID) || !validComponentName(node.Component) { + return fmt.Errorf("%w: node id and component are required", ErrInvalidSchema) + } + + if _, exists := context.seen[node.ID]; exists { + return fmt.Errorf("%w: duplicate node id %q", ErrInvalidSchema, node.ID) + } + + context.seen[node.ID] = struct{}{} + if node.Layout != nil { + if err := node.Layout.Validate(region); err != nil { + return err + } + } + + if len(node.Props) > context.limits.MaxPropsPerNode { + return fmt.Errorf("%w: too many props on node %q", ErrInvalidSchema, node.ID) + } + + if err := validateValues(node.Props, context.limits, context.view); err != nil { + return fmt.Errorf("%w: node %q props: %w", ErrInvalidSchema, node.ID, err) + } + + for event, handler := range node.Events { + if !validName(event) || handler.Action == "" && len(handler.Steps) == 0 { + return fmt.Errorf("%w: invalid event on node %q", ErrInvalidSchema, node.ID) + } + + if handler.Action != "" { + if _, ok := context.view.Actions[handler.Action]; !ok { + return fmt.Errorf("%w: action %q is not defined", ErrInvalidSchema, handler.Action) + } + } + + for _, step := range handler.Steps { + if step.Type != actionStepTypeAction || step.Action == "" { + return fmt.Errorf("%w: invalid event step on node %q", ErrInvalidSchema, node.ID) + } + + if _, ok := context.view.Actions[step.Action]; !ok { + return fmt.Errorf("%w: action %q is not defined", ErrInvalidSchema, step.Action) + } + + if err := validateValues(step.Input, context.limits, context.view); err != nil { + return fmt.Errorf("%w: event step input: %w", ErrInvalidSchema, err) + } + } + } + + if node.When != nil { + if err := validateValue(*node.When, context.limits, context.view, 0); err != nil { + return fmt.Errorf("%w: node %q condition: %w", ErrInvalidSchema, node.ID, err) + } + } + + for _, child := range node.Children { + if err := context.validateNode(child, region, depth+1); err != nil { + return err + } + } + + for _, children := range node.Slots { + for _, child := range children { + if err := context.validateNode(child, region, depth+1); err != nil { + return err + } + } + } + + return nil +} + +// Validate checks whether the layout fits region. Side panels accept only a +// positive row and require columns and offset to be zero; other regions use a +// twelve-column grid. +func (l Layout) Validate(region string) error { + if l.Row < 1 { + return fmt.Errorf("%w: row must be positive", ErrInvalidLayout) + } + + if region == "left-panel" || region == "right-panel" { + if l.Cols != 0 || l.Offset != 0 { + return fmt.Errorf("%w: side panels do not accept cols or offset", ErrInvalidLayout) + } + return nil + } + + if l.Cols < 1 || l.Cols > 12 || l.Offset < 0 || l.Offset > 11 || l.Cols+l.Offset > 12 { + return fmt.Errorf("%w: cols and offset must fit 12 columns", ErrInvalidLayout) + } + + return nil +} + +func validateValues(values map[string]Value, limits Limits, view ViewSchema) error { + for key, value := range values { + if len(key) > limits.MaxStringLength { + return errors.New("property name is too long") + } + + if err := validateValue(value, limits, view, 0); err != nil { + return err + } + } + + return nil +} + +func validateAction(action Action, limits Limits, view ViewSchema) error { + if action.Type == actionTypeTool && !validName(action.Tool) { + return errors.New("tool action requires a tool") + } + + if action.Type == actionTypeSetState || action.Type == actionTypeMergeState { + if !safePathWithLimit(action.Path, limits.MaxPathLength) { + return errors.New("state action requires a safe path") + } + } + + if action.Type == actionTypeRefreshSource || action.Type == actionTypeInvalidate { + if _, ok := view.Sources[action.Source]; !ok { + return fmt.Errorf("source %q is not defined", action.Source) + } + } + + if action.Path != "" && !safePathWithLimit(action.Path, limits.MaxPathLength) { + return errors.New("unsafe action path") + } + + if err := validateValues(action.Input, limits, view); err != nil { + return err + } + + if action.Value != nil { + if err := validateValue(*action.Value, limits, view, 0); err != nil { + return err + } + } + + for _, effect := range action.Effects { + if err := validateEffect(effect, limits, view); err != nil { + return err + } + } + + return nil +} + +func validateEffect(effect Effect, limits Limits, view ViewSchema) error { + if effect.Type == "" { + return errors.New("effect type is required") + } + + if !validEffectType(effect.Type) { + return fmt.Errorf("unsupported effect type %q", effect.Type) + } + + if effect.Path != "" && !safePathWithLimit(effect.Path, limits.MaxPathLength) { + return errors.New("unsafe effect path") + } + + if effect.Type == actionTypeSetState || effect.Type == actionTypeMergeState { + if !safePathWithLimit(effect.Path, limits.MaxPathLength) { + return errors.New("state effect requires a safe path") + } + } + + if effect.Type == actionTypeInvalidate || effect.Type == actionTypeRefreshSource { + if _, ok := view.Sources[effect.Source]; !ok { + return fmt.Errorf("source %q is not defined", effect.Source) + } + } + + if effect.Type == "toast" && effect.Message == "" { + return errors.New("toast effect requires a message") + } + + if effect.Type == "navigate" && effect.To == "" { + return errors.New("navigate effect requires a target") + } + + if effect.Value != nil { + if err := validateValue(*effect.Value, limits, view, 0); err != nil { + return err + } + } + + return nil +} + +func validateValue(value Value, limits Limits, view ViewSchema, depth int) error { + if depth > limits.MaxExpressionDepth { + return errors.New("expression depth limit exceeded") + } + + switch value.Kind { + case ValueLiteral: + return validateLiteralData(value.Data, limits, depth) + + case ValueState, ValueContext, ValueEvent, ValueResult: + if !safePathWithLimit(value.Path, limits.MaxPathLength) { + return fmt.Errorf("%w: unsafe reference path %q", ErrInvalidValue, value.Path) + } + + return nil + + case ValueSource: + if !safePathWithLimit(value.Path, limits.MaxPathLength) { + return fmt.Errorf("%w: unsafe reference path %q", ErrInvalidValue, value.Path) + } + + if _, ok := sourceForPath(value.Path, view.Sources); !ok { + return fmt.Errorf("source %q is not defined", value.Path) + } + + return nil + + case ValueExpr: + args, ok := value.Data.([]Value) + if !ok { + return errors.New("expression arguments have invalid type") + } + + arity, ok := expressionArity[value.Path] + if !ok { + return fmt.Errorf("%w: unsupported expression operator %q", ErrInvalidValue, value.Path) + } + + if len(args) < arity[0] || (arity[1] >= 0 && len(args) > arity[1]) { + return fmt.Errorf( + "%w: operator %s expects %d..%d arguments", + ErrInvalidValue, + value.Path, + arity[0], + arity[1], + ) + } + + for _, arg := range args { + if err := validateValue(arg, limits, view, depth+1); err != nil { + return err + } + } + + return nil + + default: + return fmt.Errorf("%w: unknown value kind %q", ErrInvalidValue, value.Kind) + } +} + +func sourceForPath(path string, sources map[string]DataSource) (string, bool) { + for name := path; name != ""; { + if _, ok := sources[name]; ok { + return name, true + } + + index := strings.LastIndexByte(name, '.') + if index < 0 { + break + } + + name = name[:index] + } + + return "", false +} + +func validActionType(value string) bool { + switch value { + case actionTypeTool, actionTypeSetState, actionTypeMergeState, actionTypeRefreshSource, actionTypeInvalidate: + return true + default: + return false + } +} + +func validEffectType(value string) bool { + switch value { + case actionTypeSetState, actionTypeMergeState, actionTypeInvalidate, actionTypeRefreshSource, + "refresh-view", "patch-view", "navigate", "toast", + "dialog", "close-dialog": + return true + default: + return false + } +} + +func validComponentName(value string) bool { + if value == "" { + return false + } + + for index, char := range value { + if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9' && index > 0) || (char == '-' && index > 0) { + continue + } + + return false + } + + return true +} diff --git a/go-sdk/ui/value.go b/go-sdk/ui/value.go new file mode 100644 index 0000000..38f0cd2 --- /dev/null +++ b/go-sdk/ui/value.go @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2026 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package ui + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +// ValueKind identifies how a Value obtains its data. +type ValueKind string + +// Value kind constants identify the supported value representations. +const ( + // ValueLiteral stores data directly in the value. + ValueLiteral ValueKind = "literal" + // ValueState references view state. + ValueState ValueKind = "state" + // ValueContext references host context. + ValueContext ValueKind = "context" + // ValueSource references source data. + ValueSource ValueKind = "source" + // ValueEvent references the current event payload. + ValueEvent ValueKind = "event" + // ValueResult references the result produced by a preceding action step. + ValueResult ValueKind = "result" + // ValueExpr represents an expression evaluated by the UI runtime. + ValueExpr ValueKind = "expression" +) + +const ( + unsafePrototypeKey = "__proto__" + unsafePrototypeProperty = "prototype" + unsafeConstructorKey = "constructor" +) + +// Value is a literal, reference, or expression used by the declarative UI model. +// +//nolint:recvcheck // JSON values intentionally marshal by value and unmarshal by pointer. +type Value struct { + Kind ValueKind + Path string + Data any +} + +var expressionArity = map[string][2]int{ + "$eq": {2, 2}, + "$ne": {2, 2}, + "$gt": {2, 2}, + "$gte": {2, 2}, + "$lt": {2, 2}, + "$lte": {2, 2}, + "$and": {1, -1}, + "$or": {1, -1}, + "$not": {1, 1}, + "$in": {2, 2}, + "$exists": {1, 1}, + "$concat": {0, -1}, + "$coalesce": {1, -1}, + "$if": {3, 3}, +} + +// Literal creates a value containing data directly. +func Literal(data any) Value { + return Value{Kind: ValueLiteral, Data: data} +} + +// StateRef creates a value referencing view state at path. +func StateRef(path string) Value { + return Value{Kind: ValueState, Path: path} +} + +// ContextRef creates a value referencing host context at path. +func ContextRef(path string) Value { + return Value{Kind: ValueContext, Path: path} +} + +// SourceRef creates a value referencing source data at path. +func SourceRef(path string) Value { + return Value{Kind: ValueSource, Path: path} +} + +// EventRef creates a value referencing the current event at path. +func EventRef(path string) Value { + return Value{Kind: ValueEvent, Path: path} +} + +// ResultRef creates a value referencing the previous action result at path. +func ResultRef(path string) Value { + return Value{Kind: ValueResult, Path: path} +} + +// Expr creates a value representing an expression operator and its arguments. +// The operator must be supported by Validate; argument count is checked there. +func Expr(operator string, args ...Value) Value { + return Value{Kind: ValueExpr, Path: operator, Data: append([]Value(nil), args...)} +} + +// Validate checks that the value kind, path, and expression arguments are valid. +func (v Value) Validate() error { + switch v.Kind { + case ValueLiteral: + return validateLiteralData(v.Data, DefaultLimits(), 0) + + case ValueState, ValueContext, ValueSource, ValueEvent, ValueResult: + if !safePath(v.Path) { + return fmt.Errorf("%w: unsafe reference path %q", ErrInvalidValue, v.Path) + } + + return nil + + case ValueExpr: + arity, ok := expressionArity[v.Path] + if !ok { + return fmt.Errorf("%w: unsupported expression operator %q", ErrInvalidValue, v.Path) + } + + if v.Path == "" { + return fmt.Errorf("%w: expression operator is required", ErrInvalidValue) + } + + args, ok := v.Data.([]Value) + if !ok { + return fmt.Errorf("%w: expression arguments have invalid type", ErrInvalidValue) + } + + if len(args) < arity[0] || (arity[1] >= 0 && len(args) > arity[1]) { + return fmt.Errorf("%w: operator %s expects %d..%d arguments", ErrInvalidValue, v.Path, arity[0], arity[1]) + } + + for _, arg := range args { + if err := arg.Validate(); err != nil { + return err + } + } + + return nil + + default: + return fmt.Errorf("%w: unknown value kind %q", ErrInvalidValue, v.Kind) + } +} + +// MarshalJSON encodes the value in the declarative UI wire format. Invalid +// values return an error wrapping ErrInvalidValue. +func (v Value) MarshalJSON() ([]byte, error) { + if err := v.Validate(); err != nil { + return nil, err + } + + if v.Kind == ValueLiteral { + return json.Marshal(v.Data) + } + + if v.Kind == ValueExpr { + args, ok := v.Data.([]Value) + if !ok { + return nil, fmt.Errorf("%w: expression arguments have invalid type", ErrInvalidValue) + } + + return json.Marshal(map[string]any{v.Path: args}) + } + + return json.Marshal(map[string]string{"$" + string(v.Kind): v.Path}) +} + +func (v *Value) unmarshalSpecial(data []byte) (bool, error) { + var object map[string]json.RawMessage + if err := json.Unmarshal(data, &object); err != nil { + return false, fmt.Errorf("%w: %s", ErrInvalidValue, err.Error()) + } + + if len(object) != 1 { + return false, fmt.Errorf("%w: expected 1 object, got %d", ErrInvalidValue, len(object)) + } + + for key, raw := range object { + return v.unmarshalSpecialEntry(key, raw) + } + + return false, nil +} + +func (v *Value) unmarshalSpecialEntry(key string, raw json.RawMessage) (bool, error) { + switch key { + case "$state", "$context", "$source", "$event", "$result": + var path string + if err := json.Unmarshal(raw, &path); err != nil { + return true, fmt.Errorf("%w: reference path: %w", ErrInvalidValue, err) + } + + v.Kind = ValueKind(key[1:]) + v.Path = path + v.Data = nil + return true, v.Validate() + } + + if len(key) == 0 || key[0] != '$' { + return false, fmt.Errorf("%w: reference path %q", ErrInvalidValue, key) + } + + var args []Value + if err := json.Unmarshal(raw, &args); err != nil { + return false, fmt.Errorf("%w: reference arguments have invalid type", ErrInvalidValue) + } + + v.Kind = ValueExpr + v.Path = key + v.Data = args + + return true, v.Validate() +} + +// UnmarshalJSON decodes a literal, reference, or expression value. Special +// objects must use one recognized reference or expression operator; decoded +// references and expressions are validated before they are stored. +func (v *Value) UnmarshalJSON(data []byte) error { + if v == nil { + return fmt.Errorf("%w: nil destination", ErrInvalidValue) + } + + handled, err := v.unmarshalSpecial(data) + if handled { + return err + } + + var literal any + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + if err = decoder.Decode(&literal); err != nil { + return fmt.Errorf("%w: literal: %w", ErrInvalidValue, err) + } + + v.Kind = ValueLiteral + v.Path = "" + v.Data = literal + + return nil +} + +func safePath(path string) bool { + return safePathWithLimit(path, defaultMaxPathLength) +} + +func safePathWithLimit(path string, maxLength int) bool { + if path == "" || len(path) > maxLength { + return false + } + + for _, part := range strings.Split(path, ".") { + if part == unsafePrototypeKey || part == unsafePrototypeProperty || part == unsafeConstructorKey || part == "" { + return false + } + } + + return true +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f8e5c62 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module go.osspkg.com/ui + +go 1.26.8 + +require ( + github.com/mailru/easyjson v0.9.2 + go.osspkg.com/mcp v0.1.2 +) + +require github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6c78a69 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +go.osspkg.com/mcp v0.1.2 h1:QOo55G9VaNH0KLcSF33eCx7/2r0gIJJySNwzkzOldOo= +go.osspkg.com/mcp v0.1.2/go.mod h1:/gWcxJAjnVPgcHrcw+lYjpD9uC4xe/YDRTVXAoFfbdo= diff --git a/package.json b/package.json new file mode 100644 index 0000000..f19d954 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "@osspkg/ui-workspace", + "private": true, + "packageManager": "pnpm@12.6.0", + "scripts": { + "build": "pnpm --recursive --if-present build", + "test": "pnpm --recursive --if-present test", + "typecheck": "pnpm --recursive --if-present typecheck", + "lint": "eslint \"react-sdk/**/*.{ts,tsx}\" \"example/react/**/*.{ts,tsx}\"", + "format": "prettier --write \"react-sdk/**/*.{ts,tsx,css}\" \"example/react/**/*.{ts,tsx,css,html}\"", + "format:check": "prettier --check \"react-sdk/**/*.{ts,tsx,css}\" \"example/react/**/*.{ts,tsx,css,html}\"" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.7", + "@eslint/js": "^8.57.1", + "@types/node": "^24.0.0", + "eslint": "^8.57.1", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^4.6.2", + "globals": "^17.12.0", + "prettier": "^3.9.9", + "typescript": "^5.9.0", + "typescript-eslint": "^8.70.1", + "vitest": "^3.2.0" + }, + "type": "module" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..a393acf --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4830 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.6.0 + version: 12.6.0 + +packages: + + '@pnpm/exe.android-arm64@12.6.0': + resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.6.0': + resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.6.0': + resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.6.0': + resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.6.0': + resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.6.0': + resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.6.0': + resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.6.0': + resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.6.0': + resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.6.0': + resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.6.0': + resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.6.0': + resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.6.0': + resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.6.0': + resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + cpu: [x64] + os: [win32] + + pnpm@12.6.0: + resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.6.0': + optional: true + + '@pnpm/exe.android-x64@12.6.0': + optional: true + + '@pnpm/exe.darwin-arm64@12.6.0': + optional: true + + '@pnpm/exe.darwin-x64@12.6.0': + optional: true + + '@pnpm/exe.freebsd-x64@12.6.0': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.6.0': + optional: true + + '@pnpm/exe.linux-arm64@12.6.0': + optional: true + + '@pnpm/exe.linux-ppc64@12.6.0': + optional: true + + '@pnpm/exe.linux-riscv64@12.6.0': + optional: true + + '@pnpm/exe.linux-s390x@12.6.0': + optional: true + + '@pnpm/exe.linux-x64-musl@12.6.0': + optional: true + + '@pnpm/exe.linux-x64@12.6.0': + optional: true + + '@pnpm/exe.win32-arm64@12.6.0': + optional: true + + '@pnpm/exe.win32-x64@12.6.0': + optional: true + + pnpm@12.6.0: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.6.0 + '@pnpm/exe.android-x64': 12.6.0 + '@pnpm/exe.darwin-arm64': 12.6.0 + '@pnpm/exe.darwin-x64': 12.6.0 + '@pnpm/exe.freebsd-x64': 12.6.0 + '@pnpm/exe.linux-arm64': 12.6.0 + '@pnpm/exe.linux-arm64-musl': 12.6.0 + '@pnpm/exe.linux-ppc64': 12.6.0 + '@pnpm/exe.linux-riscv64': 12.6.0 + '@pnpm/exe.linux-s390x': 12.6.0 + '@pnpm/exe.linux-x64': 12.6.0 + '@pnpm/exe.linux-x64-musl': 12.6.0 + '@pnpm/exe.win32-arm64': 12.6.0 + '@pnpm/exe.win32-x64': 12.6.0 + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/eslintrc': + specifier: ^3.3.7 + version: 3.3.7(supports-color@7.2.0) + '@eslint/js': + specifier: ^8.57.1 + version: 8.57.1 + '@types/node': + specifier: ^24.0.0 + version: 24.13.6 + eslint: + specifier: ^8.57.1 + version: 8.57.1(supports-color@7.2.0) + eslint-config-airbnb: + specifier: ^19.0.4 + version: 19.0.4(eslint-plugin-import@2.32.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1(supports-color@7.2.0)))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1(supports-color@7.2.0)))(eslint-plugin-react@7.37.5(eslint@8.57.1(supports-color@7.2.0)))(eslint@8.57.1(supports-color@7.2.0)) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@8.57.1(supports-color@7.2.0)) + eslint-plugin-import: + specifier: ^2.32.0 + version: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-jsx-a11y: + specifier: ^6.10.2 + version: 6.10.2(eslint@8.57.1(supports-color@7.2.0)) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@8.57.1(supports-color@7.2.0)) + eslint-plugin-react-hooks: + specifier: ^4.6.2 + version: 4.6.2(eslint@8.57.1(supports-color@7.2.0)) + globals: + specifier: ^17.12.0 + version: 17.12.0 + prettier: + specifier: ^3.9.9 + version: 3.9.9 + typescript: + specifier: ^5.9.0 + version: 5.9.3 + typescript-eslint: + specifier: ^8.70.1 + version: 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + vitest: + specifier: ^3.2.0 + version: 3.2.7(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0) + + example/react: + dependencies: + '@osspkg/ui-core': + specifier: workspace:* + version: link:../../react-sdk/ui-core + '@osspkg/ui-react': + specifier: workspace:* + version: link:../../react-sdk/ui-react + devDependencies: + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)) + '@types/react': + specifier: ^19.0.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.3.0(@types/react@19.3.0) + react: + specifier: ^19.0.0 + version: 19.3.0 + react-dom: + specifier: ^19.0.0 + version: 19.3.0(react@19.3.0) + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ^5.9.0 + version: 5.9.3 + vite: + specifier: ^7.3.6 + version: 7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0) + + react-sdk/ui-core: + devDependencies: + typescript: + specifier: ^5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.2.0 + version: 3.2.7(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0) + + react-sdk/ui-react: + dependencies: + '@osspkg/ui-core': + specifier: workspace:* + version: link:../ui-core + '@osspkg/ui-transport': + specifier: workspace:* + version: link:../ui-transport + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + devDependencies: + '@tailwindcss/cli': + specifier: ^4.3.3 + version: 4.3.3 + '@types/react': + specifier: ^19.0.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.3.0(@types/react@19.3.0) + react: + specifier: ^19.0.0 + version: 19.3.0 + react-dom: + specifier: ^19.0.0 + version: 19.3.0(react@19.3.0) + typescript: + specifier: ^5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.2.0 + version: 3.2.7(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0) + + react-sdk/ui-transport: + dependencies: + '@osspkg/ui-core': + specifier: workspace:* + version: link:../ui-core + devDependencies: + typescript: + specifier: ^5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.2.0 + version: 3.2.7(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0) + +packages: + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@tailwindcss/cli@4.3.3': + resolution: {integrity: sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw==} + hasBin: true + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@24.13.6': + resolution: {integrity: sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript-eslint/eslint-plugin@8.70.1': + resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.70.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.70.1': + resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.70.1': + resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.70.1': + resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.70.1': + resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.70.1': + resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.70.1': + resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.70.1': + resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.70.1': + resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.70.1': + resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.25.1: + resolution: {integrity: sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==} + engines: {node: '>=10.13.0'} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-airbnb-base@15.0.0: + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + + eslint-config-airbnb@19.0.4: + resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} + engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@4.4.5: + resolution: {integrity: sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@4.6.2: + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.12.0: + resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.10: + resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.9.9: + resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.1.0: + resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.5: + resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.70.1: + resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.24: + resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@8.57.1(supports-color@7.2.0))': + dependencies: + eslint: 8.57.1(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4(supports-color@7.2.0)': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@7.2.0) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.3.7(supports-color@7.2.0)': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@7.2.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0(supports-color@7.2.0)': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@rollup/rollup-android-arm-eabi@4.63.4': + optional: true + + '@rollup/rollup-android-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-x64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.4': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@tailwindcss/cli@4.3.3': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + enhanced-resolve: 5.25.1 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.3.3 + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.25.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0) + + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json5@0.0.29': {} + + '@types/node@24.13.6': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/type-utils': 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.70.1 + eslint: 8.57.1(supports-color@7.2.0) + ignore: 7.0.10 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.70.1 + debug: 4.4.3(supports-color@7.2.0) + eslint: 8.57.1(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.70.1(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3) + '@typescript-eslint/types': 8.70.1 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.70.1': + dependencies: + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/visitor-keys': 8.70.1 + + '@typescript-eslint/tsconfig-utils@8.70.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 8.57.1(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.70.1': {} + + '@typescript-eslint/typescript-estree@8.70.1(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3) + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/visitor-keys': 8.70.1 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + eslint: 8.57.1(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.70.1': + dependencies: + '@typescript-eslint/types': 8.70.1 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.4.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.13.0: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@1.1.21: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + confusing-browser-globals@1.0.11: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.25.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.2 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.5 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.24 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.4.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escape-string-regexp@4.0.0: {} + + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1(supports-color@7.2.0)): + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 8.57.1(supports-color@7.2.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0) + object.assign: 4.1.7 + object.entries: 1.1.9 + semver: 6.3.1 + + eslint-config-airbnb@19.0.4(eslint-plugin-import@2.32.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1(supports-color@7.2.0)))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1(supports-color@7.2.0)))(eslint-plugin-react@7.37.5(eslint@8.57.1(supports-color@7.2.0)))(eslint@8.57.1(supports-color@7.2.0)): + dependencies: + eslint: 8.57.1(supports-color@7.2.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1(supports-color@7.2.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1(supports-color@7.2.0)) + eslint-plugin-react: 7.37.5(eslint@8.57.1(supports-color@7.2.0)) + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1(supports-color@7.2.0)) + object.assign: 4.1.7 + object.entries: 1.1.9 + + eslint-config-prettier@10.1.8(eslint@8.57.1(supports-color@7.2.0)): + dependencies: + eslint: 8.57.1(supports-color@7.2.0) + + eslint-import-context@0.1.9(unrs-resolver@1.12.2): + dependencies: + get-tsconfig: 4.14.3 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.12.2 + optional: true + + eslint-import-resolver-node@0.3.10(supports-color@7.2.0): + dependencies: + debug: 3.2.7(supports-color@7.2.0) + is-core-module: 2.17.0 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@4.4.5(eslint-plugin-import@2.32.0)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + eslint: 8.57.1(supports-color@7.2.0) + eslint-import-context: 0.1.9(unrs-resolver@1.12.2) + get-tsconfig: 4.14.3 + is-bun-module: 2.0.0 + stable-hash-x: 0.2.0 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + optional: true + + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@4.4.5)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + debug: 3.2.7(supports-color@7.2.0) + optionalDependencies: + '@typescript-eslint/parser': 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 8.57.1(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import@2.32.0)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.2.0 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7(supports-color@7.2.0) + doctrine: 2.1.0 + eslint: 8.57.1(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@4.4.5)(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0) + hasown: 2.0.4 + is-core-module: 2.17.0 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1(supports-color@7.2.0)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.2.0 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.13.0 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.57.1(supports-color@7.2.0) + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1(supports-color@7.2.0)): + dependencies: + eslint: 8.57.1(supports-color@7.2.0) + + eslint-plugin-react@7.37.5(eslint@8.57.1(supports-color@7.2.0)): + dependencies: + array-includes: 3.2.0 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.4.0 + eslint: 8.57.1(supports-color@7.2.0) + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.1.0 + string.prototype.repeat: 1.0.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@8.57.1(supports-color@7.2.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1(supports-color@7.2.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4(supports-color@7.2.0) + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0(supports-color@7.2.0) + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.4.0 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.3.2 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.4.4: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@17.12.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.10: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 + optional: true + + is-callable@1.2.7: {} + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.24 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.2.0 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + math-intrinsics@1.1.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.21 + + minimist@1.2.8: {} + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + napi-postinstall@0.3.4: + optional: true + + natural-compare@1.4.0: {} + + node-addon-api@7.1.1: {} + + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.9.9: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react-is@16.13.1: {} + + react@19.3.0: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: + optional: true + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.63.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.28.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stable-hash-x@0.2.0: + optional: true + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.1.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + text-table@0.2.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.5: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.1(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 8.57.1(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@7.18.2: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + optional: true + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@3.2.4(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@7.2.0) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.63.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.6 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + + vitest@3.2.7(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3(supports-color@7.2.0) + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0) + vite-node: 3.2.4(@types/node@24.13.6)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.6 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.24 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.24: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..bbf42e7 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - react-sdk/* + - example/react + +allowBuilds: + '@parcel/watcher': false + esbuild: true + unrs-resolver: false diff --git a/protocol/error-codes.json b/protocol/error-codes.json new file mode 100644 index 0000000..c0601d7 --- /dev/null +++ b/protocol/error-codes.json @@ -0,0 +1,16 @@ +{ + "PLUGIN_NOT_FOUND": "plugin was not found", + "PLUGIN_UNAVAILABLE": "plugin is unavailable", + "PLUGIN_PROTOCOL_ERROR": "plugin protocol error", + "VIEW_NOT_FOUND": "view was not found", + "INVALID_UI_SCHEMA": "ui schema is invalid", + "UNSUPPORTED_UI_VERSION": "ui protocol version is unsupported", + "COMPONENT_NOT_SUPPORTED": "component is not supported", + "ACTION_NOT_ALLOWED": "action is not allowed", + "SOURCE_FAILED": "source failed", + "ACTION_FAILED": "action failed", + "TIMEOUT": "operation timed out", + "CANCELLED": "operation was cancelled", + "FORBIDDEN": "operation is forbidden", + "UNAUTHORIZED": "operation is unauthorized" +} diff --git a/react-sdk/ui-core/package.json b/react-sdk/ui-core/package.json new file mode 100644 index 0000000..13517ee --- /dev/null +++ b/react-sdk/ui-core/package.json @@ -0,0 +1,25 @@ +{ + "name": "@osspkg/ui-core", + "version": "0.0.0", + "type": "module", + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run --config vitest.config.ts", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "devDependencies": { + "typescript": "^5.9.0", + "vitest": "^3.2.0" + } +} \ No newline at end of file diff --git a/react-sdk/ui-core/src/core.test.ts b/react-sdk/ui-core/src/core.test.ts new file mode 100644 index 0000000..26f4e37 --- /dev/null +++ b/react-sdk/ui-core/src/core.test.ts @@ -0,0 +1,294 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import type { UIValue, ViewSchema } from "./schema.js"; +import { resolvePath } from "./path.js"; +import { resolveValue, type ExpressionLimits } from "./expression.js"; +import { createStateRuntime } from "./runtime.js"; +import { createRequestTracker } from "./request.js"; +import { parseManifest, parseView, validateView, SchemaValidationError } from "./validate.js"; + +function fixture(name: string): unknown { + return JSON.parse( + readFileSync(new URL(`../../../fixtures/${name}`, import.meta.url), "utf8"), + ) as unknown; +} + +const scope = { + state: { status: "active", name: "Ada" }, + context: { workspace: { id: "w-1" } }, + sources: { users: { status: "success" as const, data: { items: [{ id: 1 }] } } }, + event: { value: "draft" }, + result: { id: 42 }, +}; + +describe("ui core", () => { + it("rejects prototype traversal", () => { + expect(() => resolvePath({}, "constructor.name")).toThrow("unsafe path"); + }); + + it("resolves references and expressions", () => { + expect(resolveValue({ $state: "name" }, scope)).toBe("Ada"); + expect(resolveValue({ $source: "users.items" }, scope)).toEqual([{ id: 1 }]); + expect(resolveValue({ $eq: [{ $state: "status" }, "active"] }, scope)).toBe(true); + }); + + it.each([ + ["idle", false], + ["loading", true], + ["success", false], + ["error", false], + ] as const)("maps source loading marker for %s", (status, expected) => { + const sourceScope = { + ...scope, + sources: { + users: { + status, + data: { items: [{ id: 1 }] }, + ...(status === "error" ? { error: { code: "FAILED", message: "failed" } } : {}), + }, + }, + }; + expect(resolveValue({ $source: "users.$loading" }, sourceScope)).toBe(expected); + }); + + it("maps source errors and data paths", () => { + const error = { code: "FAILED", message: "failed" }; + const sourceScope = { + ...scope, + sources: { users: { status: "error" as const, error, data: { items: [] } } }, + }; + expect(resolveValue({ $source: "users.$error" }, sourceScope)).toEqual(error); + expect(resolveValue({ $source: "users.items" }, sourceScope)).toEqual([]); + }); + + it("accepts only the latest request for a source", () => { + const tracker = createRequestTracker(); + const first = tracker.next("users"); + const second = tracker.next("users"); + + expect(tracker.isCurrent("users", first)).toBe(false); + expect(tracker.isCurrent("users", second)).toBe(true); + expect(tracker.isCurrent("orders", second)).toBe(false); + }); + + it("rejects invalid layouts", () => { + expect(() => + validateView({ + protocolVersion: "1.0", + id: "broken", + regions: { + "top-header": [], + "left-panel": [], + content: [{ id: "n", component: "card", layout: { row: 1, cols: 8, offset: 5 } }], + "right-panel": [], + bottom: [], + }, + }), + ).toThrow(SchemaValidationError); + }); + + it("parses manifests and views only after validation", () => { + const manifest = { + protocolVersion: "1.0", + plugin: { id: "users", title: "Users", version: "1.0" }, + views: [], + }; + expect(parseManifest(manifest).plugin.id).toBe("users"); + expect(() => parseManifest({ ...manifest, plugin: { ...manifest.plugin, title: "" } })).toThrow( + SchemaValidationError, + ); + expect(() => + parseManifest({ ...manifest, plugin: { ...manifest.plugin, version: "" } }), + ).toThrow(SchemaValidationError); + expect(() => + parseManifest({ + ...manifest, + views: [{ id: "users.list", title: "", schema: "ui://views/users.list" }], + }), + ).toThrow(SchemaValidationError); + expect( + parseView({ + protocolVersion: "1.0", + id: "users.list", + regions: { "top-header": [], "left-panel": [], content: [], "right-panel": [], bottom: [] }, + }).id, + ).toBe("users.list"); + expect(() => + parseView({ protocolVersion: "1.0", id: "broken", regions: { content: [] } }), + ).toThrow(SchemaValidationError); + expect(() => + parseView({ + protocolVersion: "1.0", + id: "broken", + regions: { + "top-header": [], + "left-panel": [], + content: [{ id: "node", component: "-button" }], + "right-panel": [], + bottom: [], + }, + }), + ).toThrow(SchemaValidationError); + }); + + it("rejects references to unknown sources", () => { + expect(() => + validateView({ + protocolVersion: "1.0", + id: "users.list", + regions: { + "top-header": [], + "left-panel": [], + content: [ + { id: "table", component: "table", props: { rows: { $source: "missing.items" } } }, + ], + "right-panel": [], + bottom: [], + }, + }), + ).toThrow(SchemaValidationError); + }); + + it("accepts and rejects the shared Go fixtures identically", () => { + expect(parseView(fixture("valid-view.json")).id).toBe("users.list"); + expect(() => parseView(fixture("invalid-layout.json"))).toThrow(SchemaValidationError); + }); + + it("enforces expression collection, object, and string limits", () => { + const limits: ExpressionLimits = { + maxDepth: 4, + maxArrayItems: 2, + maxObjectKeys: 1, + maxStringLength: 4, + }; + expect(() => resolveValue([1, 2, 3], scope, 0, limits)).toThrow( + "expression array limit exceeded", + ); + expect(() => resolveValue({ first: 1, second: 2 }, scope, 0, limits)).toThrow( + "expression object limit exceeded", + ); + expect(() => resolveValue("12345", scope, 0, limits)).toThrow( + "expression string limit exceeded", + ); + expect(() => resolveValue({ $concat: ["123", "45"] }, scope, 0, limits)).toThrow( + "expression string limit exceeded", + ); + }); + + it.each([ + ["$eq", { $eq: [1, 1] }, true], + ["$ne", { $ne: [1, 2] }, true], + ["$gt", { $gt: [2, 1] }, true], + ["$gte", { $gte: [2, 2] }, true], + ["$lt", { $lt: [1, 2] }, true], + ["$lte", { $lte: [2, 2] }, true], + ["$and", { $and: [true, true] }, true], + ["$or", { $or: [false, true] }, true], + ["$not", { $not: [false] }, true], + ["$in", { $in: ["active", ["draft", "active"]] }, true], + ["$exists", { $exists: [{ $state: "name" }] }, true], + ["$concat", { $concat: ["Ada", " Lovelace"] }, "Ada Lovelace"], + ["$coalesce", { $coalesce: [{ $state: "missing" }, "fallback"] }, "fallback"], + ["$if", { $if: [true, "yes", "no"] }, "yes"], + ] as const)("evaluates %s", (_operator, expression, expected) => { + expect(resolveValue(expression as UIValue, scope)).toBe(expected); + }); + + it("keeps schema, path, and expression decoders bounded for hostile inputs", () => { + let seed = 0x12345678; + const next = (): number => { + seed = (seed * 1664525 + 1013904223) >>> 0; + return seed; + }; + for (let index = 0; index < 256; index += 1) { + const token = next().toString(16); + expect(() => + resolvePath({ value: token }, index % 2 === 0 ? `value.${token}` : "__proto__.x"), + ).not.toThrow(TypeError); + expect(() => resolveValue({ $concat: [token, { $state: "missing" }] }, scope)).not.toThrow( + TypeError, + ); + expect(() => + parseView(index % 2 === 0 ? { protocolVersion: "bad", id: token } : { [token]: next() }), + ).not.toThrow(TypeError); + } + }); + + it.each([ + ["set", (runtime: ReturnType) => runtime.set("form.name", "Grace")], + [ + "merge", + (runtime: ReturnType) => runtime.merge("form", { valid: true }), + ], + ])("supports local state action: %s", (_name, apply) => { + const initial = { form: { name: "Ada", valid: false } }; + const runtime = createStateRuntime(initial); + + apply(runtime); + + expect(runtime.get()).not.toBe(initial); + expect(initial).toEqual({ form: { name: "Ada", valid: false } }); + expect(runtime.get()).toEqual( + _name === "set" + ? { form: { name: "Grace", valid: false } } + : { form: { name: "Ada", valid: true } }, + ); + }); + it("resolves source names containing dots", () => { + const dottedScope = { + ...scope, + sources: { "orders.list": { status: "success" as const, data: { items: [{ id: 1 }] } } }, + }; + expect(resolveValue({ $source: "orders.list.items" }, dottedScope)).toEqual([{ id: 1 }]); + }); + + it("validates the shared value bounds", () => { + const regions = { + "top-header": [], + "left-panel": [], + content: [{ id: "value", component: "text", props: { value: null } }], + "right-panel": [], + bottom: [], + }; + const view = (value: unknown): ViewSchema => ({ + protocolVersion: "1.0", + id: "limits", + regions: { + ...regions, + content: [{ id: "value", component: "text", props: { value: value as never } }], + }, + }); + const tooManyItems = Array.from({ length: 1001 }, () => null); + expect(() => validateView(view(tooManyItems))).toThrow(SchemaValidationError); + const tooManyKeys = Object.fromEntries( + Array.from({ length: 257 }, (_, index) => [`key-${index}`, index]), + ); + expect(() => validateView(view(tooManyKeys))).toThrow(SchemaValidationError); + expect(() => validateView(view(JSON.parse('{"__proto__":true}')))).toThrow( + SchemaValidationError, + ); + }); + + it("rejects empty event step lists and overlong paths", () => { + const base = { + protocolVersion: "1.0" as const, + id: "invalid", + regions: { "top-header": [], "left-panel": [], content: [], "right-panel": [], bottom: [] }, + }; + expect(() => + validateView({ + ...base, + regions: { + ...base.regions, + content: [{ id: "button", component: "button", events: { click: { steps: [] } } }], + }, + }), + ).toThrow(SchemaValidationError); + expect(() => + validateView({ + ...base, + actions: { save: { type: "set-state", path: "a".repeat(1025), value: true } }, + }), + ).toThrow(SchemaValidationError); + }); +}); diff --git a/react-sdk/ui-core/src/expression.ts b/react-sdk/ui-core/src/expression.ts new file mode 100644 index 0000000..2420c80 --- /dev/null +++ b/react-sdk/ui-core/src/expression.ts @@ -0,0 +1,150 @@ +import { resolvePath } from "./path.js"; +import type { ResolveScope, SourceRuntimeState, UIValue } from "./schema.js"; + +const operators = new Set([ + "$eq", + "$ne", + "$gt", + "$gte", + "$lt", + "$lte", + "$and", + "$or", + "$not", + "$in", + "$exists", + "$concat", + "$coalesce", + "$if", +]); + +export interface ExpressionLimits { + maxDepth: number; + maxArrayItems: number; + maxObjectKeys: number; + maxStringLength: number; +} + +export const defaultExpressionLimits: ExpressionLimits = { + maxDepth: 32, + maxArrayItems: 1000, + maxObjectKeys: 256, + maxStringLength: 16 * 1024, +}; + +export function resolveValue( + value: UIValue, + scope: ResolveScope, + depth = 0, + limits: ExpressionLimits = defaultExpressionLimits, +): unknown { + if (depth > limits.maxDepth) throw new Error("expression depth exceeded"); + if (typeof value === "string" && value.length > limits.maxStringLength) + throw new Error("expression string limit exceeded"); + if (Array.isArray(value)) { + if (value.length > limits.maxArrayItems) throw new Error("expression array limit exceeded"); + return value.map((item) => resolveValue(item, scope, depth + 1, limits)); + } + if (!value || typeof value !== "object") return value; + const entries = Object.entries(value); + if (entries.length > limits.maxObjectKeys) throw new Error("expression object limit exceeded"); + if (entries.length === 1) { + const [key, raw] = entries[0]!; + if (key === "$state" || key === "$context" || key === "$event" || key === "$result") { + const root = + key === "$state" + ? scope.state + : key === "$context" + ? scope.context + : key === "$event" + ? scope.event + : scope.result; + return resolvePath(root, String(raw)); + } + if (key === "$source") return resolveSource(String(raw), scope); + if (operators.has(key)) return evaluateExpression(key, raw, scope, depth + 1, limits); + } + return Object.fromEntries( + entries.map(([key, nested]) => [key, resolveValue(nested, scope, depth + 1, limits)]), + ); +} + +function resolveSource(path: string, scope: ResolveScope): unknown { + const match = sourceForPath(path, scope.sources); + if (!match) return undefined; + const source = scope.sources[match.name]; + if (!source) return undefined; + if (match.rest === "$loading") return source.status === "loading"; + if (match.rest === "$error") return source.error; + if (match.rest === "$data" || match.rest === "") return source.data; + if (match.rest === "$updatedAt") return source.updatedAt; + return resolvePath(source.data, match.rest); +} + +function sourceForPath( + path: string, + sources: Record, +): { name: string; rest: string } | undefined { + for (let name = path; name;) { + if (Object.prototype.hasOwnProperty.call(sources, name)) { + return { name, rest: path === name ? "" : path.slice(name.length + 1) }; + } + const index = name.lastIndexOf("."); + if (index < 0) break; + name = name.slice(0, index); + } + return undefined; +} + +function evaluateExpression( + operator: string, + raw: unknown, + scope: ResolveScope, + depth: number, + limits: ExpressionLimits, +): unknown { + if (!Array.isArray(raw)) throw new Error(`operator ${operator} requires an array`); + if (raw.length > limits.maxArrayItems) throw new Error("expression array limit exceeded"); + const args = raw.map((item) => resolveValue(item, scope, depth, limits)); + switch (operator) { + case "$eq": + return args[0] === args[1]; + case "$ne": + return args[0] !== args[1]; + case "$gt": + return comparable(args[0]) > comparable(args[1]); + case "$gte": + return comparable(args[0]) >= comparable(args[1]); + case "$lt": + return comparable(args[0]) < comparable(args[1]); + case "$lte": + return comparable(args[0]) <= comparable(args[1]); + case "$and": + return args.every(Boolean); + case "$or": + return args.some(Boolean); + case "$not": + return !args[0]; + case "$in": + return Array.isArray(args[1]) && args[1].includes(args[0]); + case "$exists": + return args[0] !== undefined && args[0] !== null; + case "$concat": { + const result = args.map((item) => String(item ?? "")).join(""); + if (result.length > limits.maxStringLength) + throw new Error("expression string limit exceeded"); + return result; + } + case "$coalesce": + return args.find((item) => item !== undefined && item !== null); + case "$if": + return args[0] ? args[1] : args[2]; + default: + throw new Error(`unsupported expression operator ${operator}`); + } +} + +function comparable(value: unknown): string | number { + if (typeof value === "string" || typeof value === "number") return value; + throw new Error("comparison requires a string or number"); +} diff --git a/react-sdk/ui-core/src/index.ts b/react-sdk/ui-core/src/index.ts new file mode 100644 index 0000000..dca0fb6 --- /dev/null +++ b/react-sdk/ui-core/src/index.ts @@ -0,0 +1,7 @@ +export * from "./expression.js"; +export * from "./path.js"; +export * from "./request.js"; +export * from "./rpc.js"; +export * from "./runtime.js"; +export * from "./schema.js"; +export * from "./validate.js"; diff --git a/react-sdk/ui-core/src/path.ts b/react-sdk/ui-core/src/path.ts new file mode 100644 index 0000000..3bb2119 --- /dev/null +++ b/react-sdk/ui-core/src/path.ts @@ -0,0 +1,47 @@ +const forbiddenSegments = new Set(["__proto__", "prototype", "constructor"]); + +export function resolvePath(value: unknown, path: string): unknown { + const segments = validatePath(path); + let current = value; + for (const segment of segments) { + if (current === null || current === undefined) return undefined; + if (typeof current !== "object" && typeof current !== "function") return undefined; + if (!Object.prototype.hasOwnProperty.call(current, segment)) return undefined; + current = (current as Record)[segment]; + } + return current; +} + +export function setPath(value: T, path: string, next: unknown): T { + const segments = validatePath(path); + if (segments.length === 0) throw new Error("path is required"); + const root = cloneValue(value) as Record; + let current = root; + for (let index = 0; index < segments.length - 1; index += 1) { + const segment = segments[index]!; + const existing = current[segment]; + current[segment] = existing && typeof existing === "object" ? cloneValue(existing) : {}; + current = current[segment] as Record; + } + current[segments.at(-1)!] = next; + return root as T; +} + +export function validatePath(path: string): string[] { + if (path.length === 0 || path.length > 1024) throw new Error("invalid path"); + const segments = path.split("."); + if (segments.some((segment) => segment.length === 0 || forbiddenSegments.has(segment))) { + throw new Error("unsafe path"); + } + return segments; +} + +function cloneValue(value: T): T { + if (Array.isArray(value)) return value.map(cloneValue) as T; + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, nested]) => [key, cloneValue(nested)]), + ) as T; + } + return value; +} diff --git a/react-sdk/ui-core/src/request.ts b/react-sdk/ui-core/src/request.ts new file mode 100644 index 0000000..4b4dfda --- /dev/null +++ b/react-sdk/ui-core/src/request.ts @@ -0,0 +1,16 @@ +export interface RequestTracker { + next(key: string): number; + isCurrent(key: string, requestId: number): boolean; +} + +export function createRequestTracker(): RequestTracker { + const current = new Map(); + return { + next: (key) => { + const requestId = (current.get(key) ?? 0) + 1; + current.set(key, requestId); + return requestId; + }, + isCurrent: (key, requestId) => current.get(key) === requestId, + }; +} diff --git a/react-sdk/ui-core/src/rpc.ts b/react-sdk/ui-core/src/rpc.ts new file mode 100644 index 0000000..9df9419 --- /dev/null +++ b/react-sdk/ui-core/src/rpc.ts @@ -0,0 +1,30 @@ +export const uiRPCMethods = { + getView: "ui.get", + dataCall: "data.call", + action: "ui.action", +} as const; + +export interface UIGetViewRequest { + plugin?: string; + view: string; + context?: unknown; +} + +export interface UIGetViewResponse { + schema: import("./schema.js").ViewSchema; +} + +export interface UIDataCallRequest { + plugin?: string; + view: string; + source: string; + operation: string; + input?: unknown; +} + +export interface UIActionRequest { + plugin?: string; + view: string; + action: string; + input?: unknown; +} diff --git a/react-sdk/ui-core/src/runtime.ts b/react-sdk/ui-core/src/runtime.ts new file mode 100644 index 0000000..ba19250 --- /dev/null +++ b/react-sdk/ui-core/src/runtime.ts @@ -0,0 +1,41 @@ +import { resolvePath, setPath } from "./path.js"; +import type { RuntimeError, SourceRuntimeState } from "./schema.js"; + +export interface StateRuntime { + get(): T; + set(path: string, value: unknown): void; + merge(path: string, value: Record): void; +} + +export function createStateRuntime(initial: T): StateRuntime { + let state = initial; + return { + get: () => state, + set: (path, value) => { + state = setPath(state, path, value); + }, + merge: (path, value) => { + const existing = resolvePath(state, path); + const base = existing && typeof existing === "object" ? existing : {}; + state = setPath(state, path, { ...base, ...value }); + }, + }; +} + +export function runtimeError( + code: string, + message: string, + details?: unknown, + retryable?: boolean, +): RuntimeError { + return { + code, + message, + ...(details === undefined ? {} : { details }), + ...(retryable === undefined ? {} : { retryable }), + }; +} + +export function emptySource(): SourceRuntimeState { + return { status: "idle" }; +} diff --git a/react-sdk/ui-core/src/schema.ts b/react-sdk/ui-core/src/schema.ts new file mode 100644 index 0000000..f002795 --- /dev/null +++ b/react-sdk/ui-core/src/schema.ts @@ -0,0 +1,161 @@ +export const protocolVersion = "1.0" as const; + +export type RegionName = "top-header" | "left-panel" | "content" | "right-panel" | "bottom"; +export type SourcePolicy = "manual" | "on-mount"; +export type ValueReference = "$state" | "$context" | "$source" | "$event" | "$result"; +export type ExpressionOperator = + | "$eq" + | "$ne" + | "$gt" + | "$gte" + | "$lt" + | "$lte" + | "$and" + | "$or" + | "$not" + | "$in" + | "$exists" + | "$concat" + | "$coalesce" + | "$if"; +export type ActionType = "tool" | "set-state" | "merge-state" | "refresh-source" | "invalidate"; +export type EffectType = + | "set-state" + | "merge-state" + | "invalidate" + | "refresh-source" + | "refresh-view" + | "patch-view" + | "navigate" + | "toast" + | "dialog" + | "close-dialog"; + +export interface Manifest { + protocolVersion: string; + plugin: { id: string; title: string; version: string; description?: string }; + views: ViewDescriptor[]; + requiredComponents?: string[]; +} + +export interface ViewDescriptor { + id: string; + title: string; + route?: string; + schema: string; +} + +export interface ViewSchema { + protocolVersion: string; + id: string; + revision?: string; + title?: string; + state?: Record; + sources?: Record; + actions?: Record; + regions: Record; +} + +export interface GridLayout { + row: number; + cols?: number; + offset?: number; +} + +export interface UINode { + id: string; + component: string; + layout?: GridLayout; + props?: Record; + events?: Record; + children?: UINode[]; + slots?: Record; + when?: UIValue; +} + +export interface DataSource { + type: "tool"; + tool: string; + input?: Record; + policy?: SourcePolicy; + cache?: { ttl?: number }; + refreshOn?: string[]; +} + +export interface EventHandler { + action?: string; + steps?: ActionStep[]; +} + +export interface ActionStep { + type: "action"; + action?: string; + input?: Record; +} + +export interface ActionDefinition { + type: ActionType; + tool?: string; + input?: Record; + path?: string; + value?: UIValue; + source?: string; + to?: string; + variant?: string; + message?: string; + effects?: Effect[]; +} + +export interface Effect { + type: EffectType; + path?: string; + source?: string; + value?: UIValue; + baseRevision?: string; + revision?: string; + to?: string; + variant?: string; + message?: string; +} + +export type JSONValue = + string | number | boolean | null | readonly JSONValue[] | { [key: string]: JSONValue }; +export type UIReference = + | { $state: string } + | { $context: string } + | { $source: string } + | { $event: string } + | { $result: string }; +export type UIExpression = { [operator in ExpressionOperator]?: readonly UIValue[] }; +export type UIValue = JSONValue | UIReference | UIExpression; + +export interface ResolveScope { + state: unknown; + context: unknown; + sources: Record; + event?: unknown; + result?: unknown; +} + +export interface SourceRuntimeState { + status: "idle" | "loading" | "success" | "error"; + data?: T; + error?: RuntimeError; + updatedAt?: number; + requestId?: string; +} + +export interface RuntimeError { + code: string; + message: string; + details?: unknown; + retryable?: boolean; +} + +export const regionNames: readonly RegionName[] = [ + "top-header", + "left-panel", + "content", + "right-panel", + "bottom", +]; diff --git a/react-sdk/ui-core/src/validate.ts b/react-sdk/ui-core/src/validate.ts new file mode 100644 index 0000000..0f05305 --- /dev/null +++ b/react-sdk/ui-core/src/validate.ts @@ -0,0 +1,517 @@ +import { regionNames, type Manifest, type RuntimeError, type ViewSchema } from "./schema.js"; + +export interface ValidationLimits { + maxBytes: number; + maxNodes: number; + maxDepth: number; + maxPropsPerNode: number; + maxActions: number; + maxSources: number; + maxStrings: number; + maxStringLength: number; + maxExpressionDepth: number; + maxArrayItems: number; + maxObjectKeys: number; + maxPathLength: number; +} + +export const defaultLimits: ValidationLimits = { + maxBytes: 1 << 20, + maxNodes: 1000, + maxDepth: 32, + maxPropsPerNode: 64, + maxActions: 128, + maxSources: 64, + maxStrings: 10000, + maxStringLength: 16 * 1024, + maxExpressionDepth: 32, + maxArrayItems: 1000, + maxObjectKeys: 256, + maxPathLength: 1024, +}; + +export class SchemaValidationError extends Error { + readonly runtimeError: RuntimeError; + + constructor(message: string, details?: unknown) { + super(message); + this.name = "SchemaValidationError"; + this.runtimeError = { + code: "INVALID_UI_SCHEMA", + message, + ...(details === undefined ? {} : { details }), + }; + } +} + +export function parseManifest(input: unknown, limits: Partial = {}): Manifest { + if (!isRecord(input)) throw new SchemaValidationError("manifest must be an object"); + const manifest = input as unknown as Manifest; + validateManifest(manifest, limits); + return manifest; +} + +export function parseView(input: unknown, limits: Partial = {}): ViewSchema { + if (!isRecord(input)) throw new SchemaValidationError("view must be an object"); + const view = input as unknown as ViewSchema; + validateView(view, limits); + return view; +} + +export function validateManifest( + manifest: Manifest, + customLimits: Partial = {}, +): void { + const limits = withDefaults(customLimits); + if (!isRecord(manifest) || manifest.protocolVersion !== "1.0") + throw new SchemaValidationError("unsupported manifest protocol version"); + if ( + !isRecord(manifest.plugin) || + !isSafeName(manifest.plugin.id) || + !isNonEmptyText(manifest.plugin.title) || + !isNonEmptyText(manifest.plugin.version) + ) { + throw new SchemaValidationError("manifest plugin metadata is required"); + } + if (!Array.isArray(manifest.views)) + throw new SchemaValidationError("manifest views must be an array"); + const ids = new Set(); + for (const view of manifest.views) { + if ( + !isRecord(view) || + !isSafeName(view.id) || + !isNonEmptyText(view.title) || + !isNonEmptyText(view.schema) || + ids.has(view.id) + ) { + throw new SchemaValidationError("manifest view is invalid"); + } + ids.add(view.id); + } + if ( + manifest.requiredComponents !== undefined && + (!Array.isArray(manifest.requiredComponents) || + manifest.requiredComponents.some((name) => !isComponentName(name))) + ) { + throw new SchemaValidationError("manifest required components are invalid"); + } + validateSerializedLimits(manifest, limits); +} + +export function validateView(view: ViewSchema, customLimits: Partial = {}): void { + const limits = withDefaults(customLimits); + if (!isRecord(view) || view.protocolVersion !== "1.0") + throw new SchemaValidationError("unsupported view protocol version"); + if (!isSafeName(view.id)) throw new SchemaValidationError("view id is required"); + if (view.title !== undefined && !isText(view.title)) + throw new SchemaValidationError("view title is invalid"); + if (view.sources !== undefined && !isRecord(view.sources)) + throw new SchemaValidationError("sources must be an object"); + if (view.actions !== undefined && !isRecord(view.actions)) + throw new SchemaValidationError("actions must be an object"); + if (!isRecord(view.regions)) throw new SchemaValidationError("regions are required"); + validateSerializedLimits(view, limits); + + const actions = view.actions ?? {}; + const sources = view.sources ?? {}; + if ( + Object.keys(actions).length > limits.maxActions || + Object.keys(sources).length > limits.maxSources + ) { + throw new SchemaValidationError("source or action limit exceeded"); + } + for (const [name, source] of Object.entries(sources)) validateSource(name, source, view, limits); + for (const [name, action] of Object.entries(actions)) validateAction(name, action, view, limits); + + let nodes = 0; + const nodeIDs = new Set(); + for (const region of regionNames) { + const nodesInRegion = view.regions[region]; + if (!Array.isArray(nodesInRegion)) + throw new SchemaValidationError(`region ${region} must be an array`); + for (const node of nodesInRegion) + nodes = validateNode(node, region, 1, view, limits, nodeIDs, nodes); + } +} + +function validateSource( + name: string, + source: unknown, + view: ViewSchema, + limits: ValidationLimits, +): void { + if (!isSafeName(name) || !isRecord(source) || source.type !== "tool" || !isSafeName(source.tool)) + throw new SchemaValidationError(`source ${name} is invalid`); + if (source.policy !== undefined && source.policy !== "manual" && source.policy !== "on-mount") + throw new SchemaValidationError(`source ${name} policy is invalid`); + if ( + source.cache !== undefined && + (!isRecord(source.cache) || + (source.cache.ttl !== undefined && + (typeof source.cache.ttl !== "number" || + !Number.isInteger(source.cache.ttl) || + source.cache.ttl < 0))) + ) + throw new SchemaValidationError(`source ${name} cache is invalid`); + validateValues(source.input, view, limits); + if ( + source.refreshOn !== undefined && + (!Array.isArray(source.refreshOn) || source.refreshOn.some((action) => !view.actions?.[action])) + ) + throw new SchemaValidationError(`source ${name} refresh action is invalid`); +} + +function validateAction( + name: string, + action: unknown, + view: ViewSchema, + limits: ValidationLimits, +): void { + if ( + !isSafeName(name) || + !isRecord(action) || + typeof action.type !== "string" || + !["tool", "set-state", "merge-state", "refresh-source", "invalidate"].includes(action.type) + ) + throw new SchemaValidationError(`action ${name} is invalid`); + if (action.type === "tool" && !isSafeName(action.tool)) + throw new SchemaValidationError(`tool action ${name} requires a tool`); + if ( + (action.type === "set-state" || action.type === "merge-state") && + !isSafePath(action.path, limits.maxPathLength) + ) + throw new SchemaValidationError(`action ${name} requires a safe path`); + if ( + action.path !== undefined && + action.path !== "" && + !isSafePath(action.path, limits.maxPathLength) + ) + throw new SchemaValidationError(`action ${name} path is invalid`); + if ( + (action.type === "refresh-source" || action.type === "invalidate") && + !view.sources?.[action.source as string] + ) + throw new SchemaValidationError(`action ${name} source is invalid`); + validateValues(action.input, view, limits); + if (action.value !== undefined) validateValue(action.value, view, limits, 0); + if (action.effects !== undefined) { + if (!Array.isArray(action.effects)) + throw new SchemaValidationError(`action ${name} effects are invalid`); + for (const effect of action.effects) validateEffect(effect, view, limits); + } +} + +function validateEffect(effect: unknown, view: ViewSchema, limits: ValidationLimits): void { + if ( + !isRecord(effect) || + typeof effect.type !== "string" || + ![ + "set-state", + "merge-state", + "invalidate", + "refresh-source", + "refresh-view", + "patch-view", + "navigate", + "toast", + "dialog", + "close-dialog", + ].includes(effect.type) + ) + throw new SchemaValidationError("effect type is invalid"); + if ( + (effect.type === "set-state" || effect.type === "merge-state") && + !isSafePath(effect.path, limits.maxPathLength) + ) + throw new SchemaValidationError("state effect path is invalid"); + if ( + effect.path !== undefined && + effect.path !== "" && + !isSafePath(effect.path, limits.maxPathLength) + ) + throw new SchemaValidationError("effect path is invalid"); + if ( + (effect.type === "invalidate" || effect.type === "refresh-source") && + !view.sources?.[effect.source as string] + ) + throw new SchemaValidationError("effect source is invalid"); + if (effect.type === "toast" && !isText(effect.message)) + throw new SchemaValidationError("toast effect message is required"); + if (effect.type === "navigate" && !isText(effect.to)) + throw new SchemaValidationError("navigate effect target is required"); + if (effect.value !== undefined) validateValue(effect.value, view, limits, 0); +} + +function validateNode( + node: unknown, + region: string, + depth: number, + view: ViewSchema, + limits: ValidationLimits, + ids: Set, + count: number, +): number { + if ( + !isRecord(node) || + !isSafeName(node.id) || + !isComponentName(node.component) || + ids.has(node.id) + ) + throw new SchemaValidationError("node id or component is invalid"); + count += 1; + if (count > limits.maxNodes || depth > limits.maxDepth) + throw new SchemaValidationError("schema complexity limit exceeded"); + ids.add(node.id); + if (node.layout !== undefined) validateLayout(node.layout, region); + if ( + node.props !== undefined && + (!isRecord(node.props) || Object.keys(node.props).length > limits.maxPropsPerNode) + ) + throw new SchemaValidationError("node props are invalid"); + validateValues(node.props, view, limits); + if (node.events !== undefined) { + if (!isRecord(node.events)) throw new SchemaValidationError("node events are invalid"); + for (const [event, handler] of Object.entries(node.events)) { + if ( + !isSafeName(event) || + !isRecord(handler) || + (!handler.action && (!Array.isArray(handler.steps) || handler.steps.length === 0)) + ) + throw new SchemaValidationError("event handler is invalid"); + if ( + handler.action !== undefined && + (!isSafeName(handler.action) || !view.actions?.[handler.action]) + ) + throw new SchemaValidationError("event action is not defined"); + if (handler.steps !== undefined && !Array.isArray(handler.steps)) + throw new SchemaValidationError("event steps are invalid"); + if (Array.isArray(handler.steps)) + for (const step of handler.steps) { + if ( + !isRecord(step) || + step.type !== "action" || + !isSafeName(step.action) || + !view.actions?.[step.action] + ) + throw new SchemaValidationError("event step is invalid"); + validateValues(step.input, view, limits); + } + } + } + if (node.when !== undefined) validateValue(node.when, view, limits, 0); + if (node.children !== undefined) { + if (!Array.isArray(node.children)) throw new SchemaValidationError("children must be an array"); + for (const child of node.children) + count = validateNode(child, region, depth + 1, view, limits, ids, count); + } + if (node.slots !== undefined) { + if (!isRecord(node.slots)) throw new SchemaValidationError("slots must be an object"); + for (const children of Object.values(node.slots)) { + if (!Array.isArray(children)) + throw new SchemaValidationError("slot children must be an array"); + for (const child of children) + count = validateNode(child, region, depth + 1, view, limits, ids, count); + } + } + return count; +} + +function validateLayout(layout: unknown, region: string): void { + if ( + !isRecord(layout) || + typeof layout.row !== "number" || + !Number.isInteger(layout.row) || + layout.row < 1 + ) + throw new SchemaValidationError("layout row is invalid"); + if (region === "left-panel" || region === "right-panel") { + if (layout.cols !== undefined || layout.offset !== undefined) + throw new SchemaValidationError("side panel layout is invalid"); + return; + } + const cols = layout.cols ?? 0; + const offset = layout.offset ?? 0; + if ( + typeof cols !== "number" || + typeof offset !== "number" || + !Number.isInteger(cols) || + cols < 1 || + cols > 12 || + !Number.isInteger(offset) || + offset < 0 || + offset > 11 || + cols + offset > 12 + ) + throw new SchemaValidationError("grid layout is invalid"); +} + +function validateValues(values: unknown, view: ViewSchema, limits: ValidationLimits): void { + if (values === undefined) return; + if (!isRecord(values)) throw new SchemaValidationError("values must be an object"); + for (const value of Object.values(values)) validateValue(value, view, limits, 0); +} + +function validateValue( + value: unknown, + view: ViewSchema, + limits: ValidationLimits, + depth: number, +): void { + if (depth > limits.maxExpressionDepth) + throw new SchemaValidationError("expression depth exceeded"); + if (value === null || typeof value === "boolean" || typeof value === "number") return; + if (typeof value === "string") { + if (value.length > limits.maxStringLength) throw new SchemaValidationError("value is too long"); + return; + } + if (Array.isArray(value)) { + if (value.length > limits.maxArrayItems) throw new SchemaValidationError("array is too large"); + for (const item of value) validateValue(item, view, limits, depth + 1); + return; + } + if (!isRecord(value)) throw new SchemaValidationError("value is not JSON-compatible"); + for (const key of Object.keys(value)) + if (["__proto__", "prototype", "constructor"].includes(key)) + throw new SchemaValidationError("unsafe value key"); + const entries = Object.entries(value); + if (entries.length > limits.maxObjectKeys) + throw new SchemaValidationError("expression object is too large"); + if (entries.length === 1) { + const [key, raw] = entries[0]!; + if (["$state", "$context", "$event", "$result"].includes(key)) { + if (!isSafePath(raw, limits.maxPathLength)) + throw new SchemaValidationError("reference path is unsafe"); + return; + } + if (key === "$source") { + if (!isSafePath(raw, limits.maxPathLength)) + throw new SchemaValidationError("source path is unsafe"); + const sourceName = sourceForPath(raw, view.sources); + if (!sourceName) throw new SchemaValidationError("source reference is not defined"); + return; + } + const arity = expressionArity[key]; + if (arity) { + if (!Array.isArray(raw) || raw.length < arity[0] || (arity[1] >= 0 && raw.length > arity[1])) + throw new SchemaValidationError(`operator ${key} has invalid arity`); + for (const item of raw) validateValue(item, view, limits, depth + 1); + return; + } + if (key.startsWith("$")) + throw new SchemaValidationError(`unsupported expression operator ${key}`); + } + for (const nested of Object.values(value)) validateValue(nested, view, limits, depth + 1); +} + +const expressionArity: Record = { + $eq: [2, 2], + $ne: [2, 2], + $gt: [2, 2], + $gte: [2, 2], + $lt: [2, 2], + $lte: [2, 2], + $and: [1, -1], + $or: [1, -1], + $not: [1, 1], + $in: [2, 2], + $exists: [1, 1], + $concat: [0, -1], + $coalesce: [1, -1], + $if: [3, 3], +}; + +function validateSerializedLimits(value: unknown, limits: ValidationLimits): void { + let encoded: string; + try { + encoded = JSON.stringify(value); + } catch { + throw new SchemaValidationError("schema is not JSON serializable"); + } + if (new TextEncoder().encode(encoded).length > limits.maxBytes) + throw new SchemaValidationError("schema byte limit exceeded"); + let strings = 0; + walkSerialized(value, 0, limits, () => { + strings += 1; + if (strings > limits.maxStrings) throw new SchemaValidationError("string count limit exceeded"); + }); +} + +function walkSerialized( + value: unknown, + depth: number, + limits: ValidationLimits, + onString: () => void, +): void { + if (depth > limits.maxDepth) throw new SchemaValidationError("schema depth limit exceeded"); + if (typeof value === "string") { + if (value.length > limits.maxStringLength) + throw new SchemaValidationError("string length limit exceeded"); + onString(); + } else if (Array.isArray(value)) { + if (value.length > limits.maxArrayItems) + throw new SchemaValidationError("array item limit exceeded"); + for (const item of value) walkSerialized(item, depth + 1, limits, onString); + } else if (isRecord(value)) { + if (Object.keys(value).length > limits.maxObjectKeys) + throw new SchemaValidationError("object key limit exceeded"); + for (const [key, nested] of Object.entries(value)) { + if (["__proto__", "prototype", "constructor"].includes(key)) + throw new SchemaValidationError("unsafe value key"); + walkSerialized(key, depth + 1, limits, onString); + walkSerialized(nested, depth + 1, limits, onString); + } + } +} + +function withDefaults(custom: Partial): ValidationLimits { + return { ...defaultLimits, ...custom }; +} + +function isRecord(value: T): value is T; +function isRecord(value: unknown): value is Record; +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} + +function isText(value: unknown): value is string { + return typeof value === "string"; +} + +function isNonEmptyText(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} + +function isSafeName(value: unknown): value is string { + return typeof value === "string" && value.length > 0 && !/[\r\n]/.test(value); +} + +function isSafePath(value: unknown, maxLength: number): value is string { + return ( + typeof value === "string" && + value.length > 0 && + value.length <= maxLength && + value + .split(".") + .every( + (segment) => + segment.length > 0 && !["__proto__", "prototype", "constructor"].includes(segment), + ) + ); +} + +function sourceForPath( + path: string, + sources: Record | undefined, +): string | undefined { + for (let name = path; name;) { + if (sources && Object.prototype.hasOwnProperty.call(sources, name)) return name; + const index = name.lastIndexOf("."); + if (index < 0) break; + name = name.slice(0, index); + } + return undefined; +} + +function isComponentName(value: unknown): value is string { + return typeof value === "string" && /^[a-z][a-z0-9-]*$/.test(value); +} diff --git a/react-sdk/ui-core/tsconfig.json b/react-sdk/ui-core/tsconfig.json new file mode 100644 index 0000000..0b1110e --- /dev/null +++ b/react-sdk/ui-core/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": {"rootDir": "src", "outDir": "dist"}, + "include": ["src"] +} diff --git a/react-sdk/ui-core/vitest.config.ts b/react-sdk/ui-core/vitest.config.ts new file mode 100644 index 0000000..c1433e6 --- /dev/null +++ b/react-sdk/ui-core/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/react-sdk/ui-react/package.json b/react-sdk/ui-react/package.json new file mode 100644 index 0000000..d16d389 --- /dev/null +++ b/react-sdk/ui-react/package.json @@ -0,0 +1,43 @@ +{ + "name": "@osspkg/ui-react", + "version": "0.0.0", + "type": "module", + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./styles.css": { + "types": "./dist/styles.css.d.ts", + "default": "./dist/styles.css" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json && tailwindcss -i src/styles.css -o dist/styles.css && node scripts/copy-styles-types.mjs", + "test": "vitest run --config vitest.config.ts", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@osspkg/ui-core": "workspace:*", + "@osspkg/ui-transport": "workspace:*", + "tailwindcss": "^4.3.3" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "devDependencies": { + "@tailwindcss/cli": "^4.3.3", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.9.0", + "vitest": "^3.2.0" + } +} \ No newline at end of file diff --git a/react-sdk/ui-react/scripts/copy-styles-types.mjs b/react-sdk/ui-react/scripts/copy-styles-types.mjs new file mode 100644 index 0000000..84632fe --- /dev/null +++ b/react-sdk/ui-react/scripts/copy-styles-types.mjs @@ -0,0 +1,6 @@ +import { copyFile } from "node:fs/promises"; + +await copyFile( + new URL("../src/styles.css.d.ts", import.meta.url), + new URL("../dist/styles.css.d.ts", import.meta.url), +); diff --git a/react-sdk/ui-react/src/defaults.test.tsx b/react-sdk/ui-react/src/defaults.test.tsx new file mode 100644 index 0000000..9822884 --- /dev/null +++ b/react-sdk/ui-react/src/defaults.test.tsx @@ -0,0 +1,109 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import type { ViewSchema } from "@osspkg/ui-core"; +import { createComponentRegistry } from "./registry.js"; +import { htmlTagNames, registerDefaultHTMLComponents } from "./html.js"; +import { registerDefaultShadcnComponents, shadcnComponentNames } from "./shadcn.js"; +import { UIProvider } from "./runtime.js"; +import { ViewRenderer } from "./renderer.js"; + +describe("default component registrations", () => { + it("registers every HTML tag and shadcn component without overwriting a host component", () => { + const registry = createComponentRegistry(); + const hostButton = () => null; + registry.register("button", { component: hostButton }); + + registerDefaultHTMLComponents(registry); + registerDefaultShadcnComponents(registry); + + for (const name of htmlTagNames) expect(registry.has(name)).toBe(true); + for (const name of shadcnComponentNames) expect(registry.has(name)).toBe(true); + expect(registry.get("button")?.component).toBe(hostButton); + expect(registry.get("a")?.events).toContain("click"); + expect(registry.get("data-table")?.allowedProps).toContain("rows"); + }); + + it("renders safe native elements and the baseline data table", () => { + const registry = createComponentRegistry(); + registerDefaultHTMLComponents(registry); + registerDefaultShadcnComponents(registry); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "defaults", + regions: { + "top-header": [], + "left-panel": [], + "right-panel": [], + bottom: [], + content: [ + { id: "link", component: "a", props: { href: "https://example.com", text: "Example" } }, + { + id: "profile-form", + component: "form", + props: { + method: "post", + enctype: "multipart/form-data", + novalidate: true, + autocomplete: "off", + }, + }, + { id: "email-label", component: "label", props: { for: "email", text: "Email" } }, + { + id: "email", + component: "input", + props: { + id: "email", + type: "email", + autocomplete: "email", + inputmode: "email", + enterkeyhint: "next", + maxlength: 255, + spellcheck: false, + }, + }, + { id: "notes", component: "textarea", props: { dirname: "notes.dir", wrap: "soft" } }, + { id: "submit", component: "button", props: { type: "submit", text: "Save" } }, + { id: "users", component: "data-table", props: { rows: [{ name: "Ada" }] } }, + ], + }, + }; + + const markup = renderToStaticMarkup( + + + , + ); + expect(markup).toContain('href="https://example.com"'); + expect(markup).toContain('method="post"'); + expect(markup).toContain('encType="multipart/form-data"'); + expect(markup).toContain('for="email"'); + expect(markup).toContain('autoComplete="email"'); + expect(markup).toContain('inputMode="email"'); + expect(markup).toContain('enterKeyHint="next"'); + expect(markup).toContain('maxLength="255"'); + expect(markup).toContain('dirname="notes.dir"'); + expect(markup).toContain('wrap="soft"'); + expect(markup).toContain('type="submit"'); + expect(markup).toContain("Ada"); + + const unsafeURL = ["java", "script:alert(1)"].join(""); + schema.regions.content[0]!.props = { href: unsafeURL, text: "unsafe" }; + expect(() => + renderToStaticMarkup( + + + , + ), + ).toThrow("href must be a safe URL"); + + schema.regions.content[0]!.component = "form"; + schema.regions.content[0]!.props = { action: "https://attacker.example" }; + expect(() => + renderToStaticMarkup( + + + , + ), + ).toThrow("prop action is not allowed"); + }); +}); diff --git a/react-sdk/ui-react/src/defaults.tsx b/react-sdk/ui-react/src/defaults.tsx new file mode 100644 index 0000000..0d8f417 --- /dev/null +++ b/react-sdk/ui-react/src/defaults.tsx @@ -0,0 +1,338 @@ +import { createElement, type ComponentType, type ReactNode } from "react"; +import type { ComponentDefinition } from "./registry.js"; + +type Props = Record; + +const globalProps = [ + "id", + "title", + "hidden", + "dir", + "lang", + "role", + "tabIndex", + "contentEditable", + "draggable", + "translate", + "aria-label", + "aria-describedby", + "aria-labelledby", + "aria-live", + "aria-current", + "aria-expanded", + "aria-controls", + "aria-pressed", + "aria-description", + "aria-details", + "aria-errormessage", + "aria-invalid", + "aria-placeholder", + "aria-required", +] as const; + +const commonProps = [ + "text", + "type", + "value", + "defaultValue", + "name", + "placeholder", + "disabled", + "readOnly", + "required", + "autoComplete", + "autoFocus", + "min", + "max", + "minLength", + "maxLength", + "step", + "pattern", + "multiple", + "checked", + "selected", + "rows", + "cols", + "width", + "height", + "loading", + "error", +] as const; +const linkProps = ["href", "target", "rel", "download", "src", "alt", "poster"] as const; +const formProps = [ + "accept", + "acceptCharset", + "capture", + "dirname", + "encType", + "enterKeyHint", + "form", + "formEncType", + "formMethod", + "formNoValidate", + "formTarget", + "htmlFor", + "inputMode", + "list", + "method", + "noValidate", + "size", + "spellCheck", + "wrap", +] as const; +const htmlPropAliases = { + "accept-charset": "acceptCharset", + autocomplete: "autoComplete", + autofocus: "autoFocus", + contenteditable: "contentEditable", + dirname: "dirname", + enctype: "encType", + enterkeyhint: "enterKeyHint", + for: "htmlFor", + formenctype: "formEncType", + formmethod: "formMethod", + formnovalidate: "formNoValidate", + formtarget: "formTarget", + inputmode: "inputMode", + maxlength: "maxLength", + minlength: "minLength", + novalidate: "noValidate", + readonly: "readOnly", + spellcheck: "spellCheck", + tabindex: "tabIndex", +} as const; +const htmlAliases = Object.keys(htmlPropAliases); +const events = ["click", "change", "input", "submit", "focus", "blur"] as const; +const htmlProps = [ + ...globalProps, + ...commonProps, + ...linkProps, + ...formProps, + ...htmlAliases, +] as const; +const shadcnProps = [ + ...htmlProps, + "variant", + "size", + "orientation", + "side", + "align", + "open", + "defaultOpen", + "modal", + "label", + "description", + "status", +] as const; +const domProps = new Set([...globalProps, ...commonProps, ...linkProps, ...formProps]); +const eventProps = new Set(["onClick", "onChange", "onInput", "onSubmit", "onFocus", "onBlur"]); +const urlProps = new Set(["href", "src", "poster"]); + +function safeURL(value: string): boolean { + if (value.startsWith("/") || value.startsWith("#") || value.startsWith("?")) return true; + try { + return ["http:", "https:", "mailto:", "tel:"].includes(new URL(value).protocol); + } catch { + return false; + } +} + +function sanitizeProps(props: Props): Props { + const result: Props = {}; + for (const [name, value] of Object.entries(props)) { + if (value === undefined) continue; + const normalizedName: string = htmlPropAliases[name as keyof typeof htmlPropAliases] ?? name; + if (normalizedName in result) throw new Error(`duplicate prop ${normalizedName}`); + if (normalizedName === "rows") { + if (!Array.isArray(value)) throw new Error("rows must be an array"); + result[normalizedName] = value; + continue; + } + if (urlProps.has(normalizedName)) { + if (typeof value !== "string" || !safeURL(value)) + throw new Error(`${normalizedName} must be a safe URL`); + result[normalizedName] = value; + continue; + } + if (["string", "number", "boolean"].includes(typeof value)) { + result[normalizedName] = value; + continue; + } + throw new Error(`${normalizedName} must be a string, number, or boolean`); + } + return result; +} + +function DOMElement({ + tag, + props, + className, + children, +}: { + tag: string; + props: Props; + className: string; + children?: ReactNode; +}): ReactNode { + const elementProps: Props = {}; + for (const [name, value] of Object.entries(props)) { + if ( + eventProps.has(name) || + (domProps.has(name) && + name !== "text" && + name !== "rows" && + name !== "loading" && + name !== "error") + ) + elementProps[name] = value; + } + elementProps.className = className; + const content = props.text ?? children; + if (tag === "form" && typeof props.onSubmit === "function") { + elementProps.onSubmit = (event: { preventDefault(): void }) => { + event.preventDefault(); + void (props.onSubmit as (event: unknown) => Promise)(event); + }; + } + return createElement(tag, elementProps, content as ReactNode); +} + +function htmlClass(tag: string): string { + if (tag === "a") return "text-ui-primary underline-offset-4 hover:underline"; + if (tag === "button") + return "inline-flex min-h-11 items-center justify-center rounded-md bg-ui-primary px-3 py-2 text-sm font-medium text-ui-primary-foreground hover:bg-ui-primary/90 disabled:pointer-events-none disabled:opacity-50"; + if (["input", "textarea", "select"].includes(tag)) + return "flex min-h-11 w-full rounded-md border border-ui-border bg-ui-background px-3 py-2 text-sm text-ui-foreground shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ui-ring disabled:cursor-not-allowed disabled:opacity-50"; + if (tag === "table") return "w-full caption-bottom text-sm"; + if (["th", "td"].includes(tag)) + return "border-b border-ui-border px-4 py-3 text-left align-middle"; + if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) + return "font-semibold tracking-tight text-ui-foreground"; + if (tag === "img") return "h-auto max-w-full rounded-md"; + return "text-ui-foreground"; +} + +export function createDefaultHTMLDefinition(tag: string): ComponentDefinition { + return { + component: ((props: Props) => ( + + {props.children as ReactNode} + + )) as ComponentType, + allowedProps: htmlProps, + propsSchema: sanitizeProps, + events, + }; +} + +const shadcnElement: Record = { + button: "button", + checkbox: "input", + input: "input", + "native-select": "select", + select: "select", + slider: "input", + switch: "button", + textarea: "textarea", + table: "table", + "data-table": "table", + progress: "progress", + separator: "hr", + label: "label", + kbd: "kbd", + typography: "p", + spinner: "output", + accordion: "details", + collapsible: "details", + dialog: "dialog", + "alert-dialog": "dialog", +}; + +function shadcnClass(name: string): string { + if (name === "button") + return "inline-flex min-h-11 items-center justify-center rounded-md bg-ui-primary px-3 py-2 text-sm font-medium text-ui-primary-foreground hover:bg-ui-primary/90 disabled:pointer-events-none disabled:opacity-50"; + if (["input", "textarea", "select", "native-select"].includes(name)) + return "flex min-h-11 w-full rounded-md border border-ui-border bg-ui-background px-3 py-2 text-sm text-ui-foreground shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ui-ring disabled:cursor-not-allowed disabled:opacity-50"; + if (name === "card") + return "rounded-xl border border-ui-border bg-ui-card p-6 text-ui-card-foreground shadow-sm"; + if (["badge", "marker"].includes(name)) + return "inline-flex items-center rounded-md bg-ui-muted px-2 py-1 text-xs font-medium text-ui-muted-foreground"; + if (["alert", "empty", "message", "toast"].includes(name)) + return "rounded-lg border border-ui-border bg-ui-card p-4 text-ui-card-foreground"; + if (name === "separator") return "my-4 border-ui-border"; + if (name === "spinner") + return "inline-flex size-4 animate-spin rounded-full border-2 border-ui-muted border-t-ui-primary"; + if (name === "skeleton") return "animate-pulse rounded-md bg-ui-muted"; + if (name === "table") return "w-full caption-bottom text-sm"; + return "rounded-md border border-ui-border bg-ui-card p-4 text-ui-card-foreground"; +} + +function DataTable({ rows, loading, error, "aria-label": ariaLabel }: Props): ReactNode { + if (loading) return Loading…; + if (error) + return ( + + {String(error)} + + ); + const items: unknown[] = Array.isArray(rows) ? rows : []; + const firstRow = items[0]; + const columns = + firstRow && typeof firstRow === "object" && !Array.isArray(firstRow) + ? Object.keys(firstRow as Record) + : []; + return ( + + {columns.length > 0 && ( + + + {columns.map((column) => ( + + ))} + + + )} + + {items.map((row, index) => { + const record = row && typeof row === "object" ? (row as Record) : {}; + return ( + + {columns.map((column) => ( + + ))} + + ); + })} + +
+ {column} +
+ {String(record[column] ?? "")} +
+ ); +} + +export function createDefaultShadcnDefinition(name: string): ComponentDefinition { + if (name === "data-table") + return { + component: DataTable as ComponentType, + allowedProps: shadcnProps, + propsSchema: sanitizeProps, + events, + children: false, + }; + const tag = shadcnElement[name] ?? "div"; + return { + component: ((props: Props) => ( + + {props.children as ReactNode} + + )) as ComponentType, + allowedProps: shadcnProps, + propsSchema: sanitizeProps, + events, + }; +} diff --git a/react-sdk/ui-react/src/demo.test.tsx b/react-sdk/ui-react/src/demo.test.tsx new file mode 100644 index 0000000..ce83850 --- /dev/null +++ b/react-sdk/ui-react/src/demo.test.tsx @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { createUsersDemoRegistry } from "./demo.js"; + +describe("users demo registry", () => { + it("contains host-owned CRUD primitives", () => { + const registry = createUsersDemoRegistry(); + expect(registry.has("table")).toBe(true); + expect(registry.has("form")).toBe(true); + expect(registry.has("text-field")).toBe(true); + expect(registry.get("table")?.allowedProps).toEqual(["rows", "loading", "error"]); + }); +}); diff --git a/react-sdk/ui-react/src/demo.tsx b/react-sdk/ui-react/src/demo.tsx new file mode 100644 index 0000000..1a794f0 --- /dev/null +++ b/react-sdk/ui-react/src/demo.tsx @@ -0,0 +1,78 @@ +import { useMemo, type ReactElement, type ReactNode } from "react"; +import type { RPCTransport } from "@osspkg/ui-transport"; +import { createComponentRegistry, type ComponentRegistry } from "./registry.js"; +import { PluginView } from "./renderer.js"; +import { UIProvider } from "./runtime.js"; + +export interface UsersDemoProps { + transport?: RPCTransport; +} + +export function createUsersDemoRegistry(): ComponentRegistry { + const registry = createComponentRegistry(); + registry.register("heading", { + component: ({ text }: { text?: unknown }) =>

{String(text ?? "")}

, + allowedProps: ["text"], + }); + registry.register("text", { + component: ({ text }: { text?: unknown }) =>

{String(text ?? "")}

, + allowedProps: ["text"], + }); + registry.register("button", { + component: ({ text, children }: { text?: unknown; children?: ReactNode }) => ( + + ), + allowedProps: ["text"], + events: ["click"], + }); + registry.register("text-field", { + component: ({ value }: { value?: unknown }) => , + allowedProps: ["value"], + }); + registry.register("form", { + component: ({ children }: { children?: ReactNode }) =>
{children}
, + }); + registry.register("table", { + component: UsersTable, + allowedProps: ["rows", "loading", "error"], + children: false, + }); + return registry; +} + +function UsersTable({ + rows, + loading, + error, +}: { + rows?: unknown; + loading?: unknown; + error?: unknown; +}): ReactElement { + if (loading) return
Loading users…
; + if (error) return
Unable to load users
; + const items = Array.isArray(rows) ? rows : []; + return ( + + + {items.map((row) => ( + ).name ?? "user")}> + + + ))} + +
{String((row as Record).name ?? "")}
+ ); +} + +export function UsersDemo({ transport }: UsersDemoProps): ReactElement { + const components = useMemo(() => createUsersDemoRegistry(), []); + return ( + + + + ); +} diff --git a/react-sdk/ui-react/src/html.test.tsx b/react-sdk/ui-react/src/html.test.tsx new file mode 100644 index 0000000..bd4a0c6 --- /dev/null +++ b/react-sdk/ui-react/src/html.test.tsx @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { createComponentRegistry } from "./registry.js"; +import { htmlTagNames, isHTMLTagName, registerHTMLTags } from "./html.js"; + +describe("HTML tag catalog", () => { + it("contains safe semantic, form, table, and media tags", () => { + expect(htmlTagNames).toContain("article"); + expect(htmlTagNames).toContain("input"); + expect(htmlTagNames).toContain("table"); + expect(htmlTagNames).toContain("video"); + expect(isHTMLTagName("script")).toBe(false); + expect(isHTMLTagName("iframe")).toBe(false); + }); + + it("registers host implementations only for catalog tags", () => { + const registry = createComponentRegistry(); + registerHTMLTags(registry, { article: { component: () => null } }); + expect(registry.has("article")).toBe(true); + }); +}); diff --git a/react-sdk/ui-react/src/html.ts b/react-sdk/ui-react/src/html.ts new file mode 100644 index 0000000..32d7af5 --- /dev/null +++ b/react-sdk/ui-react/src/html.ts @@ -0,0 +1,128 @@ +import type { ComponentDefinition, ComponentRegistry } from "./registry.js"; +import { createDefaultHTMLDefinition } from "./defaults.js"; + +export const htmlTagNames = [ + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "bdi", + "bdo", + "blockquote", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "data", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "div", + "dl", + "dt", + "em", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "header", + "hgroup", + "hr", + "i", + "img", + "input", + "ins", + "kbd", + "label", + "legend", + "li", + "main", + "map", + "mark", + "menu", + "meter", + "nav", + "ol", + "optgroup", + "option", + "output", + "p", + "picture", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "search", + "section", + "select", + "small", + "source", + "span", + "strong", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "textarea", + "tfoot", + "th", + "thead", + "time", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr", +] as const; + +export type HTMLTagName = (typeof htmlTagNames)[number]; + +const htmlTagSet = new Set(htmlTagNames); + +export function isHTMLTagName(name: string): name is HTMLTagName { + return htmlTagSet.has(name); +} + +// registerHTMLTags registers host-owned implementations for safe HTML tags. +export function registerHTMLTags( + registry: ComponentRegistry, + definitions: Partial>, +): void { + for (const [name, definition] of Object.entries(definitions)) { + if (!isHTMLTagName(name) || !definition) throw new Error(`unsupported html tag ${name}`); + registry.register(name, definition); + } +} + +// registerDefaultHTMLComponents registers safe native React implementations for every supported HTML tag. +// Existing host registrations take precedence. +export function registerDefaultHTMLComponents(registry: ComponentRegistry): void { + for (const name of htmlTagNames) + if (!registry.has(name)) registry.register(name, createDefaultHTMLDefinition(name)); +} diff --git a/react-sdk/ui-react/src/index.ts b/react-sdk/ui-react/src/index.ts new file mode 100644 index 0000000..4d9d3e5 --- /dev/null +++ b/react-sdk/ui-react/src/index.ts @@ -0,0 +1,6 @@ +export * from "./registry.js"; +export * from "./renderer.js"; +export * from "./runtime.js"; +export * from "./demo.js"; +export * from "./shadcn.js"; +export * from "./html.js"; diff --git a/react-sdk/ui-react/src/registry.test.tsx b/react-sdk/ui-react/src/registry.test.tsx new file mode 100644 index 0000000..c58d2b6 --- /dev/null +++ b/react-sdk/ui-react/src/registry.test.tsx @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { createComponentRegistry } from "./registry.js"; + +describe("ComponentRegistry", () => { + it("registers and resolves host components", () => { + const registry = createComponentRegistry(); + const component = () => null; + registry.register("text", { component }); + expect(registry.has("text")).toBe(true); + expect(registry.get("text")?.component).toBe(component); + }); + + it("rejects duplicate logical component names", () => { + const registry = createComponentRegistry(); + registry.register("text", { component: () => null }); + expect(() => registry.register("text", { component: () => null })).toThrow( + "already registered", + ); + }); +}); diff --git a/react-sdk/ui-react/src/registry.ts b/react-sdk/ui-react/src/registry.ts new file mode 100644 index 0000000..f77ffed --- /dev/null +++ b/react-sdk/ui-react/src/registry.ts @@ -0,0 +1,31 @@ +import type { ComponentType } from "react"; + +export interface ComponentDefinition> { + component: ComponentType; + allowedProps?: readonly string[]; + propsSchema?: (props: Record) => Props; + events?: readonly string[]; + slots?: readonly string[]; + children?: boolean; +} + +export class ComponentRegistry { + private readonly definitions = new Map(); + + register(name: string, definition: ComponentDefinition): void { + if (!name || this.definitions.has(name)) + throw new Error(`component ${name} is already registered`); + this.definitions.set(name, definition); + } + + get(name: string): ComponentDefinition | undefined { + return this.definitions.get(name); + } + has(name: string): boolean { + return this.definitions.has(name); + } +} + +export function createComponentRegistry(): ComponentRegistry { + return new ComponentRegistry(); +} diff --git a/react-sdk/ui-react/src/renderer.test.tsx b/react-sdk/ui-react/src/renderer.test.tsx new file mode 100644 index 0000000..582468f --- /dev/null +++ b/react-sdk/ui-react/src/renderer.test.tsx @@ -0,0 +1,204 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import type { ReactNode } from "react"; +import type { ViewSchema } from "@osspkg/ui-core"; +import { createComponentRegistry } from "./registry.js"; +import { UIProvider } from "./runtime.js"; +import { PluginView, ViewRenderer } from "./renderer.js"; + +function emptyRegions() { + return { "top-header": [], "left-panel": [], content: [], "right-panel": [], bottom: [] }; +} + +describe("static renderer", () => { + it("renders a schema passed directly to PluginView", () => { + const registry = createComponentRegistry(); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "provided", + regions: emptyRegions(), + }; + + const markup = renderToStaticMarkup( + + + , + ); + + expect(markup).toContain('data-ui-region="content"'); + }); + + it("renders registered components, children, and grid metadata", () => { + const registry = createComponentRegistry(); + registry.register("card", { + component: (props: Record) => ( +
+ {String(props.title)} + {props.children as ReactNode} +
+ ), + allowedProps: ["title"], + }); + registry.register("text", { + component: (props: Record) => {String(props.value)}, + allowedProps: ["value"], + children: false, + }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "renderer", + regions: { + ...emptyRegions(), + content: [ + { + id: "card", + component: "card", + layout: { row: 1, cols: 8, offset: 2 }, + props: { title: "Users" }, + children: [{ id: "text", component: "text", props: { value: "Ada" } }], + }, + ], + }, + }; + + const markup = renderToStaticMarkup( + + + , + ); + + expect(markup).toContain("Users"); + expect(markup).toContain("Ada"); + expect(markup).toContain('data-ui-cols="8"'); + expect(markup).toContain('data-ui-offset="2"'); + expect(markup).toContain("grid grid-cols-12 gap-4"); + expect(markup).toContain("col-span-[var(--ui-cols)]"); + expect(markup).toContain("--ui-row:1;--ui-cols:8;--ui-start:3"); + }); + + it("renders declared slots and rejects undeclared slots", () => { + const registry = createComponentRegistry(); + registry.register("card", { + component: (props: Record) => ( +
{(props.slots as Record).footer}
+ ), + slots: ["footer"], + }); + registry.register("text", { + component: (props: Record) => {String(props.value)}, + allowedProps: ["value"], + }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "slots", + regions: { + ...emptyRegions(), + content: [ + { + id: "card", + component: "card", + slots: { footer: [{ id: "footer", component: "text", props: { value: "Footer" } }] }, + }, + ], + }, + }; + + expect( + renderToStaticMarkup( + + + , + ), + ).toContain("Footer"); + + schema.regions.content[0]!.slots = { other: [] }; + expect(() => + renderToStaticMarkup( + + + , + ), + ).toThrow("slot other is not allowed"); + }); + + it("renders a safe fallback for unsupported components", () => { + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "unsupported", + regions: { ...emptyRegions(), content: [{ id: "missing", component: "missing-component" }] }, + }; + + const markup = renderToStaticMarkup( + + + , + ); + + expect(markup).toContain("Unsupported component: missing-component"); + expect(markup).toContain('data-unsupported-component="missing-component"'); + }); + + it("rejects props without a host declaration", () => { + const registry = createComponentRegistry(); + registry.register("text", { component: () => text }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "props", + regions: { + ...emptyRegions(), + content: [{ id: "text", component: "text", props: { value: "unsafe" } }], + }, + }; + + expect(() => + renderToStaticMarkup( + + + , + ), + ).toThrow("component props are not declared"); + }); + + it("rejects events without a host allowlist", () => { + const registry = createComponentRegistry(); + registry.register("button", { component: () => }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "events", + actions: { save: { type: "set-state", path: "saved", value: true } }, + regions: { + ...emptyRegions(), + content: [{ id: "button", component: "button", events: { click: { action: "save" } } }], + }, + }; + + expect(() => + renderToStaticMarkup( + + + , + ), + ).toThrow("component events are not declared"); + }); + + it("rejects plugin styling and raw React props", () => { + const registry = createComponentRegistry(); + registry.register("card", { component: () =>
, allowedProps: ["style"] }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "unsafe-props", + regions: { + ...emptyRegions(), + content: [{ id: "card", component: "card", props: { style: { color: "red" } } }], + }, + }; + + expect(() => + renderToStaticMarkup( + + + , + ), + ).toThrow("prop style is forbidden"); + }); +}); diff --git a/react-sdk/ui-react/src/renderer.tsx b/react-sdk/ui-react/src/renderer.tsx new file mode 100644 index 0000000..4fc1d1c --- /dev/null +++ b/react-sdk/ui-react/src/renderer.tsx @@ -0,0 +1,235 @@ +import { useEffect, useState, type CSSProperties, type ReactElement, type ReactNode } from "react"; +import { + uiRPCMethods, + validateView, + type EventHandler, + type RegionName, + type UIGetViewResponse, + type UINode, + type ViewSchema, +} from "@osspkg/ui-core"; +import { useUIRuntime, useUIViewRuntime, ViewRuntimeProvider } from "./runtime.js"; + +export interface PluginViewProps { + plugin?: string; + view: string; + context?: unknown; + schema?: ViewSchema; +} + +export function PluginView(props: PluginViewProps): ReactElement { + const { plugin, view, context, schema: providedSchema } = props; + const { transport } = useUIRuntime(); + const [loaded, setLoaded] = useState<{ + plugin?: string; + view: string; + context: unknown; + transport: typeof transport; + schema?: ViewSchema; + error?: Error; + }>(); + useEffect(() => { + if (providedSchema || !transport || !plugin) return; + let active = true; + void transport + .call(uiRPCMethods.getView, { + plugin, + view, + context, + }) + .then((response) => { + if (active) { + setLoaded({ + plugin, + view, + context, + transport, + schema: response.schema, + }); + } + }) + .catch((reason: unknown) => { + if (active) { + setLoaded({ + plugin, + view, + context, + transport, + error: reason instanceof Error ? reason : new Error("view loading failed"), + }); + } + }); + return () => { + active = false; + }; + }, [context, plugin, providedSchema, view, transport]); + + const loadedMatchesProps = + loaded !== undefined && + loaded.plugin === plugin && + loaded.view === view && + loaded.context === context && + loaded.transport === transport; + const schema = providedSchema ?? (loadedMatchesProps ? loaded.schema : undefined); + const error = !providedSchema && loadedMatchesProps ? loaded.error : undefined; + + if (error) return
{error.message}
; + if (!schema) return
Loading…
; + return ; +} + +export function ViewRenderer({ + schema, + plugin, + context = {}, +}: { + schema: ViewSchema; + plugin?: string; + context?: unknown; +}): ReactElement { + validateView(schema); + const runtimeKey = `${plugin ?? ""}:${schema.id}:${schema.revision ?? ""}`; + return ( + + + + ); +} + +function ViewRegions(): ReactElement { + const runtime = useUIViewRuntime(); + const regions: RegionName[] = ["top-header", "left-panel", "content", "right-panel", "bottom"]; + return ( + <> + {regions.map((region) => ( +
+ {runtime.schema.regions[region].map((node) => ( + + ))} +
+ ))} + + ); +} + +function NodeRenderer({ node }: { node: UINode }): ReactNode { + const runtime = useUIViewRuntime(); + if (node.when !== undefined && !runtime.resolve(node.when)) return null; + const definition = runtime.components.get(node.component); + if (!definition) + return ( +
+ Unsupported component: {node.component} +
+ ); + const rawProps = Object.fromEntries( + Object.entries(node.props ?? {}).map(([key, value]) => [key, runtime.resolve(value)]), + ); + const props = validateProps(rawProps, definition.allowedProps, definition.propsSchema); + const events = bindEvents(node.events, definition.events, runtime); + const children = + definition.children === false || !node.children?.length ? undefined : ( +
+ {node.children.map((child) => ( + + ))} +
+ ); + const slots = renderSlots(node.slots, definition.slots); + const Component = definition.component as (props: Record) => ReactElement; + const { layout } = node; + const layoutStyle = { + "--ui-row": layout?.row ?? "auto", + "--ui-cols": layout?.cols ?? 12, + "--ui-start": (layout?.offset ?? 0) + 1, + } as CSSProperties; + return ( +
+ + {children} + +
+ ); +} + +function renderSlots( + slots: Record | undefined, + allowedSlots: readonly string[] | undefined, +): Record | undefined { + if (!slots) return undefined; + if (!allowedSlots) throw new Error("component slots are not declared"); + const rendered: Record = {}; + for (const [name, nodes] of Object.entries(slots)) { + if (!allowedSlots.includes(name)) throw new Error(`slot ${name} is not allowed`); + rendered[name] = nodes.map((child) => ); + } + return rendered; +} + +function validateProps( + rawProps: Record, + allowedProps: readonly string[] | undefined, + schema: ((props: Record) => unknown) | undefined, +): Record { + for (const key of Object.keys(rawProps)) { + if (forbiddenPluginProps.has(key) || key.startsWith("on")) + throw new Error(`prop ${key} is forbidden`); + } + if (!allowedProps && !schema && Object.keys(rawProps).length > 0) + throw new Error("component props are not declared"); + if (allowedProps) + for (const key of Object.keys(rawProps)) + if (!allowedProps.includes(key)) throw new Error(`prop ${key} is not allowed`); + return schema ? (schema(rawProps) as Record) : rawProps; +} + +const forbiddenPluginProps = new Set([ + "dangerouslySetInnerHTML", + "style", + "className", + "ref", + "key", +]); + +function bindEvents( + handlers: Record | undefined, + allowedEvents: readonly string[] | undefined, + runtime: ReturnType, +): Record Promise> { + const entries = Object.entries(handlers ?? {}); + if (entries.length > 0 && !allowedEvents) throw new Error("component events are not declared"); + return Object.fromEntries( + entries.map(([name, handler]) => { + if (!/^[a-z][a-z0-9-]*$/.test(name) || !allowedEvents?.includes(name)) + throw new Error(`event ${name} is not allowed`); + return [eventProp(name), createEventHandler(handler, runtime)]; + }), + ); +} + +function eventProp(name: string): string { + return `on${name.slice(0, 1).toUpperCase()}${name.slice(1)}`; +} + +function createEventHandler( + handler: EventHandler, + runtime: ReturnType, +): (event: unknown) => Promise { + return async (event) => { + if (handler.action) await runtime.runAction(handler.action, event); + for (const step of handler.steps ?? []) + if (step.action) await runtime.runAction(step.action, event); + }; +} diff --git a/react-sdk/ui-react/src/runtime.test.tsx b/react-sdk/ui-react/src/runtime.test.tsx new file mode 100644 index 0000000..8e1c5da --- /dev/null +++ b/react-sdk/ui-react/src/runtime.test.tsx @@ -0,0 +1,202 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import type { ViewSchema } from "@osspkg/ui-core"; +import type { RPCTransport } from "@osspkg/ui-transport"; +import { createComponentRegistry } from "./registry.js"; +import { ViewRenderer } from "./renderer.js"; +import { UIProvider, useUISource, type UISource } from "./runtime.js"; + +describe("view runtime bindings", () => { + it("resolves state and context values before rendering and binds events", () => { + let received: Record | undefined; + const registry = createComponentRegistry(); + registry.register("probe", { + component: (props: Record) => { + received = props; + return ( + + {String(props.name)}:{String(props.workspace)} + + ); + }, + allowedProps: ["name", "workspace"], + events: ["click"], + }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "bindings", + state: { name: "Ada" }, + actions: { submit: { type: "set-state", path: "name", value: "Grace" } }, + regions: { + "top-header": [], + "left-panel": [], + content: [ + { + id: "probe", + component: "probe", + props: { name: { $state: "name" }, workspace: { $context: "workspace.id" } }, + events: { click: { action: "submit" } }, + }, + ], + "right-panel": [], + bottom: [], + }, + }; + + const markup = renderToStaticMarkup( + + + , + ); + + expect(markup).toContain("Ada:w-1"); + expect(received?.name).toBe("Ada"); + expect(received?.workspace).toBe("w-1"); + expect(received?.onClick).toEqual(expect.any(Function)); + }); + + it("does not render nodes whose when condition is false", () => { + const registry = createComponentRegistry(); + registry.register("probe", { component: () => visible }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "conditions", + state: { enabled: false }, + regions: { + "top-header": [], + "left-panel": [], + content: [{ id: "conditional", component: "probe", when: { $state: "enabled" } }], + "right-panel": [], + bottom: [], + }, + }; + + const markup = renderToStaticMarkup( + + + , + ); + + expect(markup).not.toContain("visible"); + }); + + it("exposes idle source state and manual refresh", () => { + let source: UISource | undefined; + const registry = createComponentRegistry(); + function SourceProbe() { + source = useUISource("users"); + return {source.status}; + } + registry.register("source-probe", { component: SourceProbe }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "source", + sources: { users: { type: "tool", tool: "users.list", policy: "manual" } }, + regions: { + "top-header": [], + "left-panel": [], + content: [{ id: "source-probe", component: "source-probe" }], + "right-panel": [], + bottom: [], + }, + }; + + const markup = renderToStaticMarkup( + + + , + ); + + expect(markup).toContain("idle"); + expect(source?.refresh).toEqual(expect.any(Function)); + }); + + it("executes server actions and host-controlled effects", async () => { + let received: Record | undefined; + const calls: Array<{ method: string; params: unknown }> = []; + const transport: RPCTransport = { + async call(method: string, params: unknown): Promise { + calls.push({ method, params }); + return { id: "u-1" } as T; + }, + subscribe: () => () => undefined, + close: () => undefined, + }; + const toasts: string[] = []; + const refreshedViews: string[] = []; + const patchedViews: string[] = []; + const registry = createComponentRegistry(); + registry.register("button", { + component: (props: Record) => { + received = props; + return ; + }, + events: ["click"], + }); + const schema: ViewSchema = { + protocolVersion: "1.0", + id: "actions", + actions: { + save: { + type: "tool", + tool: "users.save", + input: { name: { $event: "value" } }, + effects: [ + { type: "toast", variant: "success", message: "Saved" }, + { type: "refresh-view", revision: "rev-2" }, + { type: "patch-view", baseRevision: "rev-1", revision: "rev-2" }, + ], + }, + }, + sources: { users: { type: "tool", tool: "users.list", refreshOn: ["save"] } }, + regions: { + "top-header": [], + "left-panel": [], + content: [{ id: "save", component: "button", events: { click: { action: "save" } } }], + "right-panel": [], + bottom: [], + }, + }; + + renderToStaticMarkup( + { + toasts.push(message); + }, + refreshView: (effect) => { + refreshedViews.push(effect.type); + }, + patchView: (effect) => { + patchedViews.push(effect.type); + }, + }} + > + + , + ); + await (received?.onClick as (event: unknown) => Promise)({ value: "Ada" }); + + expect(calls).toEqual([ + { + method: "ui.action", + params: { plugin: "users", view: "actions", action: "save", input: { name: "Ada" } }, + }, + { + method: "data.call", + params: { + plugin: "users", + view: "actions", + source: "users", + operation: "users.list", + input: {}, + }, + }, + ]); + expect(toasts).toEqual(["Saved"]); + expect(refreshedViews).toEqual(["refresh-view"]); + expect(patchedViews).toEqual(["patch-view"]); + }); +}); diff --git a/react-sdk/ui-react/src/runtime.tsx b/react-sdk/ui-react/src/runtime.tsx new file mode 100644 index 0000000..97e52a7 --- /dev/null +++ b/react-sdk/ui-react/src/runtime.tsx @@ -0,0 +1,365 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type Dispatch, + type PropsWithChildren, + type ReactElement, + type SetStateAction, +} from "react"; +import { + createRequestTracker, + resolveValue, + resolvePath, + setPath, + uiRPCMethods, + type Effect, + type ResolveScope, + type SourceRuntimeState, + type UIValue, + type ViewSchema, +} from "@osspkg/ui-core"; +import type { RPCTransport } from "@osspkg/ui-transport"; +import { ComponentRegistry } from "./registry.js"; + +export interface UIEffectHandlers { + toast?(message: string, variant?: string): void | Promise; + navigate?(to: string): void | Promise; + dialog?(effect: Effect): void | Promise; + closeDialog?(effect: Effect): void | Promise; + refreshView?(effect: Effect): void | Promise; + patchView?(effect: Effect): void | Promise; +} + +export interface UIRuntime { + components: ComponentRegistry; + transport?: RPCTransport; + effects?: UIEffectHandlers; +} + +export interface ViewRuntime extends UIRuntime { + schema: ViewSchema; + plugin?: string; + context: unknown; + state: unknown; + sources: Record; + setState(path: string, value: unknown): void; + mergeState(path: string, value: Record): void; + resolve(value: unknown, event?: unknown, result?: unknown): unknown; + runAction(name: string, event?: unknown): Promise; + refreshSource(name: string): Promise; +} + +export interface UISource extends SourceRuntimeState { + refresh(): Promise; +} + +const runtimeContext = createContext(undefined); +const viewRuntimeContext = createContext(undefined); + +export function UIProvider({ + components, + transport, + effects, + children, +}: PropsWithChildren): ReactElement { + const value = useMemo( + () => ({ components, ...(transport ? { transport } : {}), ...(effects ? { effects } : {}) }), + [components, effects, transport], + ); + return {children}; +} + +export function useUIRuntime(): UIRuntime { + const runtime = useContext(runtimeContext); + if (!runtime) throw new Error("UIProvider is required"); + return runtime; +} + +export function useUIViewRuntime(): ViewRuntime { + const runtime = useContext(viewRuntimeContext); + if (!runtime) throw new Error("ViewRuntime is required"); + return runtime; +} + +export function useUISource(name: string): UISource { + const runtime = useUIViewRuntime(); + const source = runtime.sources[name] as SourceRuntimeState | undefined; + const refresh = useCallback(() => runtime.refreshSource(name), [name, runtime]); + return useMemo( + () => ({ ...(source ?? { status: "idle" as const }), refresh }), + [refresh, source], + ); +} + +export function useUIAction(name: string): (event?: unknown) => Promise { + const runtime = useUIViewRuntime(); + return useCallback((event?: unknown) => runtime.runAction(name, event), [name, runtime]); +} + +export function ViewRuntimeProvider({ + schema, + plugin, + context, + children, +}: PropsWithChildren<{ schema: ViewSchema; plugin?: string; context: unknown }>): ReactElement { + const base = useUIRuntime(); + const [state, setState] = useState(schema.state ?? {}); + const [sources, setSources] = useState>(() => + initialSourceStates(schema), + ); + const requestIDs = useRef(createRequestTracker()); + const controllers = useRef(new Map()); + + useEffect(() => { + requestIDs.current = createRequestTracker(); + setState(schema.state ?? {}); + setSources(initialSourceStates(schema)); + }, [schema]); + + const refreshSource = useCallback( + async (name: string): Promise => { + const source = schema.sources?.[name]; + if (!source || !base.transport) return; + controllers.current.get(name)?.abort(); + const controller = new AbortController(); + controllers.current.set(name, controller); + const requestID = requestIDs.current.next(name); + setSources((current) => ({ + ...current, + [name]: { status: "loading", requestId: String(requestID) }, + })); + try { + const input = resolveValue(source.input ?? {}, { state, context, sources }); + const data = await base.transport.call( + uiRPCMethods.dataCall, + { plugin, view: schema.id, source: name, operation: source.tool, input }, + { signal: controller.signal }, + ); + if (!requestIDs.current.isCurrent(name, requestID)) return; + setSources((current) => ({ + ...current, + [name]: { status: "success", data, updatedAt: Date.now(), requestId: String(requestID) }, + })); + } catch (error) { + if (controller.signal.aborted || !requestIDs.current.isCurrent(name, requestID)) return; + setSources((current) => ({ + ...current, + [name]: { + status: "error", + error: { + code: "SOURCE_FAILED", + message: error instanceof Error ? error.message : "source failed", + retryable: true, + }, + requestId: String(requestID), + }, + })); + } finally { + if (controllers.current.get(name) === controller) controllers.current.delete(name); + } + }, + [base.transport, context, plugin, schema, sources, state], + ); + + useEffect(() => { + for (const [name, source] of Object.entries(schema.sources ?? {})) { + if (source.policy === "on-mount") void refreshSource(name); + } + const activeControllers = controllers.current; + return () => { + for (const controller of activeControllers.values()) controller.abort(); + }; + // on-mount execution belongs to one schema lifecycle; source updates must not retrigger it. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [schema, base.transport, plugin]); + + useEffect(() => { + const onReconnect = base.transport?.onReconnect; + if (!onReconnect) return; + return onReconnect(() => { + for (const [name, source] of Object.entries(schema.sources ?? {})) { + if (source.policy === "on-mount") void refreshSource(name); + } + }); + }, [base.transport, refreshSource, schema]); + + useEffect(() => { + const { transport } = base; + if (!transport) return; + return transport.subscribe((notification) => { + const params = isRecord(notification.params) ? notification.params : {}; + const notificationPlugin = params.plugin; + const notificationView = params.view; + if (typeof notificationPlugin === "string" && notificationPlugin !== plugin) return; + if (typeof notificationView === "string" && notificationView !== schema.id) return; + if (notification.method === "data.invalidate" && typeof params.source === "string") { + void refreshSource(params.source); + return; + } + if (notification.method === "ui.invalidate" || notification.method === "plugin.changed") { + for (const [name, source] of Object.entries(schema.sources ?? {})) { + if (source.policy === "on-mount") void refreshSource(name); + } + } + }); + }, [base, plugin, refreshSource, schema]); + + const runtime = useMemo( + () => ({ + ...base, + schema, + ...(plugin ? { plugin } : {}), + context, + state, + sources, + setState: (path, value) => setState((current: unknown) => setPath(current, path, value)), + mergeState: (path, value) => + setState((current: unknown) => { + const currentValue = resolvePath(current, path); + const merged = + currentValue && typeof currentValue === "object" + ? { ...currentValue, ...value } + : value; + return setPath(current, path, merged); + }), + resolve: (value, event, result) => + resolveValue(value as UIValue, { state, context, sources, event, result }), + runAction: (name, event) => + executeAction( + name, + schema, + base.transport, + base.effects, + plugin, + state, + context, + sources, + setState, + refreshSource, + event, + ), + refreshSource, + }), + [base, context, plugin, refreshSource, schema, sources, state], + ); + return {children}; +} + +function initialSourceStates(schema: ViewSchema): Record { + return Object.fromEntries( + Object.keys(schema.sources ?? {}).map((name) => [name, { status: "idle" as const }]), + ); +} + +async function executeAction( + name: string, + schema: ViewSchema, + transport: RPCTransport | undefined, + handlers: UIEffectHandlers | undefined, + plugin: string | undefined, + state: unknown, + context: unknown, + sources: Record, + setState: Dispatch>, + refreshSource: (name: string) => Promise, + event?: unknown, +): Promise { + const action = schema.actions?.[name]; + if (!action) throw new Error(`action ${name} is not defined`); + const scope: ResolveScope = { state, context, sources, event }; + if (action.type === "set-state") { + setState((current: unknown) => + setPath(current, action.path ?? "", resolveValue(action.value ?? null, scope)), + ); + for (const effect of action.effects ?? []) + await executeEffect(effect, undefined, scope, handlers, setState, refreshSource); + await refreshTriggeredSources(name, schema, refreshSource); + return undefined; + } + if (action.type === "merge-state") { + const current = resolvePath(state, action.path ?? ""); + const next = resolveValue(action.value ?? null, scope); + setState((value: unknown) => + setPath(value, action.path ?? "", { + ...(current && typeof current === "object" ? current : {}), + ...(next && typeof next === "object" ? next : {}), + }), + ); + for (const effect of action.effects ?? []) + await executeEffect(effect, undefined, scope, handlers, setState, refreshSource); + await refreshTriggeredSources(name, schema, refreshSource); + return undefined; + } + if (action.type === "refresh-source" || action.type === "invalidate") { + if (action.source) await refreshSource(action.source); + await refreshTriggeredSources(name, schema, refreshSource); + return undefined; + } + if (action.type !== "tool" || !transport) throw new Error(`action ${name} requires a transport`); + const input = resolveValue(action.input ?? {}, scope); + const response = await transport.call(uiRPCMethods.action, { + plugin, + view: schema.id, + action: name, + input, + }); + for (const effect of action.effects ?? []) + await executeEffect(effect, response, scope, handlers, setState, refreshSource); + await refreshTriggeredSources(name, schema, refreshSource); + return response; +} + +async function refreshTriggeredSources( + name: string, + schema: ViewSchema, + refreshSource: (name: string) => Promise, +): Promise { + for (const [sourceName, source] of Object.entries(schema.sources ?? {})) { + if (source.refreshOn?.includes(name)) await refreshSource(sourceName); + } +} + +async function executeEffect( + effect: Effect, + result: unknown, + scope: ResolveScope, + handlers: UIEffectHandlers | undefined, + setState: Dispatch>, + refreshSource: (name: string) => Promise, +): Promise { + const effectScope = { ...scope, result }; + if (effect.type === "set-state" && effect.path) { + const value = effect.value === undefined ? result : resolveValue(effect.value, effectScope); + setState((current: unknown) => setPath(current, effect.path!, value)); + } + if (effect.type === "merge-state" && effect.path) { + const value = resolveValue(effect.value ?? {}, effectScope); + setState((current: unknown) => { + const existing = resolvePath(current, effect.path!); + const merged = + existing && typeof existing === "object" + ? { ...existing, ...(value && typeof value === "object" ? value : {}) } + : value; + return setPath(current, effect.path!, merged); + }); + } + if ((effect.type === "invalidate" || effect.type === "refresh-source") && effect.source) + await refreshSource(effect.source); + if (effect.type === "toast" && effect.message && handlers?.toast) + await handlers.toast(effect.message, effect.variant); + if (effect.type === "navigate" && effect.to && handlers?.navigate) + await handlers.navigate(effect.to); + if (effect.type === "dialog" && handlers?.dialog) await handlers.dialog(effect); + if (effect.type === "close-dialog" && handlers?.closeDialog) await handlers.closeDialog(effect); + if (effect.type === "refresh-view" && handlers?.refreshView) await handlers.refreshView(effect); + if (effect.type === "patch-view" && handlers?.patchView) await handlers.patchView(effect); +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} diff --git a/react-sdk/ui-react/src/shadcn.test.tsx b/react-sdk/ui-react/src/shadcn.test.tsx new file mode 100644 index 0000000..5ee5262 --- /dev/null +++ b/react-sdk/ui-react/src/shadcn.test.tsx @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { createComponentRegistry } from "./registry.js"; +import { isShadcnComponentName, registerShadcnComponents, shadcnComponentNames } from "./shadcn.js"; + +describe("shadcn component catalog", () => { + it("contains the official top-level component names", () => { + expect(shadcnComponentNames).toHaveLength(64); + expect(isShadcnComponentName("alert-dialog")).toBe(true); + expect(isShadcnComponentName("input-otp")).toBe(true); + expect(isShadcnComponentName("not-a-component")).toBe(false); + }); + + it("registers only catalog components with host-owned definitions", () => { + const registry = createComponentRegistry(); + registerShadcnComponents(registry, { button: { component: () => null, events: ["click"] } }); + expect(registry.has("button")).toBe(true); + }); +}); diff --git a/react-sdk/ui-react/src/shadcn.ts b/react-sdk/ui-react/src/shadcn.ts new file mode 100644 index 0000000..1e802c6 --- /dev/null +++ b/react-sdk/ui-react/src/shadcn.ts @@ -0,0 +1,98 @@ +import type { ComponentDefinition, ComponentRegistry } from "./registry.js"; +import { createDefaultShadcnDefinition } from "./defaults.js"; + +export const shadcnComponentNames = [ + "accordion", + "alert", + "alert-dialog", + "aspect-ratio", + "attachment", + "avatar", + "badge", + "breadcrumb", + "bubble", + "button", + "button-group", + "calendar", + "card", + "carousel", + "chart", + "checkbox", + "collapsible", + "combobox", + "command", + "context-menu", + "data-table", + "date-picker", + "dialog", + "direction", + "drawer", + "dropdown-menu", + "empty", + "field", + "hover-card", + "input", + "input-group", + "input-otp", + "item", + "kbd", + "label", + "marker", + "menubar", + "message", + "message-scroller", + "native-select", + "navigation-menu", + "pagination", + "popover", + "progress", + "questionnaire", + "radio-group", + "resizable", + "scroll-area", + "select", + "separator", + "sheet", + "sidebar", + "skeleton", + "slider", + "spinner", + "switch", + "table", + "tabs", + "textarea", + "toast", + "toggle", + "toggle-group", + "tooltip", + "typography", +] as const; + +export type ShadcnComponentName = (typeof shadcnComponentNames)[number]; + +const shadcnComponentSet = new Set(shadcnComponentNames); + +export function isShadcnComponentName(name: string): name is ShadcnComponentName { + return shadcnComponentSet.has(name); +} + +// registerShadcnComponents registers host implementations for shadcn/ui names. +// The host deliberately supplies the implementations and their prop allowlists. +export function registerShadcnComponents( + registry: ComponentRegistry, + definitions: Partial>, +): void { + for (const [name, definition] of Object.entries(definitions)) { + if (!isShadcnComponentName(name) || !definition) + throw new Error(`unsupported shadcn component ${name}`); + registry.register(name, definition); + } +} + +// registerDefaultShadcnComponents registers baseline, dependency-free renderers for every shadcn catalog name. +// Install and register host-owned shadcn/ui source components to replace these defaults where richer behavior is needed. +// Existing host registrations take precedence. +export function registerDefaultShadcnComponents(registry: ComponentRegistry): void { + for (const name of shadcnComponentNames) + if (!registry.has(name)) registry.register(name, createDefaultShadcnDefinition(name)); +} diff --git a/react-sdk/ui-react/src/styles.css b/react-sdk/ui-react/src/styles.css new file mode 100644 index 0000000..c9b6ef3 --- /dev/null +++ b/react-sdk/ui-react/src/styles.css @@ -0,0 +1,17 @@ +@import "tailwindcss"; + +@source "./"; + +@theme { + --color-ui-background: oklch(1 0 0); + --color-ui-foreground: oklch(0.145 0 0); + --color-ui-card: oklch(1 0 0); + --color-ui-card-foreground: oklch(0.145 0 0); + --color-ui-primary: oklch(0.205 0 0); + --color-ui-primary-foreground: oklch(0.985 0 0); + --color-ui-muted: oklch(0.97 0 0); + --color-ui-muted-foreground: oklch(0.556 0 0); + --color-ui-border: oklch(0.922 0 0); + --color-ui-ring: oklch(0.708 0 0); + --color-ui-destructive: oklch(0.577 0.245 27.325); +} diff --git a/react-sdk/ui-react/src/styles.css.d.ts b/react-sdk/ui-react/src/styles.css.d.ts new file mode 100644 index 0000000..a9f6f08 --- /dev/null +++ b/react-sdk/ui-react/src/styles.css.d.ts @@ -0,0 +1,3 @@ +declare const stylesheet: string; + +export default stylesheet; diff --git a/react-sdk/ui-react/tsconfig.json b/react-sdk/ui-react/tsconfig.json new file mode 100644 index 0000000..1c2ad44 --- /dev/null +++ b/react-sdk/ui-react/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": {"jsx": "react-jsx", "rootDir": "src", "outDir": "dist"}, + "include": ["src"] +} diff --git a/react-sdk/ui-react/vitest.config.ts b/react-sdk/ui-react/vitest.config.ts new file mode 100644 index 0000000..5465441 --- /dev/null +++ b/react-sdk/ui-react/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.tsx"], + }, +}); diff --git a/react-sdk/ui-transport/package.json b/react-sdk/ui-transport/package.json new file mode 100644 index 0000000..a25dde7 --- /dev/null +++ b/react-sdk/ui-transport/package.json @@ -0,0 +1,28 @@ +{ + "name": "@osspkg/ui-transport", + "version": "0.0.0", + "type": "module", + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run --config vitest.config.ts", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@osspkg/ui-core": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.9.0", + "vitest": "^3.2.0" + } +} \ No newline at end of file diff --git a/react-sdk/ui-transport/src/http.ts b/react-sdk/ui-transport/src/http.ts new file mode 100644 index 0000000..1c0ec0d --- /dev/null +++ b/react-sdk/ui-transport/src/http.ts @@ -0,0 +1,57 @@ +import { + createRequest, + readResponse, + type CallOptions, + type RPCNotification, + type RPCTransport, +} from "./rpc.js"; + +export interface HTTPTransportOptions { + fetch?: typeof globalThis.fetch; + headers?: Record; + timeoutMs?: number; +} + +export class HTTPTransport implements RPCTransport { + private readonly fetcher: typeof globalThis.fetch; + private readonly headers: Record; + private readonly timeoutMs: number; + + constructor( + private readonly endpoint: string, + options: HTTPTransportOptions = {}, + ) { + this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis); + this.headers = { "content-type": "application/json", ...(options.headers ?? {}) }; + this.timeoutMs = options.timeoutMs ?? 30_000; + } + + async call(method: string, params: unknown, options: CallOptions = {}): Promise { + const request = createRequest(method, params); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? this.timeoutMs); + const abort = () => controller.abort(); + options.signal?.addEventListener("abort", abort, { once: true }); + try { + if (options.signal?.aborted) throw new DOMException("request was cancelled", "AbortError"); + const response = await this.fetcher(this.endpoint, { + method: "POST", + headers: this.headers, + body: JSON.stringify(request), + signal: controller.signal, + }); + const payload = (await response.json()) as unknown; + if (!response.ok) throw new Error(`http request failed with status ${response.status}`); + return readResponse(payload, request.id); + } finally { + clearTimeout(timeout); + options.signal?.removeEventListener("abort", abort); + } + } + + subscribe(_handler: (notification: RPCNotification) => void): () => void { + return () => undefined; + } + + close(): void {} +} diff --git a/react-sdk/ui-transport/src/index.ts b/react-sdk/ui-transport/src/index.ts new file mode 100644 index 0000000..69bb765 --- /dev/null +++ b/react-sdk/ui-transport/src/index.ts @@ -0,0 +1,3 @@ +export * from "./http.js"; +export * from "./rpc.js"; +export * from "./websocket.js"; diff --git a/react-sdk/ui-transport/src/rpc.ts b/react-sdk/ui-transport/src/rpc.ts new file mode 100644 index 0000000..f92613d --- /dev/null +++ b/react-sdk/ui-transport/src/rpc.ts @@ -0,0 +1,69 @@ +export interface RPCRequest { + jsonrpc: "2.0"; + id: number; + method: string; + params: unknown; + meta?: { correlationId: string }; +} + +export interface RPCResponse { + jsonrpc: "2.0"; + id: number; + result?: T; + error?: { code: number | string; message: string; data?: unknown }; +} + +export interface RPCNotification { + jsonrpc: "2.0"; + method: string; + params?: unknown; +} + +export interface CallOptions { + signal?: AbortSignal; + timeoutMs?: number; +} + +export interface RPCTransport { + call(method: string, params: unknown, options?: CallOptions): Promise; + subscribe(handler: (notification: RPCNotification) => void): () => void; + onReconnect?(handler: () => void): () => void; + close(): void; +} + +export class RPCError extends Error { + readonly code: number | string; + readonly details: unknown; + + constructor(code: number | string, message: string, details?: unknown) { + super(message); + this.name = "RPCError"; + this.code = code; + this.details = details; + } +} + +let nextRequestID = 1; +let nextCorrelationID = 1; + +export function createRequest(method: string, params: unknown): RPCRequest { + const id = nextRequestID++; + return { + jsonrpc: "2.0", + id, + method, + params, + meta: { correlationId: `ui-${nextCorrelationID++}` }, + }; +} + +export function readResponse(value: unknown, expectedID: number): T { + if (!value || typeof value !== "object") + throw new RPCError("INVALID_RESPONSE", "rpc response is invalid"); + const response = value as RPCResponse; + if (response.jsonrpc !== "2.0" || response.id !== expectedID) + throw new RPCError("INVALID_RESPONSE", "rpc response id is invalid"); + if (response.error) + throw new RPCError(response.error.code, response.error.message, response.error.data); + return response.result as T; +} diff --git a/react-sdk/ui-transport/src/transport.test.ts b/react-sdk/ui-transport/src/transport.test.ts new file mode 100644 index 0000000..60cf4e0 --- /dev/null +++ b/react-sdk/ui-transport/src/transport.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it, vi } from "vitest"; +import { HTTPTransport } from "./http.js"; + +describe("HTTPTransport", () => { + it("sends JSON-RPC requests and returns result", async () => { + let requestInit: RequestInit | undefined; + const fetcher = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { + requestInit = init; + return new Response(JSON.stringify({ jsonrpc: "2.0", id: 1, result: { ok: true } }), { + status: 200, + }); + }); + const transport = new HTTPTransport("https://example.test/rpc", { fetch: fetcher }); + await expect(transport.call("ping", { value: 1 })).resolves.toEqual({ ok: true }); + expect(fetcher).toHaveBeenCalledOnce(); + expect(JSON.parse(String(requestInit?.body))).toMatchObject({ + method: "ping", + params: { value: 1 }, + }); + }); + + it("propagates caller cancellation to fetch", async () => { + const fetcher = vi.fn( + (_input: RequestInfo | URL, init?: RequestInit) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener( + "abort", + () => reject(new DOMException("aborted", "AbortError")), + { once: true }, + ); + }), + ); + const transport = new HTTPTransport("https://example.test/rpc", { fetch: fetcher }); + const controller = new AbortController(); + const request = transport.call("slow", {}, { signal: controller.signal }); + + controller.abort(); + + await expect(request).rejects.toMatchObject({ name: "AbortError" }); + }); + + it("does not send a request when the caller signal is already aborted", async () => { + const fetcher = vi.fn(); + const transport = new HTTPTransport("https://example.test/rpc", { fetch: fetcher }); + const controller = new AbortController(); + controller.abort(); + + await expect( + transport.call("cancelled", {}, { signal: controller.signal }), + ).rejects.toMatchObject({ name: "AbortError" }); + expect(fetcher).not.toHaveBeenCalled(); + }); + + it("aborts requests that exceed the timeout", async () => { + vi.useFakeTimers(); + const fetcher = vi.fn( + (_input: RequestInfo | URL, init?: RequestInit) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener( + "abort", + () => reject(new DOMException("timed out", "AbortError")), + { once: true }, + ); + }), + ); + const transport = new HTTPTransport("https://example.test/rpc", { + fetch: fetcher, + timeoutMs: 10, + }); + const request = transport.call("slow", {}); + const assertion = expect(request).rejects.toMatchObject({ name: "AbortError" }); + + await vi.advanceTimersByTimeAsync(10); + + await assertion; + vi.useRealTimers(); + }); + + it("rejects malformed JSON-RPC responses", async () => { + const fetcher = vi.fn( + async () => + new Response(JSON.stringify({ jsonrpc: "2.0", id: 999, result: true }), { status: 200 }), + ); + const transport = new HTTPTransport("https://example.test/rpc", { fetch: fetcher }); + + await expect(transport.call("ping", {})).rejects.toMatchObject({ + name: "RPCError", + code: "INVALID_RESPONSE", + }); + }); +}); diff --git a/react-sdk/ui-transport/src/websocket.test.ts b/react-sdk/ui-transport/src/websocket.test.ts new file mode 100644 index 0000000..133b0c8 --- /dev/null +++ b/react-sdk/ui-transport/src/websocket.test.ts @@ -0,0 +1,98 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { WebSocketTransport } from "./websocket.js"; + +class FakeSocket { + static instances: FakeSocket[] = []; + readyState = 0; + onopen: ((event: unknown) => void) | null = null; + onclose: ((event: unknown) => void) | null = null; + onerror: ((event: unknown) => void) | null = null; + onmessage: ((event: { data: string }) => void) | null = null; + readonly sent: string[] = []; + + constructor(readonly url: string) { + FakeSocket.instances.push(this); + } + + send(data: string): void { + this.sent.push(data); + } + + close(): void { + this.readyState = 3; + this.onclose?.({}); + } + + open(): void { + this.readyState = 1; + this.onopen?.({}); + } + + receive(message: unknown): void { + this.onmessage?.({ data: JSON.stringify(message) }); + } +} + +afterEach(() => { + FakeSocket.instances = []; + vi.useRealTimers(); +}); + +describe("WebSocketTransport", () => { + it("sends calls and delivers notifications", async () => { + const transport = new WebSocketTransport("wss://example.test/rpc", { socket: FakeSocket }); + const notifications: unknown[] = []; + transport.subscribe((notification) => notifications.push(notification)); + const result = transport.call<{ ok: boolean }>("ping", { value: 1 }); + const socket = FakeSocket.instances[0]!; + socket.open(); + await Promise.resolve(); + const request = JSON.parse(socket.sent[0]!) as { id: number }; + socket.receive({ jsonrpc: "2.0", id: request.id, result: { ok: true } }); + await expect(result).resolves.toEqual({ ok: true }); + + socket.receive({ jsonrpc: "2.0", method: "ui.invalidate", params: { view: "users" } }); + expect(notifications).toEqual([ + { jsonrpc: "2.0", method: "ui.invalidate", params: { view: "users" } }, + ]); + transport.close(); + }); + + it("reconnects and notifies the runtime after a new socket opens", async () => { + vi.useFakeTimers(); + const transport = new WebSocketTransport("wss://example.test/rpc", { + socket: FakeSocket, + reconnect: true, + reconnectDelayMs: 10, + }); + const reconnected = vi.fn(); + transport.onReconnect(reconnected); + const result = transport.call("ping", {}); + const first = FakeSocket.instances[0]!; + first.open(); + await Promise.resolve(); + const request = JSON.parse(first.sent[0]!) as { id: number }; + first.receive({ jsonrpc: "2.0", id: request.id, result: true }); + await expect(result).resolves.toBe(true); + + first.close(); + await vi.advanceTimersByTimeAsync(10); + const second = FakeSocket.instances[1]!; + second.open(); + + expect(reconnected).toHaveBeenCalledOnce(); + transport.close(); + }); + + it("rejects a call when the socket closes before opening", async () => { + const transport = new WebSocketTransport("wss://example.test/rpc", { + socket: FakeSocket, + reconnect: true, + }); + const result = transport.call("ping", {}); + FakeSocket.instances[0]!.close(); + + await expect(result).rejects.toThrow("websocket closed before connection opened"); + transport.close(); + }); +}); diff --git a/react-sdk/ui-transport/src/websocket.ts b/react-sdk/ui-transport/src/websocket.ts new file mode 100644 index 0000000..fb4a70b --- /dev/null +++ b/react-sdk/ui-transport/src/websocket.ts @@ -0,0 +1,194 @@ +import { + createRequest, + readResponse, + type CallOptions, + type RPCNotification, + type RPCTransport, +} from "./rpc.js"; + +interface SocketLike { + readyState: number; + onopen: ((event: unknown) => void) | null; + onclose: ((event: unknown) => void) | null; + onerror: ((event: unknown) => void) | null; + onmessage: ((event: { data: string }) => void) | null; + send(data: string): void; + close(): void; +} + +interface SocketConstructor { + new (url: string): SocketLike; +} + +export interface WebSocketTransportOptions { + socket?: SocketConstructor; + timeoutMs?: number; + reconnect?: boolean; + reconnectDelayMs?: number; +} + +interface PendingCall { + resolve(value: unknown): void; + reject(error: unknown): void; + timer: ReturnType; + abort?: () => void; + signal?: AbortSignal; +} + +export class WebSocketTransport implements RPCTransport { + private socket: SocketLike | undefined; + private closed = false; + private connecting: Promise | undefined; + private readonly pending = new Map(); + private readonly listeners = new Set<(notification: RPCNotification) => void>(); + private readonly reconnectListeners = new Set<() => void>(); + private readonly Socket: SocketConstructor; + private reconnectTimer: ReturnType | undefined; + private hasConnected = false; + + constructor( + private readonly endpoint: string, + private readonly options: WebSocketTransportOptions = {}, + ) { + this.Socket = options.socket ?? (WebSocket as unknown as SocketConstructor); + } + + async call(method: string, params: unknown, callOptions: CallOptions = {}): Promise { + if (this.closed) throw new Error("transport is closed"); + await this.connect(); + const request = createRequest(method, params); + const timeoutMs = callOptions.timeoutMs ?? this.options.timeoutMs ?? 30_000; + return new Promise((resolve, reject) => { + const pending: PendingCall = { + resolve, + reject, + timer: setTimeout(() => { + if (this.pending.get(request.id) !== pending) return; + this.pending.delete(request.id); + this.cleanupPending(pending); + reject(new Error("rpc request timed out")); + }, timeoutMs), + ...(callOptions.signal ? { signal: callOptions.signal } : {}), + }; + const abort = () => { + if (this.pending.get(request.id) !== pending) return; + this.pending.delete(request.id); + this.cleanupPending(pending); + reject(new DOMException("request was cancelled", "AbortError")); + }; + pending.abort = abort; + this.pending.set(request.id, pending); + callOptions.signal?.addEventListener("abort", abort, { once: true }); + if (callOptions.signal?.aborted) { + abort(); + return; + } + try { + this.socket!.send(JSON.stringify(request)); + } catch (error) { + this.pending.delete(request.id); + this.cleanupPending(pending); + reject(error); + } + }); + } + + subscribe(handler: (notification: RPCNotification) => void): () => void { + this.listeners.add(handler); + return () => this.listeners.delete(handler); + } + + onReconnect(handler: () => void): () => void { + this.reconnectListeners.add(handler); + return () => this.reconnectListeners.delete(handler); + } + + close(): void { + this.closed = true; + if (this.reconnectTimer) clearTimeout(this.reconnectTimer); + this.reconnectTimer = undefined; + this.socket?.close(); + for (const pending of this.pending.values()) { + this.cleanupPending(pending); + pending.reject(new Error("transport is closed")); + } + this.pending.clear(); + } + + private connect(): Promise { + if (this.socket?.readyState === 1) return Promise.resolve(); + if (this.connecting) return this.connecting; + this.connecting = new Promise((resolve, reject) => { + const socket = new this.Socket(this.endpoint); + this.socket = socket; + socket.onopen = () => { + if (this.socket !== socket) return; + this.connecting = undefined; + const reconnect = this.hasConnected; + this.hasConnected = true; + resolve(); + if (reconnect) for (const listener of this.reconnectListeners) listener(); + }; + socket.onerror = () => { + if (this.socket !== socket) return; + this.socket = undefined; + this.connecting = undefined; + reject(new Error("websocket connection failed")); + if (!this.closed && this.options.reconnect) this.scheduleReconnect(); + }; + socket.onclose = () => { + if (this.socket !== socket) return; + this.socket = undefined; + this.connecting = undefined; + reject(new Error("websocket closed before connection opened")); + if (!this.closed && this.options.reconnect) this.scheduleReconnect(); + for (const [id, pending] of this.pending) { + this.pending.delete(id); + this.cleanupPending(pending); + pending.reject(new Error("websocket closed")); + } + }; + socket.onmessage = (event) => this.handleMessage(event.data); + }); + return this.connecting; + } + + private handleMessage(data: string): void { + let message: unknown; + try { + message = JSON.parse(data); + } catch { + return; + } + if (!message || typeof message !== "object") return; + const record = message as { id?: unknown; method?: unknown }; + if (typeof record.id === "number") { + const pending = this.pending.get(record.id); + if (!pending) return; + this.pending.delete(record.id); + this.cleanupPending(pending); + try { + pending.resolve(readResponse(message, record.id)); + } catch (error) { + pending.reject(error); + } + return; + } + if (typeof record.method === "string") + for (const listener of this.listeners) listener(message as RPCNotification); + } + + private scheduleReconnect(): void { + if (this.reconnectTimer) return; + const delay = this.options.reconnectDelayMs ?? 1000; + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = undefined; + if (!this.closed) void this.connect().catch(() => undefined); + }, delay); + } + + private cleanupPending(pending: PendingCall): void { + clearTimeout(pending.timer); + if (pending.abort && pending.signal) pending.signal.removeEventListener("abort", pending.abort); + } +} diff --git a/react-sdk/ui-transport/tsconfig.json b/react-sdk/ui-transport/tsconfig.json new file mode 100644 index 0000000..0b1110e --- /dev/null +++ b/react-sdk/ui-transport/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": {"rootDir": "src", "outDir": "dist"}, + "include": ["src"] +} diff --git a/react-sdk/ui-transport/vitest.config.ts b/react-sdk/ui-transport/vitest.config.ts new file mode 100644 index 0000000..c1433e6 --- /dev/null +++ b/react-sdk/ui-transport/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/schemas/actions.schema.json b/schemas/actions.schema.json new file mode 100644 index 0000000..0fe3598 --- /dev/null +++ b/schemas/actions.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "UI Actions", + "description": "A name-keyed map of actions available to a view.", + "type": "object", + "propertyNames": { "$ref": "node.schema.json#/$defs/SafeName" }, + "additionalProperties": { "$ref": "#/$defs/Action" }, + "$defs": { + "SafePath": { "$ref": "node.schema.json#/$defs/SafePath" }, + "UIValue": { "$ref": "node.schema.json#/$defs/UIValue" }, + "Action": { + "type": "object", + "properties": { + "type": { "enum": ["tool", "set-state", "merge-state", "refresh-source", "invalidate"] }, + "tool": { "$ref": "node.schema.json#/$defs/SafeName" }, + "input": { + "type": "object", + "additionalProperties": { "$ref": "node.schema.json#/$defs/UIValue" } + }, + "path": { "$ref": "node.schema.json#/$defs/SafePath" }, + "value": { "$ref": "node.schema.json#/$defs/UIValue" }, + "source": { "$ref": "node.schema.json#/$defs/SafeName" }, + "to": { "type": "string" }, + "variant": { "type": "string" }, + "message": { "type": "string" }, + "effects": { "$ref": "effects.schema.json" } + }, + "required": ["type"], + "allOf": [ + { + "if": { "properties": { "type": { "const": "tool" } }, "required": ["type"] }, + "then": { "required": ["tool"] } + }, + { + "if": { "properties": { "type": { "enum": ["set-state", "merge-state"] } }, "required": ["type"] }, + "then": { "required": ["path"] } + }, + { + "if": { "properties": { "type": { "enum": ["refresh-source", "invalidate"] } }, "required": ["type"] }, + "then": { "required": ["source"] } + } + ] + } + } +} diff --git a/schemas/effects.schema.json b/schemas/effects.schema.json new file mode 100644 index 0000000..25b4379 --- /dev/null +++ b/schemas/effects.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "UI Action Effects", + "description": "An ordered list of client-side effects emitted by an action.", + "type": "array", + "items": { "$ref": "#/$defs/Effect" }, + "$defs": { + "SafePath": { "$ref": "node.schema.json#/$defs/SafePath" }, + "UIValue": { "$ref": "node.schema.json#/$defs/UIValue" }, + "Effect": { + "type": "object", + "properties": { + "type": { + "enum": ["set-state", "merge-state", "invalidate", "refresh-source", "refresh-view", "patch-view", "navigate", "toast", "dialog", "close-dialog"] + }, + "path": { "$ref": "node.schema.json#/$defs/SafePath" }, + "source": { "$ref": "node.schema.json#/$defs/SafeName" }, + "value": { "$ref": "node.schema.json#/$defs/UIValue" }, + "baseRevision": { "type": "string" }, + "revision": { "type": "string" }, + "to": { "type": "string", "minLength": 1 }, + "variant": { "type": "string" }, + "message": { "type": "string", "minLength": 1 } + }, + "required": ["type"], + "allOf": [ + { + "if": { "properties": { "type": { "enum": ["set-state", "merge-state"] } }, "required": ["type"] }, + "then": { "required": ["path"] } + }, + { + "if": { "properties": { "type": { "enum": ["invalidate", "refresh-source"] } }, "required": ["type"] }, + "then": { "required": ["source"] } + }, + { + "if": { "properties": { "type": { "const": "navigate" } }, "required": ["type"] }, + "then": { "required": ["to"] } + }, + { + "if": { "properties": { "type": { "const": "toast" } }, "required": ["type"] }, + "then": { "required": ["message"] } + } + ] + } + } +} diff --git a/schemas/layout.schema.json b/schemas/layout.schema.json new file mode 100644 index 0000000..c1d6b12 --- /dev/null +++ b/schemas/layout.schema.json @@ -0,0 +1,255 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "UI Grid Layout", + "description": "Places a node in the protocol's 12-column grid. Region-specific rules for side panels are enforced by the SDK validators.", + "type": "object", + "properties": { + "row": { + "type": "integer", + "minimum": 1 + }, + "cols": { + "type": "integer", + "minimum": 1, + "maximum": 12 + }, + "offset": { + "type": "integer", + "minimum": 0, + "maximum": 11 + } + }, + "required": [ + "row" + ], + "allOf": [ + { + "if": { + "properties": { + "cols": { + "const": 1 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 11 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 2 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 10 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 3 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 9 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 4 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 8 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 5 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 7 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 6 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 6 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 7 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 5 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 8 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 4 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 9 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 3 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 10 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 2 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 11 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 1 + } + } + } + }, + { + "if": { + "properties": { + "cols": { + "const": 12 + } + }, + "required": [ + "cols" + ] + }, + "then": { + "properties": { + "offset": { + "maximum": 0 + } + } + } + } + ] +} diff --git a/schemas/manifest.schema.json b/schemas/manifest.schema.json new file mode 100644 index 0000000..74bb59c --- /dev/null +++ b/schemas/manifest.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "UI Manifest", + "description": "Lists a plugin's UI views and their schema resource URIs. The SDK validators additionally require view IDs to be unique.", + "type": "object", + "properties": { + "protocolVersion": { + "const": "1.0" + }, + "plugin": { + "type": "object", + "properties": { + "id": { + "$ref": "node.schema.json#/$defs/SafeName" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "version" + ] + }, + "views": { + "type": "array", + "items": { + "$ref": "#/$defs/ViewDescriptor" + } + }, + "requiredComponents": { + "type": "array", + "items": { + "$ref": "node.schema.json#/$defs/ComponentName" + } + } + }, + "required": [ + "protocolVersion", + "plugin", + "views" + ], + "$defs": { + "ViewDescriptor": { + "type": "object", + "properties": { + "id": { + "$ref": "node.schema.json#/$defs/SafeName" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "route": { + "type": "string" + }, + "schema": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "id", + "title", + "schema" + ] + } + } +} diff --git a/schemas/node.schema.json b/schemas/node.schema.json new file mode 100644 index 0000000..2e490b3 --- /dev/null +++ b/schemas/node.schema.json @@ -0,0 +1,304 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "UI Node", + "description": "A declarative component node. Node trees are recursive; IDs must be unique within their view (checked by the SDK validators).", + "$ref": "#/$defs/Node", + "$defs": { + "SafeName": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\r\\n]+$" + }, + "ComponentName": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "SafePath": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!\\.)(?!.*\\.$)(?!.*(?:^|\\.)(?:__proto__|prototype|constructor)(?:\\.|$)).+$" + }, + "JSONValue": { + "anyOf": [ + { + "type": [ + "string", + "number", + "integer", + "boolean", + "null" + ] + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/UIValue" + } + }, + { + "type": "object", + "not": { + "minProperties": 1, + "maxProperties": 1, + "propertyNames": { + "pattern": "^\\$" + } + }, + "propertyNames": { + "not": { + "enum": [ + "__proto__", + "prototype", + "constructor" + ] + } + }, + "additionalProperties": { + "$ref": "#/$defs/UIValue" + } + } + ] + }, + "Reference": { + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "properties": { + "$state": { + "$ref": "#/$defs/SafePath" + }, + "$context": { + "$ref": "#/$defs/SafePath" + }, + "$source": { + "$ref": "#/$defs/SafePath" + }, + "$event": { + "$ref": "#/$defs/SafePath" + }, + "$result": { + "$ref": "#/$defs/SafePath" + } + }, + "additionalProperties": false + }, + "Expression": { + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "properties": { + "$eq": { + "$ref": "#/$defs/TwoValues" + }, + "$ne": { + "$ref": "#/$defs/TwoValues" + }, + "$gt": { + "$ref": "#/$defs/TwoValues" + }, + "$gte": { + "$ref": "#/$defs/TwoValues" + }, + "$lt": { + "$ref": "#/$defs/TwoValues" + }, + "$lte": { + "$ref": "#/$defs/TwoValues" + }, + "$and": { + "$ref": "#/$defs/OneOrMoreValues" + }, + "$or": { + "$ref": "#/$defs/OneOrMoreValues" + }, + "$not": { + "$ref": "#/$defs/OneValue" + }, + "$in": { + "$ref": "#/$defs/TwoValues" + }, + "$exists": { + "$ref": "#/$defs/OneValue" + }, + "$concat": { + "$ref": "#/$defs/ZeroOrMoreValues" + }, + "$coalesce": { + "$ref": "#/$defs/OneOrMoreValues" + }, + "$if": { + "$ref": "#/$defs/ThreeValues" + } + }, + "additionalProperties": false + }, + "UIValue": { + "description": "JSON literal, protocol reference, or expression. Path safety, source existence, and aggregate limits are checked by the SDK validators.", + "anyOf": [ + { + "$ref": "#/$defs/JSONValue" + }, + { + "$ref": "#/$defs/Reference" + }, + { + "$ref": "#/$defs/Expression" + } + ] + }, + "OneValue": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/UIValue" + } + ], + "items": false, + "minItems": 1 + }, + "TwoValues": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/UIValue" + }, + { + "$ref": "#/$defs/UIValue" + } + ], + "items": false, + "minItems": 2 + }, + "ThreeValues": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/UIValue" + }, + { + "$ref": "#/$defs/UIValue" + }, + { + "$ref": "#/$defs/UIValue" + } + ], + "items": false, + "minItems": 3 + }, + "OneOrMoreValues": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/UIValue" + } + }, + "ZeroOrMoreValues": { + "type": "array", + "items": { + "$ref": "#/$defs/UIValue" + } + }, + "EventHandler": { + "type": "object", + "properties": { + "action": { + "$ref": "#/$defs/SafeName" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/$defs/ActionStep" + } + } + }, + "anyOf": [ + { + "required": [ + "action" + ] + }, + { + "required": [ + "steps" + ], + "properties": { + "steps": { + "minItems": 1 + } + } + } + ] + }, + "ActionStep": { + "type": "object", + "properties": { + "type": { + "const": "action" + }, + "action": { + "$ref": "#/$defs/SafeName" + }, + "input": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/UIValue" + } + } + }, + "required": [ + "type", + "action" + ] + }, + "Node": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/SafeName" + }, + "component": { + "$ref": "#/$defs/ComponentName" + }, + "layout": { + "$ref": "layout.schema.json" + }, + "props": { + "type": "object", + "maxProperties": 64, + "additionalProperties": { + "$ref": "#/$defs/UIValue" + } + }, + "events": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/EventHandler" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/Node" + } + }, + "slots": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/Node" + } + } + }, + "when": { + "$ref": "#/$defs/UIValue" + } + }, + "required": [ + "id", + "component" + ] + } + } +} diff --git a/schemas/view.schema.json b/schemas/view.schema.json new file mode 100644 index 0000000..5dd0d6d --- /dev/null +++ b/schemas/view.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "UI View", + "description": "A protocol 1.0 declarative view. Referential integrity and region-specific layout rules are additionally enforced by the SDK validators.", + "type": "object", + "properties": { + "protocolVersion": { "const": "1.0" }, + "id": { "$ref": "node.schema.json#/$defs/SafeName" }, + "revision": { "type": "string" }, + "title": { "type": "string" }, + "state": { "type": "object" }, + "sources": { + "type": "object", + "propertyNames": { "$ref": "node.schema.json#/$defs/SafeName" }, + "additionalProperties": { "$ref": "#/$defs/DataSource" } + }, + "actions": { "$ref": "actions.schema.json" }, + "regions": { + "type": "object", + "properties": { + "top-header": { "$ref": "#/$defs/NodeList" }, + "left-panel": { "$ref": "#/$defs/NodeList" }, + "content": { "$ref": "#/$defs/NodeList" }, + "right-panel": { "$ref": "#/$defs/NodeList" }, + "bottom": { "$ref": "#/$defs/NodeList" } + }, + "required": ["top-header", "left-panel", "content", "right-panel", "bottom"] + } + }, + "required": ["protocolVersion", "id", "regions"], + "$defs": { + "NodeList": { + "type": "array", + "items": { "$ref": "node.schema.json" } + }, + "DataSource": { + "type": "object", + "properties": { + "type": { "const": "tool" }, + "tool": { "$ref": "node.schema.json#/$defs/SafeName" }, + "input": { + "type": "object", + "additionalProperties": { "$ref": "node.schema.json#/$defs/UIValue" } + }, + "policy": { "enum": ["manual", "on-mount"] }, + "cache": { + "type": "object", + "properties": { "ttl": { "type": "integer", "minimum": 0 } } + }, + "refreshOn": { + "type": "array", + "items": { "$ref": "node.schema.json#/$defs/SafeName" } + } + }, + "required": ["type", "tool"] + } + } +} diff --git a/skills/go-ui-development/SKILL.md b/skills/go-ui-development/SKILL.md new file mode 100644 index 0000000..bdb77aa --- /dev/null +++ b/skills/go-ui-development/SKILL.md @@ -0,0 +1,32 @@ +--- +name: go-ui-development +description: Implement and integrate features in the go-ui declarative protocol, Go SDK, React SDK, and examples while keeping their behavior aligned. +--- + +# go-ui development + +Use this skill for development involving the `go-ui` protocol, Go or React SDKs, MCP adapter, transports, or examples. + +## Principles + +- Treat the wire contract below as shared by Go and TypeScript. Preserve parity in field names, enum values, reference resolution, validation, limits, and layout behavior. +- Keep the UI schema independent of transport. MCP resource publication and host RPC handling are separate concerns. +- A node's `layout` positions that node. Each node with children owns a new 12-column grid, so each child chooses its own `row`, `cols`, and `offset` within that immediate parent. +- Keep validation and safe value/path handling at the schema boundary. React component props and events must pass through registry allowlists; never forward arbitrary plugin props. +- For implementation work, inspect the existing corresponding Go and TypeScript behavior before changing the contract. Update schemas, fixtures, examples, and behavior checks together when applicable. +- Read [the protocol reference](references/protocol-map.md) for the complete model and [the examples reference](references/examples.md) for Go, React, and host usage snippets. + +## Development commands + +Run from the repository root. The workspace uses Go 1.26.8 and pnpm 12.6.0. + +- `make install`: install frozen pnpm dependencies, install `goppy@latest`, and run `goppy setup-lib`. +- `make lint`: run pnpm lint, rewrite files with `pnpm format`, check formatting, and run `goppy lint`. +- `make tests`: run pnpm tests and `goppy test`. In a clean checkout, run `make build` first because this target does not build npm packages. +- `make build`: build `@osspkg/ui-core`, `@osspkg/ui-transport`, and `@osspkg/ui-react` in order, run pnpm typecheck, and run `goppy build --arch=amd64`. +- `make license`: run `goppy license`. +- `make pre-commit`: run install, license, lint, build, and tests in sequence. There is no `make ci` target. +- `make examples-dev`: start the React example app. +- `pnpm --filter @osspkg/ui-core typecheck|test|build`, likewise `@osspkg/ui-transport` and `@osspkg/ui-react`, for focused package scripts. + +Inspect the worktree before running `make install`, `make lint`, or `make pre-commit`: setup, formatting, lint, and build workflows can update local, generated, or formatted files. `make npm-publish` runs the full build, then publishes all three packages publicly. Do not run publishing unless the user explicitly requests a release. diff --git a/skills/go-ui-development/agents/openai.yaml b/skills/go-ui-development/agents/openai.yaml new file mode 100644 index 0000000..6bc2844 --- /dev/null +++ b/skills/go-ui-development/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Go UI Development" + short_description: "Help with Go UI Development tasks" diff --git a/skills/go-ui-development/references/examples.md b/skills/go-ui-development/references/examples.md new file mode 100644 index 0000000..986ae80 --- /dev/null +++ b/skills/go-ui-development/references/examples.md @@ -0,0 +1,143 @@ +# go-ui code examples + +These examples are included here so the skill does not depend on documentation outside this directory. Confirm details against the current project API when using them after a protocol or SDK change. + +## Compose and validate a Go view + +```go +package profile + +import "go.osspkg.com/ui/go-sdk/ui" + +func NewApp() (*ui.App, error) { + app := ui.New( + ui.AppID("profile"), + ui.AppTitle("Profile"), + ui.AppVersion("1.0.0"), + ) + view := ui.View( + "profile.edit", + ui.Title("Edit profile"), + ui.Content( + ui.HTML(ui.HTMLArticle, "profile-card").Row(1).Cols(8).Offset(2).Children( + ui.HTML(ui.HTMLH1, "heading").Row(1).Cols(12).Prop("text", "Edit profile"), + ui.HTML(ui.HTMLInput, "name").Row(2).Cols(8).Offset(2).Prop("placeholder", "Ada Lovelace"), + ui.HTML(ui.HTMLButton, "save").Row(3).Cols(4).Offset(4).Prop("text", "Save"), + ), + ), + ) + if err := app.AddView(view); err != nil { + return nil, err + } + return app, nil +} +``` + +`App.AddView` validates the schema and stores a copy. Node builders support `.Row(n)`, `.Cols(n)`, `.Offset(n)`, `.Prop(key, value)`, `.Children(...)`, and `.On(event, ui.ActionRef(name))`. View options include `ui.Title`, `ui.State`, `ui.Source`, `ui.Content`, `ui.TopHeader`, `ui.LeftPanel`, `ui.RightPanel`, and `ui.Bottom`. + +A local state action can be declared as: + +```go +ui.View("profile.edit").Action( + "profile.submit", + ui.SetState("submitted", true).Then(ui.ToastSuccess("Profile saved")), +) +``` + +For a tool source, use `ui.Source("metrics", ui.Tool("metrics.summary").OnMount().TTL(30))`. Use the typed builders rather than hand-constructing action JSON when the builder supports the operation. + +## Register UI resources on an MCP server + +```go +import ( + goMCP "go.osspkg.com/mcp" + uiMCP "go.osspkg.com/ui/go-sdk/mcp" +) + +server, err := goMCP.New( + goMCP.ServerInfo{Name: "profile", Version: "1.0.0"}, + uiMCP.Capabilities(), +) +if err != nil { + return err +} +if err := uiMCP.Register(server, app); err != nil { + return err +} +``` + +Registration publishes the manifest and view resources. Implement `ui.get`, `data.call`, and `ui.action` in the host if the React runtime needs RPC calls. + +## Render a static schema in React + +```tsx +import { + createComponentRegistry, + registerDefaultHTMLComponents, + registerDefaultShadcnComponents, + UIProvider, + ViewRenderer, +} from "@osspkg/ui-react"; +import type { ViewSchema } from "@osspkg/ui-core"; +import "@osspkg/ui-react/styles.css"; + +const components = createComponentRegistry(); +registerDefaultHTMLComponents(components); +registerDefaultShadcnComponents(components); + +const schema: ViewSchema = { + protocolVersion: "1.0", + id: "welcome", + regions: { + "top-header": [], + "left-panel": [], + "right-panel": [], + bottom: [], + content: [ + { + id: "card", + component: "card", + layout: { row: 1, cols: 8, offset: 2 }, + children: [ + { id: "title", component: "h1", layout: { row: 1, cols: 12 }, props: { text: "Welcome" } }, + { id: "body", component: "p", layout: { row: 2, cols: 8, offset: 2 }, props: { text: "A schema rendered by React." } }, + ], + }, + ], + }, +}; + +export function WelcomeView() { + return ; +} +``` + +Default component registries provide dependency-free HTML and shadcn-name renderers. A host can register its own implementation first; the default registration functions leave existing registry entries intact. Each custom component definition declares allowed props, events, slots, and whether it accepts children. + +## Connect a host RPC transport + +```tsx +import { PluginView, UIProvider, createComponentRegistry, registerDefaultHTMLComponents, registerDefaultShadcnComponents } from "@osspkg/ui-react"; +import { HTTPTransport } from "@osspkg/ui-transport"; + +const components = createComponentRegistry(); +registerDefaultHTMLComponents(components); +registerDefaultShadcnComponents(components); +const transport = new HTTPTransport("/rpc"); + +export function RemoteProfile() { + return ( + + + + ); +} +``` + +`HTTPTransport` sends JSON-RPC calls to its endpoint. `WebSocketTransport` supports calls, subscriptions, and optional reconnect handling. Both implement `RPCTransport`. Close long-lived transports when their owning application lifecycle ends. + +The server must return `{ "schema": }` for `ui.get`, accept source requests for `data.call`, and handle declared tool actions for `ui.action`. React local actions such as `set-state` execute in the runtime; host calls are required for tool-backed behavior. + +## Reference project patterns + +For an HTML form, use component names such as `article`, `form`, `label`, `input`, and `button`, bind the form `submit` event to an action, and use `when` to conditionally show a result. For data dashboards, use a source with `on-mount`, bind displayed values to `$source` references, and refresh sources from action effects. In both cases, place children with their own layouts when they should not simply stack at default width. diff --git a/skills/go-ui-development/references/protocol-map.md b/skills/go-ui-development/references/protocol-map.md new file mode 100644 index 0000000..c28baca --- /dev/null +++ b/skills/go-ui-development/references/protocol-map.md @@ -0,0 +1,100 @@ +# go-ui protocol reference + +This file is a self-contained working reference for protocol 1.0. The protocol is JSON and independent of Go, React, and MCP. The Go module import path is `go.osspkg.com/ui`; SDK packages are `go.osspkg.com/ui/go-sdk/ui` and `go.osspkg.com/ui/go-sdk/mcp`. The npm packages are `@osspkg/ui-core`, `@osspkg/ui-transport`, and `@osspkg/ui-react`. + +## Object model + +### Manifest + +```json +{ + "protocolVersion": "1.0", + "plugin": { "id": "profile", "title": "Profile", "version": "1.0.0" }, + "views": [{ "id": "profile.edit", "title": "Edit profile", "schema": "ui://views/profile.edit" }], + "requiredComponents": ["card"] +} +``` + +`plugin.id`, `plugin.title`, `plugin.version`, and the `views` array are required. A view descriptor has `id`, `title`, and `schema`; `route` is optional. `requiredComponents` is optional. + +### View + +A view contains `protocolVersion`, `id`, and all five `regions`. Optional members are `revision`, `title`, `state`, `sources`, and `actions`. + +Region keys are exactly: + +- `top-header` +- `left-panel` +- `content` +- `right-panel` +- `bottom` + +Each region is an array of nodes. + +### Node and layout + +A node has `id` and `component`. Optional members are `layout`, `props`, `events`, `children`, `slots`, and `when`. + +```json +{ + "id": "profile-card", + "component": "card", + "layout": { "row": 1, "cols": 8, "offset": 2 }, + "children": [ + { "id": "heading", "component": "h1", "layout": { "row": 1, "cols": 12 }, "props": { "text": "Profile" } }, + { "id": "name", "component": "input", "layout": { "row": 2, "cols": 8, "offset": 2 }, "props": { "placeholder": "Ada Lovelace" } } + ] +} +``` + +A layout is a 12-column placement: `row` identifies its grid row; `cols` is the width; `offset` is the number of preceding columns. Children render in their parent's own grid. Nodes without a layout use the available row/width defaults. + +### Values and references + +`UIValue` is a JSON scalar, array, object, reference, or expression. A reference is a single-key object: + +- `{ "$state": "path" }` +- `{ "$context": "path" }` +- `{ "$source": "sourceName.path" }` +- `{ "$event": "path" }` +- `{ "$result": "path" }` + +Expressions use one operator key, for example `{ "$eq": [{ "$state": "saved" }, true] }` or `{ "$concat": ["Hello, ", { "$state": "name" }] }`. Preserve safe-path checks and rejection of prototype-sensitive keys such as `__proto__`, `prototype`, and `constructor`. + +### Sources, actions, and effects + +A source describes a host tool operation: + +```json +{ "type": "tool", "tool": "profile.read", "policy": "on-mount", "cache": { "ttl": 30 } } +``` + +`policy` is `manual` or `on-mount`; optional `input`, `cache.ttl`, and `refreshOn` configure its behavior. + +Action types: `tool`, `set-state`, `merge-state`, `refresh-source`, `invalidate`. Common fields include `tool`, `input`, `path`, `value`, `source`, and `effects`. + +Effect types: `set-state`, `merge-state`, `invalidate`, `refresh-source`, `refresh-view`, `patch-view`, `navigate`, `toast`, `dialog`, and `close-dialog`. Effects can use `path`, `source`, `value`, `to`, `variant`, and `message` depending on type. + +An event handler can name one `action` or a list of `steps`. Each step has `type: "action"`, an action name, and optional input. A view condition `when` takes a `UIValue` expression. + +## Package responsibilities + +- `@osspkg/ui-core`: protocol types, validation, value/expression resolution, request tracking, and RPC method/request types. +- `@osspkg/ui-transport`: `RPCTransport`, JSON-RPC primitives, `HTTPTransport`, and `WebSocketTransport`. +- `@osspkg/ui-react`: component registry, default HTML/shadcn component definitions, `UIProvider`, `PluginView`, `ViewRenderer`, hooks, and runtime actions/effects. +- Go `ui` package: protocol types, builders, validation, values, and shared RPC method names/types. +- Go `mcp` package: advertises `osspkg.ui` capability and registers `ui://manifest` plus each view schema as MCP resources. It does not implement the host's UI RPC handlers. + +Shared RPC method strings are `ui.get`, `data.call`, and `ui.action`. The host implements these methods for remote/runtime use. `PluginView` requests `{ plugin?, view, context? }` through `ui.get`; source and tool actions use `data.call` and `ui.action` respectively. Do not assume registering MCP resources creates these RPC handlers. + +## Compatibility checklist + +For a wire or runtime behavior change, review all applicable layers: + +1. Go protocol structs, builders, validation, and JSON serialization. +2. TypeScript types, validation, resolution, and React runtime/rendering. +3. JSON Schemas for manifest, view, node, layout, actions, and effects. +4. Valid/invalid fixtures and relevant Go/TypeScript tests. +5. Go and React examples, including nested layout placement. + +JSON Schema validation covers structural constraints; SDK validators also enforce semantic checks such as references, safe paths, supported actions, and limits. Keep both layers consistent. diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..2e66530 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "isolatedModules": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "skipLibCheck": true + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..afed279 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; +import { fileURLToPath } from "node:url"; + +export default defineConfig({ + root: fileURLToPath(new URL(".", import.meta.url)), + test: { + include: ["react-sdk/*/src/**/*.test.ts", "react-sdk/*/src/**/*.test.tsx"], + environment: "node", + }, +});