diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e30ed64 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "enabledPlugins": { + "svelte@svelte": true, + "figma@claude-plugins-official": true + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "if": "Bash(npm *)", + "type": "command", + "command": "echo '{ \"hookSpecificOutput\": { \"hookEventName\": \"PreToolUse\", \"permissionDecision\": \"deny\", \"permissionDecisionReason\": \"Use pnpm instead\" } }'" + }, + { + "if": "Bash(npx *)", + "type": "command", + "command": "echo '{ \"hookSpecificOutput\": { \"hookEventName\": \"PreToolUse\", \"permissionDecision\": \"deny\", \"permissionDecisionReason\": \"Use pnpm dlx instead\" } }'" + } + ] + } + ] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/actions/setup-env/action.yaml b/.github/actions/setup-env/action.yaml new file mode 100644 index 0000000..3bce496 --- /dev/null +++ b/.github/actions/setup-env/action.yaml @@ -0,0 +1,23 @@ +name: Setup Environment +description: Sets up development environment. + +# Required Permissions: +# - contents: read + +runs: + using: 'composite' + + steps: + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + cache: true + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version-file: 'package.json' + + - name: Install dependencies + run: pnpm install + shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..adea0fc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,36 @@ +version: 2 + +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' + commit-message: + prefix: 'ci' + open-pull-requests-limit: 2 + groups: + security: + applies-to: security-updates + patterns: + - '*' + version: + applies-to: version-updates + patterns: + - '*' + - package-ecosystem: 'npm' + directory: '/' + versioning-strategy: 'increase' + schedule: + interval: 'weekly' + open-pull-requests-limit: 0 + commit-message: + prefix: 'build(deps)' + groups: + security: + applies-to: security-updates + patterns: + - '*' + version: + applies-to: version-updates + patterns: + - '*' diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml new file mode 100644 index 0000000..bd56f09 --- /dev/null +++ b/.github/workflows/chromatic.yml @@ -0,0 +1,24 @@ +name: 'Chromatic' + +on: push + +jobs: + chromatic: + name: Run Chromatic + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Environment + uses: ./.github/actions/setup-env + + - name: Run Chromatic + uses: chromaui/action@latest + with: + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + autoAcceptChanges: 'main' + exitOnceUploaded: true + skip: 'production' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c66307a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: 'Test' + +on: pull_request + +permissions: + contents: read + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Environment + uses: ./.github/actions/setup-env + + - parallel: + - name: Svelte Check + run: pnpm run check --fail-on-warnings + + - name: ESLint + run: pnpm run lint:eslint + + - name: Prettier + run: pnpm run lint:prettier + + - name: Vitest + run: pnpm exec playwright install --with-deps && pnpm run test --project client --project server + + - name: Commit Message + run: pnpm exec commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbbb584 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build +/static/assets/fonts +worker-configuration.d.ts +*.tsbuildinfo + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +*storybook.log +storybook-static +chromatic.log +chromatic-build-*.xml +chromatic-diagnostics.json + +.claude/settings.local.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f78dcf3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "useTabs": true, + "singleQuote": true, + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/.storybook/PaddingDecorator.svelte b/.storybook/PaddingDecorator.svelte new file mode 100644 index 0000000..bbf414f --- /dev/null +++ b/.storybook/PaddingDecorator.svelte @@ -0,0 +1,19 @@ + + +
+ {@render children?.()} +
+ + diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..6b48685 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,15 @@ +import type { StorybookConfig } from '@storybook/sveltekit'; + +const config = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'], + addons: [ + '@storybook/addon-svelte-csf', + '@chromatic-com/storybook', + '@storybook/addon-vitest', + '@storybook/addon-a11y', + '@storybook/addon-docs', + 'storybook-addon-pseudo-states', + ], + framework: '@storybook/sveltekit', +} satisfies StorybookConfig; +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..17ef383 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,28 @@ +import type { Preview } from '@storybook/sveltekit'; + +import 'unfonts.css'; +import 'virtual:uno.css'; + +const preview: Preview = { + tags: ['autodocs'], + + parameters: { + layout: 'centered', + + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + + a11y: { + // 'todo' - show a11y violations in the test UI only + // 'error' - fail CI on a11y violations + // 'off' - skip a11y checks entirely + test: 'todo', + }, + }, +}; + +export default preview; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..b984b30 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "svelte.svelte-vscode", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "antfu.unocss" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..286067d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "svelte.plugin.svelte.defaultScriptLanguage": "ts", + "[svelte]": { + "editor.defaultFormatter": "svelte.svelte-vscode" + }, + "debug.javascript.terminalOptions": { + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "!**/node_modules/**", + "**/node_modules/.vite-temp/**" + ] + }, + "js/ts.experimental.useTsgo": false +} diff --git a/@types/.gitkeep b/@types/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa23bb0 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project +npx sv create my-app +``` + +To recreate this project with the same configuration: + +```sh +# recreate this project +pnpm dlx sv@0.16.1 create --template minimal --types ts --add prettier eslint vitest="usages:unit,component" sveltekit-adapter="adapter:cloudflare+cfTarget:workers" storybook --install pnpm summer-summit +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/chromatic.config.json b/chromatic.config.json new file mode 100644 index 0000000..7ddfc8c --- /dev/null +++ b/chromatic.config.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://www.chromatic.com/config-file.schema.json", + "onlyChanged": true, + "projectId": "Project:6a32172276e4b7b57bdf6b4e", + "zip": true +} diff --git a/commitlint.config.ts b/commitlint.config.ts new file mode 100644 index 0000000..9338159 --- /dev/null +++ b/commitlint.config.ts @@ -0,0 +1,5 @@ +import type { UserConfig } from '@commitlint/types'; + +const config: UserConfig = { extends: ['@commitlint/config-conventional'] }; + +export default config; diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..0a7f441 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,245 @@ +import path from 'node:path'; + +import { defineConfig, includeIgnoreFile, type Config } from 'eslint/config'; +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; +import perfectionist, { type SortImportsOptions } from 'eslint-plugin-perfectionist'; +import { Alphabet } from 'eslint-plugin-perfectionist/alphabet'; +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from 'eslint-plugin-storybook'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import stylistic from '@stylistic/eslint-plugin'; +import type { UndefinedOnPartialDeep } from 'type-fest'; +import ts, { type CompatibleConfigArray } from 'typescript-eslint'; +import unocss from '@unocss/eslint-config/flat'; + +import svelteConfig from './svelte.config.ts'; + +const gitignorePath = path.resolve(import.meta.dirname, '.gitignore'); +const extraFileExtensions = ['.svelte']; + +// sort subpaths before hyphenated packages +const alphabet = Alphabet.generateRecommendedAlphabet() + .sortByNaturalSort() + .placeCharacterBefore({ characterBefore: '/', characterAfter: '-' }) + .placeCharacterBefore({ characterBefore: '.', characterAfter: '/' }) + .getCharacters(); + +function importSortRule(hoistedGroups?: string[], files?: Config['files']) { + type GroupNames = NonNullable; + + const result = { + ...(files !== undefined && { files }), + rules: { + 'perfectionist/sort-imports': [ + 'warn', + { + type: 'custom', + alphabet, + order: 'asc', + specialCharacters: 'trim', + fallbackSort: { type: 'natural', order: 'asc' }, + tsconfig: { + rootDir: '.', + }, + newlinesBetween: 0, + // mirrors `svelte.config.ts` kit alias and default sveltekit aliases + internalPattern: ['^\\$lib(/.*)?$', '^\\$app/.*', '^\\$env/.*', '^\\$storybook(/.*)?$'], + customGroups: [ + { groupName: 'svelte', elementNamePattern: '^svelte(/.*)?|^@sveltejs/kit$' }, + { groupName: 'storybook', elementNamePattern: '^@storybook/.*$' }, + ], + groups: [ + 'builtin', + { newlinesBetween: 1 }, + ...((hoistedGroups as GroupNames | undefined)?.concat([{ newlinesBetween: 1 }]) ?? []), + 'external', + { newlinesBetween: 1 }, + ['internal', 'tsconfig-path'], + { newlinesBetween: 1 }, + 'parent', + 'sibling', + 'index', + { newlinesBetween: 1 }, + 'side-effect', + 'unknown', + ], + }, + ], + }, + } as const satisfies UndefinedOnPartialDeep; + + return result as Config; +} + +function stripFileFilter(...configs: CompatibleConfigArray[]): Config[] { + // Split each config's `rules` into a `files`-less object so they apply to every + // file, while the original keeps its plugin/parser registration scoped as-is. + return configs.flat().flatMap(({ rules, ...config }) => (rules ? [config, { rules }] : [config])); +} + +export default defineConfig( + includeIgnoreFile(gitignorePath, { + gitignoreResolution: true, + }), + js.configs.recommended, + stripFileFilter(ts.configs.strictTypeChecked, ts.configs.stylisticTypeChecked), + svelte.configs.recommended, + unocss, + storybook.configs['flat/recommended'] as unknown as Config[], + prettier, + svelte.configs.prettier, + importSortRule(['svelte'], ['src/**/*', '@types/**/*']), + importSortRule(['storybook', 'svelte'], ['src/**/*.stories.svelte']), + { + plugins: { + '@stylistic': stylistic, + perfectionist, + }, + languageOptions: { + globals: { ...globals.browser, ...globals.node }, + parserOptions: { + projectService: true, + extraFileExtensions, + }, + }, + linterOptions: { + reportUnusedDisableDirectives: 'error', + reportUnusedInlineConfigs: 'error', + }, + rules: { + // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + 'no-undef': 'off', + }, + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + parser: ts.parser, + projectService: true, + svelteConfig: svelteConfig({ mode: 'development' }), + extraFileExtensions, + }, + }, + rules: { + 'prefer-const': 'off', // covered by svelte plugin + 'svelte/no-bind-value-on-checkable-inputs': 'error', + 'svelte/no-conflicting-module-names': 'error', + }, + }, + { + // Override or add rule settings here, such as: + // 'svelte/button-has-type': 'error' + rules: { + // --- Possible Problems --- + 'array-callback-return': ['error', { allowImplicit: true }], + 'no-constructor-return': 'error', + 'no-promise-executor-return': ['error', { allowVoid: true }], + 'no-self-compare': 'error', + 'no-unmodified-loop-condition': 'error', + 'no-unreachable-loop': 'error', + 'require-atomic-updates': 'error', + 'no-implicit-coercion': 'error', + + // --- Security / correctness --- + eqeqeq: ['error', 'always', { null: 'ignore' }], + 'no-new-wrappers': 'error', + 'no-object-constructor': 'error', + 'no-return-assign': ['error', 'always'], + 'no-sequences': 'error', + 'no-new': 'error', + 'no-octal-escape': 'error', + 'no-proto': 'error', + 'no-iterator': 'error', + 'no-caller': 'error', + 'no-extend-native': 'error', + 'no-multi-str': 'error', + 'no-div-regex': 'error', + 'no-label-var': 'error', + 'no-extra-label': 'error', + 'no-labels': 'error', // safe in runes mode (no `$:`) + 'guard-for-in': 'error', + radix: 'error', + 'symbol-description': 'error', + yoda: 'error', + 'no-undef-init': 'error', + + // --- Modernization / cleanup --- + 'object-shorthand': ['error', 'always'], + 'prefer-template': 'error', + 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], + 'prefer-object-has-own': 'error', + 'prefer-object-spread': 'error', + 'prefer-exponentiation-operator': 'error', + 'prefer-numeric-literals': 'error', + 'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }], + 'logical-assignment-operators': ['error', 'always'], + 'operator-assignment': ['error', 'always'], + 'no-useless-call': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-unneeded-ternary': 'error', + 'no-else-return': ['error', { allowElseIf: false }], + 'no-extra-bind': 'error', + 'no-multi-assign': 'error', + 'default-case-last': 'error', + 'grouped-accessor-pairs': 'error', + 'func-name-matching': 'error', + + '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }], + '@typescript-eslint/consistent-type-exports': [ + 'error', + { fixMixedExportsWithInlineTypeSpecifier: true }, + ], + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/switch-exhaustiveness-check': [ + 'error', + { considerDefaultExhaustiveForUnions: true }, + ], + '@typescript-eslint/strict-void-return': 'error', + '@typescript-eslint/require-array-sort-compare': ['error', { ignoreStringArrays: true }], + '@typescript-eslint/method-signature-style': ['error', 'property'], + '@typescript-eslint/prefer-readonly': 'error', + '@typescript-eslint/no-unnecessary-parameter-property-assignment': 'error', + '@typescript-eslint/no-unnecessary-qualifier': 'error', + '@typescript-eslint/no-useless-empty-export': 'error', + '@typescript-eslint/no-unused-private-class-members': 'error', + '@typescript-eslint/class-methods-use-this': ['error', { ignoreOverrideMethods: true }], + '@typescript-eslint/strict-boolean-expressions': [ + 'error', + { + allowString: false, + allowNumber: false, + allowNullableBoolean: true, + }, + ], + '@typescript-eslint/restrict-template-expressions': [ + 'error', + { + allowNumber: true, + }, + ], + + '@stylistic/quotes': ['error', 'single', { avoidEscape: true, ignoreStringLiterals: true }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + enableAutofixRemoval: { imports: true }, + }, + ], + '@typescript-eslint/no-unnecessary-condition': [ + 'error', + { + checkTypePredicates: true, + }, + ], + }, + }, +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..efe7e1e --- /dev/null +++ b/package.json @@ -0,0 +1,101 @@ +{ + "name": "summer-summit", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "prebuild": "node ./tools/generate-puddle-snapshots.ts && node ./tools/clean-build.ts", + "build": "node -e \"if (process.env.WRANGLER_COMMAND !== 'types'){process.exit(1)} \" || vite build", + "preview": "wrangler dev .svelte-kit/cloudflare/_worker.js --port 4173", + "prepare": "pnpm run gen || echo ''", + "precheck:wrangler": "pnpm run /^gen:wrangler$/", + "precheck:svelte": "pnpm run /^gen:svelte-kit$/", + "check:app": "svelte-check --tsconfig ./tsconfig.app.json", + "check:tools": "svelte-check --tsconfig ./tsconfig.tools.json", + "check": "pnpm run /^check:.*/", + "watch:svelte": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.app.json --watch", + "lint:prettier": "prettier --check .", + "lint:eslint": "eslint --max-warnings=0 .", + "lint:commit": "node ./tools/lint-commit-msg.ts --verbose", + "lint": "pnpm run /^lint:.*/", + "format": "prettier --write .", + "test": "vitest", + "gen:wrangler": "wrangler types ./@types/worker-configuration.d.ts", + "gen:svelte-kit": "svelte-kit sync", + "gen": "pnpm run /^gen:.*/", + "storybook": "storybook dev --no-open -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "@fontsource/redaction-35": "^5.3.0", + "@fontsource/redaction-50": "^5.3.0", + "@unocss/preset-wind4": "^66.7.5", + "@unocss/reset": "^66.7.5", + "bits-ui": "^2.18.1", + "culori": "^4.0.2", + "runed": "^0.37.1", + "twgl.js": "^7.0.0" + }, + "devDependencies": { + "@chromatic-com/storybook": "^5.2.1", + "@commitlint/cli": "^21.2.1", + "@commitlint/config-conventional": "^21.2.0", + "@commitlint/types": "^21.2.0", + "@eslint/js": "^10.0.1", + "@iconify-json/mdi": "^1.2.3", + "@iconify-json/pixel": "^1.2.2", + "@iconify-json/pixelarticons": "^1.2.7", + "@storybook/addon-a11y": "^10.5.4", + "@storybook/addon-docs": "^10.5.4", + "@storybook/addon-svelte-csf": "^5.1.2", + "@storybook/addon-vitest": "^10.5.4", + "@storybook/sveltekit": "^10.5.4", + "@stylistic/eslint-plugin": "^5.10.0", + "@sveltejs/adapter-cloudflare": "^7.2.9", + "@sveltejs/enhanced-img": "^0.11.0", + "@sveltejs/kit": "^2.70.1", + "@sveltejs/mcp": "^0.1.25", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@types/culori": "^4.0.1", + "@types/node": "^24.13.3", + "@unocss/eslint-config": "^66.7.5", + "@unocss/extractor-svelte": "^66.7.5", + "@unocss/preset-web-fonts": "^66.7.5", + "@unocss/svelte-scoped": "^66.7.5", + "@vitest/browser-playwright": "^4.1.10", + "@vitest/coverage-v8": "^4.1.10", + "chromatic": "^18.1.0", + "eslint": "^10.8.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-perfectionist": "^5.10.0", + "eslint-plugin-storybook": "^10.5.4", + "eslint-plugin-svelte": "^3.22.0", + "globals": "^17.8.0", + "playwright": "^1.62.0", + "prettier": "^3.9.6", + "prettier-plugin-svelte": "^4.1.1", + "storybook": "^10.5.4", + "storybook-addon-pseudo-states": "^10.5.4", + "svelte": "^5.56.8", + "svelte-check": "^4.7.3", + "type-fest": "^5.8.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.65.0", + "unocss": "^66.7.5", + "unplugin-fonts": "^2.0.0", + "vite": "^8.1.5", + "vite-plugin-devtools-json": "^1.1.0", + "vitest": "^4.1.10", + "vitest-browser-svelte": "^3.0.0", + "wrangler": "^4.114.0" + }, + "packageManager": "pnpm@11.11.0+sha512.4463f65fd80ed80d69bc1d4bf163ee94f605c7380fc318bb5b2ebe15f8cd12d49c51a4d59e951b401e764d3b6ca751cbf51bc50ed7001a6bcb4935e684c34882", + "devEngines": { + "runtime": { + "name": "node", + "version": "^24.18.0", + "onFail": "download" + } + } +} diff --git a/patches/unplugin-fonts@2.0.0.patch b/patches/unplugin-fonts@2.0.0.patch new file mode 100644 index 0000000..02d7905 --- /dev/null +++ b/patches/unplugin-fonts@2.0.0.patch @@ -0,0 +1,43 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +index c9c2808405c2f3f41c21910df6c13fd42ed69701..af6a07cde8104f2c26609e47bb65a795087e1aaf 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -3,7 +3,7 @@ import { customVirtualModule, resolveFontFiles, resolveUserOption } from "./load + import { collectFallbackNames, generateAllFallbacks, transformFontFamilyDeclarations } from "./loaders/fallback.mjs"; + import { fontsourceImports, fontsourceVirtualModule } from "./loaders/fontsource.mjs"; + import MagicString from "magic-string"; +-import { basename, extname, join } from "pathe"; ++import { basename, dirname, extname, join, relative } from "pathe"; + import { createUnplugin } from "unplugin"; + //#region src/index.ts + const virtualStylesId = "unfonts.css"; +@@ -11,6 +11,20 @@ const resolvedVirtualStylesId = `\0${virtualStylesId}`; + const virtualModuleId = "unplugin-fonts/head"; + const resolvedVirtualModuleId = `\0${virtualModuleId}`; + const fontFileRegex = /\.(?:woff2?|ttf|eot|otf)(?:\?.*)?$/i; ++const cssUrlRegex = /url\((\s*)(?:(["'])(.*?)\2|([^'")]*?))(\s*)\)/g; ++const absoluteUrlRegex = /^(?:[a-z][a-z\d+.-]*:|\/\/|\/|#)/i; ++function rebaseCssUrls(css, cssFileName, htmlPath) { ++ const htmlFileName = htmlPath.replace(/^\/+/, ""); ++ return css.replace(cssUrlRegex, (match, before, quote, quotedUrl, unquotedUrl, after) => { ++ const url = (quotedUrl ?? unquotedUrl).trim(); ++ if (!url || absoluteUrlRegex.test(url)) return match; ++ const parts = /^([^?#]*)([?#].*)?$/.exec(url); ++ if (!parts?.[1]) return match; ++ let rebasedUrl = relative(dirname(htmlFileName), join(dirname(cssFileName), parts[1])); ++ if (!rebasedUrl.startsWith(".")) rebasedUrl = `./${rebasedUrl}`; ++ return `url(${before}${quote ?? ""}${rebasedUrl}${parts[2] ?? ""}${quote ?? ""}${after})`; ++ }); ++} + var src_default = createUnplugin((userOptions) => { + const options = userOptions || {}; + let root; +@@ -101,7 +115,7 @@ var src_default = createUnplugin((userOptions) => { + const css = typeof info.source === "string" ? info.source : ""; + const matches = css.match(fontFaceRegex); + if (matches) { +- fontFaceRules.push(...matches); ++ fontFaceRules.push(...matches.map((rule) => rebaseCssUrls(rule, info.fileName, ctx.path))); + info.source = css.replace(fontFaceRegex, ""); + } + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6781a5f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7181 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + undici@>=7.0.0 <7.28.0: ^7.28.0 + undici@>=7.23.0 <7.28.0: ^7.28.0 + ws@>=8.0.0 <8.21.0: ^8.21.0 + +patchedDependencies: + unplugin-fonts@2.0.0: daf89586c1c46e1f69c3db9885a0abddd667d76a91ab3ff48091ec0b0b1c0706 + +importers: + + .: + dependencies: + '@fontsource/redaction-35': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource/redaction-50': + specifier: ^5.3.0 + version: 5.3.0 + '@unocss/preset-wind4': + specifier: ^66.7.5 + version: 66.7.5 + '@unocss/reset': + specifier: ^66.7.5 + version: 66.7.5 + bits-ui: + specifier: ^2.18.1 + version: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + culori: + specifier: ^4.0.2 + version: 4.0.2 + runed: + specifier: ^0.37.1 + version: 0.37.1(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + twgl.js: + specifier: ^7.0.0 + version: 7.0.0 + devDependencies: + '@chromatic-com/storybook': + specifier: ^5.2.1 + version: 5.2.1(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8)) + '@commitlint/cli': + specifier: ^21.2.1 + version: 21.2.1(@types/node@24.13.3)(conventional-commits-parser@7.1.0)(typescript@6.0.3) + '@commitlint/config-conventional': + specifier: ^21.2.0 + version: 21.2.0 + '@commitlint/types': + specifier: ^21.2.0 + version: 21.2.0 + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)) + '@iconify-json/mdi': + specifier: ^1.2.3 + version: 1.2.3 + '@iconify-json/pixel': + specifier: ^1.2.2 + version: 1.2.2 + '@iconify-json/pixelarticons': + specifier: ^1.2.7 + version: 1.2.7 + '@storybook/addon-a11y': + specifier: ^10.5.4 + version: 10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8)) + '@storybook/addon-docs': + specifier: ^10.5.4 + version: 10.5.4(@types/react@19.2.17)(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@storybook/addon-svelte-csf': + specifier: ^5.1.2 + version: 5.1.2(@storybook/svelte@10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0)))(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@storybook/addon-vitest': + specifier: ^10.5.4 + version: 10.5.4(@vitest/browser-playwright@4.1.10)(@vitest/browser@4.1.10)(@vitest/runner@4.1.10)(react@19.2.8)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vitest@4.1.10) + '@storybook/sveltekit': + specifier: ^10.5.4 + version: 10.5.4(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@stylistic/eslint-plugin': + specifier: ^5.10.0 + version: 5.10.0(eslint@10.8.0(jiti@2.7.0)) + '@sveltejs/adapter-cloudflare': + specifier: ^7.2.9 + version: 7.2.9(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(wrangler@4.114.0) + '@sveltejs/enhanced-img': + specifier: ^0.11.0 + version: 0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@sveltejs/kit': + specifier: ^2.70.1 + version: 2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@sveltejs/mcp': + specifier: ^0.1.25 + version: 0.1.25(jiti@2.7.0)(typescript@6.0.3) + '@sveltejs/vite-plugin-svelte': + specifier: ^7.2.0 + version: 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@types/culori': + specifier: ^4.0.1 + version: 4.0.1 + '@types/node': + specifier: ^24.13.3 + version: 24.13.3 + '@unocss/eslint-config': + specifier: ^66.7.5 + version: 66.7.5(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@unocss/extractor-svelte': + specifier: ^66.7.5 + version: 66.7.5 + '@unocss/preset-web-fonts': + specifier: ^66.7.5 + version: 66.7.5 + '@unocss/svelte-scoped': + specifier: ^66.7.5 + version: 66.7.5 + '@vitest/browser-playwright': + specifier: ^4.1.10 + version: 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))(vitest@4.1.10) + '@vitest/coverage-v8': + specifier: ^4.1.10 + version: 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) + chromatic: + specifier: ^18.1.0 + version: 18.1.0 + eslint: + specifier: ^10.8.0 + version: 10.8.0(jiti@2.7.0) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)) + eslint-plugin-perfectionist: + specifier: ^5.10.0 + version: 5.10.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-storybook: + specifier: ^10.5.4 + version: 10.5.4(eslint@10.8.0(jiti@2.7.0))(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(typescript@6.0.3) + eslint-plugin-svelte: + specifier: ^3.22.0 + version: 3.22.0(eslint@10.8.0(jiti@2.7.0))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + globals: + specifier: ^17.8.0 + version: 17.8.0 + node: + specifier: runtime:^24.18.0 + version: runtime:24.18.0 + playwright: + specifier: ^1.62.0 + version: 1.62.0 + prettier: + specifier: ^3.9.6 + version: 3.9.6 + prettier-plugin-svelte: + specifier: ^4.1.1 + version: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + storybook: + specifier: ^10.5.4 + version: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + storybook-addon-pseudo-states: + specifier: ^10.5.4 + version: 10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8)) + svelte: + specifier: ^5.56.8 + version: 5.56.8(@typescript-eslint/types@8.65.0) + svelte-check: + specifier: ^4.7.3 + version: 4.7.3(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3) + type-fest: + specifier: ^5.8.0 + version: 5.8.0 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: ^8.65.0 + version: 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + unocss: + specifier: ^66.7.5 + version: 66.7.5(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + unplugin-fonts: + specifier: ^2.0.0 + version: 2.0.0(patch_hash=daf89586c1c46e1f69c3db9885a0abddd667d76a91ab3ff48091ec0b0b1c0706)(esbuild@0.28.1)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + vite: + specifier: ^8.1.5 + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + vite-plugin-devtools-json: + specifier: ^1.1.0 + version: 1.1.0(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + vitest-browser-svelte: + specifier: ^3.0.0 + version: 3.0.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vitest@4.1.10) + wrangler: + specifier: ^4.114.0 + version: 4.114.0 + +packages: + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + + '@capsizecss/unpack@4.0.1': + resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} + engines: {node: '>=18'} + + '@chromatic-com/storybook@5.2.1': + resolution: {integrity: sha512-z6I7NJk/0VngA64y5TNYaB4Hc2X8+90n4op6lBt9PvWk5TmIlFLDqdX33rlrwbNRkkYijVgA/wO04rVYXi5Mlg==} + engines: {node: '>=20.0.0', yarn: '>=1.22.18'} + peerDependencies: + storybook: ^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260722.1': + resolution: {integrity: sha512-vZOP8vIS3NwnuaO+gz0FZ7kIGeiO3bZmxV35Ph9zOXKSREhDFlH7wQ7mkCdhW3O4jnXsew+XT7b+DNEI2CcJGQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260722.1': + resolution: {integrity: sha512-EmIQymihDq6WNdER4+LF8Qn80yqayBUpJ+tkOO7wmY8pmgfyXjIUFNXotl21AHovTeu2seR7HdVUgeN/BilCWw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260722.1': + resolution: {integrity: sha512-jvZ3k9fxcnEn04s80CgIYxQfpOyAiz/8qC42DP8EBa9tR27qWyg9wmm31zIobVlrgBZn/+8NfdP73avRGcQOjQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260722.1': + resolution: {integrity: sha512-BOSB55SMNdy+DA5uj2WirgiNanpHGis5PVvXH1wSfvjRKr4JGgWK+EZzxz0RFUo6QjjQQC/NimEzNZ7va7jmKg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260722.1': + resolution: {integrity: sha512-sYM8YgUpKnRz2xjvdJLX1Ojzoi4MlA4gk8WTTExhGydjYB2UTs5NIbv0ZmpKgMoK9io3ixgmiW56ZnTbcWOdiA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260702.1': + resolution: {integrity: sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==} + + '@commitlint/cli@21.2.1': + resolution: {integrity: sha512-blsZGe29hJ72VGEFVl72IVYX+1vsfINpjA9yWQA6i7OKD/McGEOXg08sKIRKjFk4JvzhV/9n0l3i6NooPLTNfg==} + engines: {node: '>=22.12.0'} + hasBin: true + + '@commitlint/config-conventional@21.2.0': + resolution: {integrity: sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ==} + engines: {node: '>=22.12.0'} + + '@commitlint/config-validator@21.2.0': + resolution: {integrity: sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==} + engines: {node: '>=22.12.0'} + + '@commitlint/ensure@21.2.0': + resolution: {integrity: sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==} + engines: {node: '>=22.12.0'} + + '@commitlint/execute-rule@21.0.1': + resolution: {integrity: sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==} + engines: {node: '>=22.12.0'} + + '@commitlint/format@21.2.0': + resolution: {integrity: sha512-c4q64xaav2U83t7k7RyzJerBZurPer7FxUOY0RL5L/6CZijZ7K+s6HIBGIghj0ey1P2+seRX0J9XQYtDued6tg==} + engines: {node: '>=22.12.0'} + + '@commitlint/is-ignored@21.2.0': + resolution: {integrity: sha512-4/eB0vBN7L88O/oC4ajAEqi7j2ZfNgxl/+11RfAV9YosejZgDXhY2C9VcHnHJhOzPLoSy5P3Mg/46kqeyJfXKw==} + engines: {node: '>=22.12.0'} + + '@commitlint/lint@21.2.0': + resolution: {integrity: sha512-ceO5dp9pLjEZ6y6qbq/uXWXDPykqqlTsyzoQ0NzecpisSJhK3kTy9qzQoPeJuWG/IMNdV1lO0RgmzqoAlSi1uw==} + engines: {node: '>=22.12.0'} + + '@commitlint/load@21.2.0': + resolution: {integrity: sha512-RjlzWQqruRwIenJEfZtq7kG97co97nKoHpflE5YnF61tDLXxHPrdWImgzw6VL6MlFyaOcVlk74eBV8ZQmc3oIA==} + engines: {node: '>=22.12.0'} + + '@commitlint/message@21.2.0': + resolution: {integrity: sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==} + engines: {node: '>=22.12.0'} + + '@commitlint/parse@21.2.0': + resolution: {integrity: sha512-QHWxG4d0PLTF634/AdyZ0MQS+CLn5YOuJlCFhMMlSGKFxzYGUetkHBj18xgBD+6fVzUrA2lrCdi/vlS2f/oYXg==} + engines: {node: '>=22.12.0'} + + '@commitlint/read@21.2.1': + resolution: {integrity: sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==} + engines: {node: '>=22.12.0'} + + '@commitlint/resolve-extends@21.2.0': + resolution: {integrity: sha512-4O/1j51+79Wth9s/MGxt/5gs0XYLDgNlYpltQfhAvLE0itusLKs9zruxbiNg1oOkmkb9L9L4USYGjEj7n87NxA==} + engines: {node: '>=22.12.0'} + + '@commitlint/rules@21.2.0': + resolution: {integrity: sha512-C2yXMNpiB8ETZKfx5JD8+ExgF8vTU1VQMKPSUUYwqKpw9oJWQBrlXBpdU038mj2WPjof7o9UzFpmTyBeGMZwZg==} + engines: {node: '>=22.12.0'} + + '@commitlint/to-lines@21.0.1': + resolution: {integrity: sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==} + engines: {node: '>=22.12.0'} + + '@commitlint/top-level@21.2.0': + resolution: {integrity: sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==} + engines: {node: '>=22.12.0'} + + '@commitlint/types@21.2.0': + resolution: {integrity: sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==} + engines: {node: '>=22.12.0'} + + '@conventional-changelog/git-client@3.1.0': + resolution: {integrity: sha512-Tqa/gHco2WJWa740NRjOrfKVvzIqxkZpecb8bemaQ8sKM5PXb1UK4uTyTb/1wIqNuOVaDOFxyBdhTIQZn6gdjQ==} + engines: {node: '>=22'} + peerDependencies: + conventional-commits-filter: ^6.0.1 + conventional-commits-parser: ^7.0.1 + peerDependenciesMeta: + conventional-commits-filter: + optional: true + conventional-commits-parser: + optional: true + + '@conventional-changelog/template@1.2.1': + resolution: {integrity: sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==} + engines: {node: '>=22'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + 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/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@fontsource/redaction-35@5.3.0': + resolution: {integrity: sha512-c4n4fgTbYoiyODKvasBWjj1EzV9G+X2oYDgr1PPShIO3rzM50CYvlJ+VCpgBmNyw9l8D/6T1PUmn8ZGgzOlV3g==} + + '@fontsource/redaction-50@5.3.0': + resolution: {integrity: sha512-gB62yw+lqqRW21fZA/i+O4icjaXlKfdmrwzrJoyZzMJ5PJknBAdUBcKdF+Vhyw9T9vSoU7/hf5J12tjOvnT0+Q==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify-json/mdi@1.2.3': + resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==} + + '@iconify-json/pixel@1.2.2': + resolution: {integrity: sha512-33uIyTe0teB9ykpRNucSXveMFYb4Hd0lxSO0oShs44OEixTuQdQk7kZpZWMMHdjDTGORpLTuOrV26pFkoIYSng==} + + '@iconify-json/pixelarticons@1.2.7': + resolution: {integrity: sha512-81h/yovmRFsHC5xYsDPnZibiofVKU/97PaA5rR//8sfgTFOQ/r3M3nhGCQU3HLWv0C5knMrsp6CFXAZ2qv7O1w==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} + + '@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.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@neoconfetti/react@1.0.0': + resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==} + + '@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'} + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm-eabi@0.131.0': + resolution: {integrity: sha512-t2xicr9pfzkSRYx5aPqZqlLaayIwJTqgQ81Jor31Xep2nGyL2Aq3d0K5wOfeR7VevaSdxaS9dzSQP9xDwn8fDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-android-arm64@0.131.0': + resolution: {integrity: sha512-nlGIod6gw75x1aEDgLS+srj+JRGY0HHm9MI9YgzE/B64l6d6+H3MSP9NOgp0+HTg8tp4vV9rVfgQGgd+TfVZcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-arm64@0.131.0': + resolution: {integrity: sha512-jukuV6xe5RbQKFo7QD34NDCLDZp4PSOm8rmckhNdH/60ymG5zXbDzGBEyc+nTkuLQNama2aSGCt+CPfpjNTqyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.131.0': + resolution: {integrity: sha512-g3JOo4khe9rslHm5WYaVDWb0HS/M1MLR3I9S8560MkKIcC96VQY00QjOlsuRyfSj/JDXj8i9T7ryPO2RidiXVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-freebsd-x64@0.131.0': + resolution: {integrity: sha512-1hziITDTxjMePnX+dR9ocVT+EuZkQ8wm4FPAbmbEiKG+Phbo73J1ZnPAA6Y/aGsWF3McOFnQuZIktAFwalkfJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.131.0': + resolution: {integrity: sha512-9uRxfXwyKG9+MwmGQBo2ncPNwZH5HTmCETFM2WiuDBNDCW4NC5ttSQkwCAMrTAWgwMzVBH1CP8pM0v7nebCWXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.131.0': + resolution: {integrity: sha512-mgbLvzRShXOLBdWGInf08Af4q+pfj1xD8hSgLClDZ9of/BXkB6+LIhTH7fihiDUipqB3yoSkKBWaZ3Ejlf5Yag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-gnu@0.131.0': + resolution: {integrity: sha512-OPT8++4aN6j2GJ8+3IZHS/byXoZP4aSBn+FoG6rgBJ2fKwPKXWF3MqrFMNW7NKHM28FLY579xYLxJSfgobEqPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-arm64-musl@0.131.0': + resolution: {integrity: sha512-vtPiwmfVTAXzaxDKsOXG+LwgRAA7WEnaeHzhS5z0GE89gAK18KSXnly7Z6saXXq6L3dVMyK44uoTI03zKxrpmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-ppc64-gnu@0.131.0': + resolution: {integrity: sha512-8AW8L7w5cGHSdZPcyZX2yR0+GUODsT15rbRjfdD54rv6DMbtuEB19ysLOpKJlRGfH6UNYNpCHaU1uJWgTWf1/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.131.0': + resolution: {integrity: sha512-vvpjkjEOUsPcsYf8evE4MO3aGx9+3wodXEBOicGNnOwTuAik8eBONNkgSdhkGsAblQmfVHJyanRnpxglddTXIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-riscv64-musl@0.131.0': + resolution: {integrity: sha512-AqmcNC3fClXX+fxQ6VGEN1667xVFiRBkY0CZmDMSiaeFUsv1+UkBPYYi48IUKcA9/ivvoKNRzQl2I4//kT9F/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-s390x-gnu@0.131.0': + resolution: {integrity: sha512-7d3jOMKy7RSQCcDLIci+ySll2FgsOMl/GiRux4q2JNv0zg4EdhFISa9idvrdN/HEUIQQJNg6dmveUeJl2YErGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.131.0': + resolution: {integrity: sha512-JHK/h95qVqVQ+ITER837kcTdwBDFpFaNnOTYGCP0zdUSX/mLKC7tXOoyrTb6vG7iRPwGlcgBil3v2IjYw1FqJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-x64-musl@0.131.0': + resolution: {integrity: sha512-b2BO82O8azXAyf7EUgOPKu145nWypbNyk07HbU09fkzhm9lEA5oPvaN/M8Nlo7tOErVTa2WOgS4QbOnxAPXdDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-openharmony-arm64@0.131.0': + resolution: {integrity: sha512-GHO9glZaX7LkX/OGfluEPf1yjg+ehiFbUdowbX6uNWOQhmwKWU4m4+nZ9FJkrHNKuxyI1KKertMdGjVKCApKWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-wasm32-wasi@0.131.0': + resolution: {integrity: sha512-3SkikPaEFoih1N83qLVEDLRLeY4nYsf6JT9SnWiMCQ5lGQdKup6bEuKCqkRiG9dD1IIaFeYz9RjlciPmYoFIWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-arm64-msvc@0.131.0': + resolution: {integrity: sha512-Os5bEhryeA2jkH+ZrnZyAC1EP5gs+X4YB1Fjqml7UPD5kU7ecsK1MPEVMfCrdt/GDNpDbavYXiOXOdyJ5b3OPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.131.0': + resolution: {integrity: sha512-m+jNz9EuF0NXoiptc6B9h5yompZQVW/a5MJeOu5zojfH5yWk82tvF2ccrHkfhgtrS9h9DD5l1Qv8dWlfY7Nz8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.131.0': + resolution: {integrity: sha512-o14Hk8dAyiEUMFEWEgmAwFZvBt1RzAYLM3xeQ+5315JXgVYhoemivgYcbYVRbsFkS71ShMGlAFE0kPnr460rww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-project/types@0.131.0': + resolution: {integrity: sha512-PgnWDfV0h+b16XNKbXU7Daib/BFSt/J2mEzfYIBu6JB/wNdlU+kVYXCkGA1A9fWkTbOgbjh4e6NhPeQOYvFhEA==} + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + cpu: [x64] + os: [win32] + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@simple-libs/child-process-utils@2.0.0': + resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==} + engines: {node: '>=22'} + + '@simple-libs/stream-utils@2.0.0': + resolution: {integrity: sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==} + engines: {node: '>=22'} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.17': + resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@storybook/addon-a11y@10.5.4': + resolution: {integrity: sha512-UWdZtB7Dh1GjqxTPOrisUNDhDGF5pKGzZdzW9DSSHMBcAOx2dsTmXGzLSFprAz4LTT0OHCtKhxNqKK0JZJ0Y8g==} + peerDependencies: + storybook: ^10.5.4 + + '@storybook/addon-docs@10.5.4': + resolution: {integrity: sha512-2Z/x2pKEmXOCQjmttYzPuQBu9aWeMly8uEs3msrCTBLiHs/F7IlBFnMu0Z+T2Qvk0LEy8O93AlcPSP76aCcKjw==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.4 + peerDependenciesMeta: + '@types/react': + optional: true + + '@storybook/addon-svelte-csf@5.1.2': + resolution: {integrity: sha512-NpImknEb48J7yr/ArTYpvhDSvGUrgm5Nuybu9PCicjSKTACsXX7cln2R19572ORtns399RTE+t20BBOKxSPm2g==} + peerDependencies: + '@storybook/svelte': ^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 + '@sveltejs/vite-plugin-svelte': ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + storybook: ^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 + svelte: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/addon-vitest@10.5.4': + resolution: {integrity: sha512-5lr81sgrbK3Tjjbstieg/yYj3jdtQYP70ihpOiN1TwogvFATY2/9eMT46FE5ioKtEFcfJRDcgJ2wSlmJKE+kZw==} + peerDependencies: + '@vitest/browser': ^3.0.0 || ^4.0.0 + '@vitest/browser-playwright': ^4.0.0 + '@vitest/runner': ^3.0.0 || ^4.0.0 + storybook: ^10.5.4 + vitest: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/runner': + optional: true + vitest: + optional: true + + '@storybook/builder-vite@10.5.4': + resolution: {integrity: sha512-eXdgow+brSzZrG3CnG18YwxZwEYNAZIh2G5qKwNoZf0uk2ZCPgLRQwtVAcd7BiiEDGnXUHm9pycAS+UiF4F4Mg==} + peerDependencies: + storybook: ^10.5.4 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/csf-plugin@10.5.4': + resolution: {integrity: sha512-DSp5Z/eZlRnKq0KrKLJE6uoYf/Ysc+FP0Z5DVTGnOrie+z3tC0lNi9I4RB++EXkJeUDS9/4dxvJZVSWjhLlXxw==} + peerDependencies: + esbuild: '*' + rollup: '*' + storybook: ^10.5.4 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + '@storybook/csf@0.1.13': + resolution: {integrity: sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q==} + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@2.1.0': + resolution: {integrity: sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@storybook/react-dom-shim@10.5.4': + resolution: {integrity: sha512-YdlppEOReg8MvTECRNuf79gu2zL83JqKDHIR/65eS0M6y+ue9pkpfjYo7hZVIcyOcRd9npBDXMdt2kC92bCuaA==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.4 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@storybook/svelte-vite@10.5.4': + resolution: {integrity: sha512-XQj2vvfw40S1ohmgs5pPobbYGT76k8B90haDQinHJ+IdW8b8MEI7gI38gkSFQnE/q6TeR9iniix1Rll1QXP1dA==} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + storybook: ^10.5.4 + svelte: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/svelte@10.5.4': + resolution: {integrity: sha512-mPEDAxFxfKXLAr0KNl2IGyrMCPM+SP85q0KhXI2IQqdlJoSG6mk6L9l6255d21FD13IGndsKV95gyCKclLLUjQ==} + peerDependencies: + storybook: ^10.5.4 + svelte: ^5.0.0 + + '@storybook/sveltekit@10.5.4': + resolution: {integrity: sha512-5cfBHHVjw5JVy4wlIpk8D3RCpF8Q+Bf1xI1SI9wFst+b5YqtN/XBD7BA9CKjvWdWbDdDej4ZX3Di0qcMfRcQ0A==} + peerDependencies: + storybook: ^10.5.4 + svelte: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@stylistic/eslint-plugin@5.10.0': + resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + + '@sveltejs/acorn-typescript@1.0.11': + resolution: {integrity: sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-cloudflare@7.2.9': + resolution: {integrity: sha512-LEfLRYKZIiNYBPYk9Cu4dFCCIX37NO96jESNRDv7yr3vvblKU4Yh05FPUMRSrOWFKtG6bJcGIvdG/M2DSf4D1w==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + wrangler: ^4.0.0 + + '@sveltejs/enhanced-img@0.11.0': + resolution: {integrity: sha512-TN7VzGoqwFqvA4Faj1brUPHLtcEkyFa08nrn9bfm98yVrzr0g4bOZHjlBoPYHHX+sZNGE+cxznhTVgarECPQsA==} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0 || ^7.0.0 + svelte: ^5.0.0 + vite: ^6.3.0 || >=7.0.0 + + '@sveltejs/kit@2.70.1': + resolution: {integrity: sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 || ^6.0.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + + '@sveltejs/load-config@0.2.0': + resolution: {integrity: sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/mcp@0.1.25': + resolution: {integrity: sha512-gjXU22CAxs+HA/7hW8v/cuHZR7qOOQWQTVKA2BNeXivrojrYHRRT1OqDxms8rWcSdXKGC8MjOibFpshWd8KWig==} + hasBin: true + + '@sveltejs/vite-plugin-svelte@7.2.0': + resolution: {integrity: sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/svelte-core@1.1.3': + resolution: {integrity: sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g==} + engines: {node: '>=16'} + peerDependencies: + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/culori@4.0.1': + resolution: {integrity: sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdx@2.0.14': + resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.65.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + 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.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + 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.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + 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.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unocss/cli@66.7.5': + resolution: {integrity: sha512-fgWkECRn2LGVo9sEpmjk/KJ3NSKUDitaZCNrJcEYM93DG+ELriTHcL+VWBlF4rcZf9fdGlq1nKbbRHUZirFCHQ==} + hasBin: true + + '@unocss/config@66.7.5': + resolution: {integrity: sha512-dkPl9glhEahJ+Xoja5ZseKKnH+vZaeaKQzg8b0otcKcPPNrHQgu1nu3QgfeOjnXOGrjZIotHwUeVtt4ZA2Skgg==} + + '@unocss/core@66.7.5': + resolution: {integrity: sha512-UdJb8MiMywcau8QrWEVgUAz0kvoFHyR+sACwYCgmBh/BpKJGyR/zw/Ys3wvysbm0f+i/20VGBex/QQxYVlzdyQ==} + + '@unocss/eslint-config@66.7.5': + resolution: {integrity: sha512-uFy7R0Fmb881p9Arv+ZFmnSzjhL+7E26bIsY8LKX+ueKUvzCFcQ1+OQ6cuHfriau9lwZ4iG7T4sGW8oYayNOYQ==} + + '@unocss/eslint-plugin@66.7.5': + resolution: {integrity: sha512-VCOY8w/y+o/2Vm6QqDgKZmX7XUjU0y/22ScI6AU0CxYtKUjHukUFubKgvn7/iRHiJEhrgtpxF1kDLvkNzhsklg==} + + '@unocss/extractor-arbitrary-variants@66.7.5': + resolution: {integrity: sha512-5zOkbnLIJc8E749qNjLXfPHl3FdHloop12h706/ojr6idK4ix0KLm43R//uLnphixCehdHJRuHnavnM4C/kQbA==} + + '@unocss/extractor-svelte@66.7.5': + resolution: {integrity: sha512-9H6uQ5MvNIEJ6AshxPeCFkbCloDInEETq0OOVusd79vSi4SJKvWvLOnd7Wnnc24JrKt59qTnb9dJi1ZNAgkGUw==} + + '@unocss/inspector@66.7.5': + resolution: {integrity: sha512-WmTJMnj8bmRxvw/wGeShmL2eEHr2/L0BdGTXSxhfzwcO8y5XZEbBmVciLcM7pqaTXQ6JY4+KnITrDUf1urjZxQ==} + + '@unocss/preset-attributify@66.7.5': + resolution: {integrity: sha512-1Oi1Cp81pqYJwG3h4+OlP5A0FKyaa07MFBXaXc4Yr3faiTbsI8SKj2TqnZCb+NQvBsEqslEd+jKXGZ3lKzCVOQ==} + + '@unocss/preset-icons@66.7.5': + resolution: {integrity: sha512-ZsxadWnGGtHdANTikuMnjkQaT1qwUFJHZBF4fzVsPMnUiiKGs8rzXukwM9w3Gi9ontM7nbG2FYi9clWETSlpFg==} + + '@unocss/preset-mini@66.7.5': + resolution: {integrity: sha512-o37TSl4ecT0dKu+3/TYuTYht75h82SEwDNl476m9ve0KW4Pv1O2tT/9TWd7N5cutEpySr8/YtjMwtWBD+drxBg==} + + '@unocss/preset-tagify@66.7.5': + resolution: {integrity: sha512-/8YB1tXVi+WmNKPhsCdtO1xnQKEkshSnWDp6AbvVP3f6qOF7adlMYpAt3kpWCoVUBsfOQ06ZQlnfricMjObyoQ==} + + '@unocss/preset-typography@66.7.5': + resolution: {integrity: sha512-2dxC8LT9KYa29UZT4muDFl7DbIXvKktTfeSMbUGMdZKbHcuMtKSP2U52wti1PL5I9duqp4v+8G33EeVutGTUaQ==} + + '@unocss/preset-uno@66.7.5': + resolution: {integrity: sha512-zaUlYgNngbt50fZA/LbtsEnmPKojPqeiXCyUUiKQMv2uJQhncR32xhLZXVQ+TJD7hlfZ+6FAqlco1NIiAcub9w==} + + '@unocss/preset-web-fonts@66.7.5': + resolution: {integrity: sha512-OLLTK7kswdSu51qxEm6O+AehecgCfS08Ivqo+280lKzFW7V1jXr5okeJ1ty+85oHCprJqzcD9iG1khxNRLomcA==} + + '@unocss/preset-wind3@66.7.5': + resolution: {integrity: sha512-atFe/7Qein+oMdyZs0hEo+3EjV99Av2LVxDbzpCungxIfbS3TnQt70EIuHXMPNCVWLYwrMV+/P1n9Ntb0E3mow==} + + '@unocss/preset-wind4@66.7.5': + resolution: {integrity: sha512-n3jIvQv8x1jXpmWfvNFBIqHNARki4mKZXfxAA31gekpXsRRTg16u1+yC1+PBBJgoEDFEnnmKnG7EZP88S8LVXA==} + + '@unocss/preset-wind@66.7.5': + resolution: {integrity: sha512-LVKgGr0A9Lc7F85xGXrrb71DDXMlHGA8bcj/Kht8BCsuRdBZadNbLlnv5qnSJiHYhi3/blzcgVoaeRor6L9yNA==} + + '@unocss/reset@66.7.5': + resolution: {integrity: sha512-z3wbt2cuQPjtT6w5xzRYZYFIIlUPzhVKz8yIcE9fvu7BgR3hO7uVsg/5mzDoGwQ96Ya3Sk68rpmI/gLYl4bJag==} + + '@unocss/rule-utils@66.7.5': + resolution: {integrity: sha512-/AKHBRF6ZOexE3EDDv8ZEYh40P5e2Eha41IYUbE1wjigW7++ssmvimSTdufJzZvU5DGP++E3B3WEsFPprZMjAg==} + + '@unocss/svelte-scoped@66.7.5': + resolution: {integrity: sha512-HVEs+DAP4PzdUsYEswGYj/FUwdJPQ6ddugvew/LKCXoy75tGHAvQwYz5xf8JRwGLUWtra8o/MClYaQC+xoIhew==} + + '@unocss/transformer-attributify-jsx@66.7.5': + resolution: {integrity: sha512-r8qDwNSt0eGSwWws3xsuIHb3PZYR2embFdYoE67hD/xlYgLjX1uPy/HUlGCSSh4uLc4vVYjurr0Oq5xF/B8YiA==} + + '@unocss/transformer-compile-class@66.7.5': + resolution: {integrity: sha512-KuECgsGF7tGe808vIvKViEjI0UCUgYgneJfK+/TWBkjC7s8dLVaUtJzzcP9/r6OfGlgyn/x1YTdRqTaCENa7Xg==} + + '@unocss/transformer-directives@66.7.5': + resolution: {integrity: sha512-VMJApXXOwlDubkW+cNMmOkfxLefFupJr+yU23gGYUB2NPsuVltc8H5CDM0gfVebpeL5CUW05evxzEAk2wsju+Q==} + + '@unocss/transformer-variant-group@66.7.5': + resolution: {integrity: sha512-iDmP3mM8J+IxuQf5Uh33zsi528xnGxTpKRJ0c+LCrqBTTkR2tZr4aCzNmJ0g29Js2yEOsyNXRRc8BNTuvgn0AA==} + + '@unocss/vite@66.7.5': + resolution: {integrity: sha512-1z1TBCNJCR2WzjPtjzmCHr8rtzXHosMjLdsCNe6Mzsfwiw044gzzLVuiEZO9vIjkI50lvQ+gIOxOiVQG0hGAeA==} + peerDependencies: + vite: ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 + + '@vitest/browser-playwright@4.1.10': + resolution: {integrity: sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==} + peerDependencies: + playwright: '*' + vitest: 4.1.10 + + '@vitest/browser@4.1.10': + resolution: {integrity: sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==} + peerDependencies: + vitest: 4.1.10 + + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + peerDependencies: + '@vitest/browser': 4.1.10 + vitest: 4.1.10 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + argue-cli@3.1.0: + resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==} + engines: {node: '>=22'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + ast-v8-to-istanbul@1.0.4: + resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} + + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + 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} + + bits-ui@2.18.1: + resolution: {integrity: sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==} + engines: {node: '>=20'} + peerDependencies: + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + brace-expansion@1.1.16: + resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + 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'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + 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'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chromatic@16.10.0: + resolution: {integrity: sha512-nFsztmnu7rFiGafUJgXvLUNpqmRylz92eNvzBoJNTKKQj4EQUyxznwnfpf1dTs7hXtWD8JwcH92jADydaHA1sw==} + hasBin: true + peerDependencies: + '@chromatic-com/cypress': ^0.*.* || ^1.0.0 + '@chromatic-com/playwright': ^0.*.* || ^1.0.0 + '@chromatic-com/vitest': ^0.*.* || ^1.0.0 + peerDependenciesMeta: + '@chromatic-com/cypress': + optional: true + '@chromatic-com/playwright': + optional: true + '@chromatic-com/vitest': + optional: true + + chromatic@18.1.0: + resolution: {integrity: sha512-I9av8lUc5CQRlYzwKpmOG9cwYvZ4AFmTvtHeNrzOMC1353F9xYw45CHpSNIsNKuOiqqmzci9esXQd5akl0fi6w==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@chromatic-com/cypress': ^0.*.* || ^1.0.0 + '@chromatic-com/playwright': ^0.*.* || ^1.0.0 + '@chromatic-com/vitest': ^0.*.* || ^1.0.0 + peerDependenciesMeta: + '@chromatic-com/cypress': + optional: true + '@chromatic-com/playwright': + optional: true + '@chromatic-com/vitest': + optional: true + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + 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==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + conventional-changelog-angular@9.2.1: + resolution: {integrity: sha512-oWSL6ZhnXbYraOFTK3PgRAQJ8fADDAEv5K6AdeyQPLvjFmhG8+ejL0jZZp/R7vTmGJaBvZEE+sE7dB4bCv7sAw==} + engines: {node: '>=22'} + + conventional-changelog-conventionalcommits@10.2.1: + resolution: {integrity: sha512-n4Kr1HFMTf3iMbES0TMxKIcYtUUv4rKqyQQp2JwfOEfFCOfGT3Tq4mCyJ8S9/YPyWhydjfKrrvnyl+gCjA+mJQ==} + engines: {node: '>=22'} + + conventional-commits-parser@7.1.0: + resolution: {integrity: sha512-DPp6hkUjvwIivxbkrTiLXeRswNv1A/4GFA2X6scXma0AMa9632V3TwxmrlkUIEtUktiM3Ln+RrSH2xlP3/jUTw==} + engines: {node: '>=22'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + culori@4.0.2: + resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + 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==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.8.2: + resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + es-toolkit@1.49.0: + resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-perfectionist@5.10.0: + resolution: {integrity: sha512-HiqpDrUDbGrMC6iHQbemgDyHJ0366Vyz/qRWmxQcSAkmG25cXr8BdRgx8yAhOKhEfBXn8Rnf/mTCsV4EqUJSxg==} + engines: {node: ^20.0.0 || >=22.0.0} + peerDependencies: + eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-storybook@10.5.4: + resolution: {integrity: sha512-IEG6GK9iMKH45WxiBUoDu3AajUyz+AcRXsLRkwnsNz9TwzZI6tbyHgdZ1Tkvs2RXOovYUYYksNpbtnmY746LAw==} + peerDependencies: + eslint: '>=8' + storybook: ^10.5.4 + + eslint-plugin-svelte@3.22.0: + resolution: {integrity: sha512-O3qn0NePTWta+1o25dIThqeEP/hEQ3VxDK2LVO8SQ5wG9umLMvulK+m1yQ4JGOb2Pkl8IB0G1lpRV/HXDXSLTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + 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@10.8.0: + resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrap@1.2.2: + resolution: {integrity: sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==} + + esrap@1.4.9: + resolution: {integrity: sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==} + + esrap@2.3.0: + resolution: {integrity: sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + 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@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + 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-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + 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@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.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@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + + fontaine@0.8.0: + resolution: {integrity: sha512-eek1GbzOdWIj9FyQH/emqW1aEdfC3lYRCHepzwlFCm5T77fBSRSyNRKE6/antF1/B1M+SfJXVRQTY9GAr7lnDg==} + engines: {node: '>=18.12.0'} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + global-directory@5.0.0: + resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} + engines: {node: '>=20'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globals@17.8.0: + resolution: {integrity: sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + imagetools-core@9.1.0: + resolution: {integrity: sha512-xQjs+2vrxLnAjCq+omuNkd5UQTld9/bP8+YT0LyYTlKfuSQtgUBvqhUwGugzSAh6sCdN+LnROMuLswn5hZ9Fhg==} + engines: {node: '>=20.0.0'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-rpc-2.0@1.7.1: + resolution: {integrity: sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + 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'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + 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==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-regexp@0.10.0: + resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + miniflare@4.20260722.0: + resolution: {integrity: sha512-LW6ABMhCx/yIEFBLC/DO4yAhdm2T/G7jp7pr5T2kj895+CCIaHZqpMXdW9O6YE48LcYcCJChwWc8aEs1vpbTXw==} + engines: {node: '>=22.0.0'} + hasBin: true + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node@runtime:24.18.0: + resolution: + type: variations + variants: + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-ZGOiNzn2sjAOvsXM1gJ14TjjhY/Br8bo+bxOT2pLdvo= + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-aix-ppc64.tar.gz + targets: + - cpu: ppc64 + os: aix + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-4al+FMmcgD6WxzOUAyguoFpJnDL42D3v6e9exm+XntE= + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-darwin-arm64.tar.gz + targets: + - cpu: arm64 + os: darwin + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-39Db0+chUDQ033tyBecZ9hs6OjGyvPlym4uR/qJA8IA= + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-darwin-x64.tar.gz + targets: + - cpu: x64 + os: darwin + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-a0SEwhkCdBdd+aqPKOLXWKgZyxwf5qtIHi+VtGOrhQg= + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-arm64.tar.gz + targets: + - cpu: arm64 + os: linux + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-/hM4ly95KDxrwh5h2/RXa76MBa3tKZnUHIZDrTAmUUI= + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-ppc64le.tar.gz + targets: + - cpu: ppc64le + os: linux + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-Nx68E5RfwWlJPnUvLdr6+rbs2My0Ubz/RuCfacXdjHo= + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-s390x.tar.gz + targets: + - cpu: s390x + os: linux + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-eDEwmElj23upy9AQierywu+wVcfBaTyUMXS5Z7MFDLg= + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-x64.tar.gz + targets: + - cpu: x64 + os: linux + - resolution: + archive: zip + bin: + node: node.exe + integrity: sha256-8nRmmtuTsf0Pv48h/QeGCencyEMz1PJxjS3eP5oWGgE= + prefix: node-v24.18.0-win-arm64 + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-win-arm64.zip + targets: + - cpu: arm64 + os: win32 + - resolution: + archive: zip + bin: + node: node.exe + integrity: sha256-CuaEBrQtdyVmHal5sUA+yZJtogXGdwgn8zqsnY8m6CE= + prefix: node-v24.18.0-win-x64 + type: binary + url: https://nodejs.org/download/release/v24.18.0/node-v24.18.0-win-x64.zip + targets: + - cpu: x64 + os: win32 + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-scbC3DG0bdj7IyL0/nWwfndcUSC8NyUd7uoo9SnUVns= + type: binary + url: https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-arm64-musl.tar.gz + targets: + - cpu: arm64 + os: linux + libc: musl + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-6lhAmRHhQexrGdkXjvotkYWhMpUAWxy/VSGzFX7tHZU= + type: binary + url: https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-x64-musl.tar.gz + targets: + - cpu: x64 + os: linux + libc: musl + version: 24.18.0 + hasBin: true + + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-parser@0.131.0: + resolution: {integrity: sha512-SJ3/7ZPbgie8dr5Z9BI/M51zZbpXba+hRSG0MDzVwMW5CRQg2fjYE0jHGlLX4eeiibGgC/mzoDFKSDHwVZEHRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + + oxc-walker@0.7.0: + resolution: {integrity: sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==} + peerDependencies: + oxc-parser: '>=0.98.0' + + 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'} + + package-manager-detector@1.7.0: + resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + 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'} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + 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.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + playwright-core@1.62.0: + resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.0: + resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + engines: {node: '>=20'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + engines: {node: '>=4'} + + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@4.1.1: + resolution: {integrity: sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==} + engines: {node: '>=20'} + peerDependencies: + prettier: ^3.0.0 + svelte: ^5.0.0 + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + recast@0.23.12: + resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} + engines: {node: '>=8'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + + runed@0.37.1: + resolution: {integrity: sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + zod: ^4.1.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + zod: + optional: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@3.1.2: + resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + engines: {node: '>=20.9.0'} + + 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'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sqids@0.3.0: + resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + storybook-addon-pseudo-states@10.5.4: + resolution: {integrity: sha512-B2b1OtB3MzJmdUTJel4x3IVutISezyWQLDEpFxFJ9Vg6gCiqscPeCt5nRwZoB/rEfhxZHqM+BQlw0I4TYqP1fA==} + peerDependencies: + storybook: ^10.5.4 + + storybook@10.5.4: + resolution: {integrity: sha512-bmLxPsxVSPnbeiZqYQpozyNOiJXfk+pf7WfHZflvPkwT6Y+rvYz3Cj/D6H4Kf2jHpuDNiMXBKO3yawLN2OWirg==} + hasBin: true + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + prettier: ^2 || ^3 + vite-plus: ^0.1.15 || ^0.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + prettier: + optional: true + vite-plus: + optional: true + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + svelte-ast-print@0.4.2: + resolution: {integrity: sha512-hRHHufbJoArFmDYQKCpCvc0xUuIEfwYksvyLYEQyH+1xb5LD5sM/IthfooCdXZQtOIqXz6xm7NmaqdfwG4kh6w==} + engines: {node: '>=18'} + peerDependencies: + svelte: ^5.0.0 + + svelte-check@4.7.3: + resolution: {integrity: sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-eslint-parser@1.8.0: + resolution: {integrity: sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.34.1} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-parse-markup@0.1.5: + resolution: {integrity: sha512-T6mqZrySltPCDwfKXWQ6zehipVLk4GWfH1zCMGgRtLlOIFPuw58ZxVYxVvotMJgJaurKi1i14viB2GIRKXeJTQ==} + peerDependencies: + svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1 + + svelte-toolbelt@0.10.6: + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte2tsx@0.7.58: + resolution: {integrity: sha512-NXzvlRQ2ZH8mhHOIQURyzGAu2RbbY+rcSNIXs39m2bBo6Fcw3T+EgDHnCKhncupRtzUqhWNaqQnAkfLCDjHrDg==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 || ^6.0.0 + + svelte@5.56.8: + resolution: {integrity: sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==} + engines: {node: '>=18'} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + tabbable@6.5.0: + resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tmcp@1.19.4: + resolution: {integrity: sha512-fMoUJ3Gef9iA0yKZNeW2SQCCKTluCwghUyOz/qxPS8XxrQk6Jlw4lgql3S+s6L//FteLeSJ7erKxMWl727mZoQ==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + twgl.js@7.0.0: + resolution: {integrity: sha512-9HHZ8emdZb2nKPU0FDIGDlkp6AD9rWFv7ThqdbmUqyBhD3WTQpLN+89w4B+YshGwD1fL6PMVpVKZqC0pikgmcA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + + type-level-regexp@0.1.17: + resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} + + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + 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 + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unocss@66.7.5: + resolution: {integrity: sha512-nAdmU8TwnQoiLnQjZ6Hm1GmHy9lTexKsAZNEJtZnxN9wyFRc1eLjQgmh9r7UEGxBQMQLD8OZOgmk5HpwObbh0Q==} + peerDependencies: + '@unocss/astro': 66.7.5 + '@unocss/postcss': 66.7.5 + '@unocss/webpack': 66.7.5 + peerDependenciesMeta: + '@unocss/astro': + optional: true + '@unocss/postcss': + optional: true + '@unocss/webpack': + optional: true + + unplugin-fonts@2.0.0: + resolution: {integrity: sha512-MjyPgq9UVXYSQlcqG5Y/tYFCJvK0unURtT8+PdhrYV7u/8uwycqqLj5ouwpZ+oNQXRvemYqgnSLxGUv00r984g==} + peerDependencies: + '@nuxt/kit': ^3.0.0 || ^4.0.0 + vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + unplugin-utils@0.3.2: + resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==} + engines: {node: '>=20.19.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unplugin@3.3.0: + resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: '*' + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + uri-template-matcher@1.1.2: + resolution: {integrity: sha512-uZc1h12jdO3m/R77SfTEOuo6VbMhgWznaawKpBjRGSJb7i91x5PgI37NQJtG+Cerxkk0yr1pylBY2qG1kQ+aEQ==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + vite-imagetools@9.0.3: + resolution: {integrity: sha512-FwjApRNZyN+RucPW9Z9kf0dyzyi3r3zlDfrTnzHXNaYpmT3pZ5w//d6QkApy1iypbDm+3fq+Gwfv+PYA4j4uYw==} + engines: {node: '>=20.0.0'} + + vite-plugin-devtools-json@1.1.0: + resolution: {integrity: sha512-Fy9nMgMudGzDtUh1tGL7v0WAH0gsnZcZu2LFr/+1YxvYaJnxgbqRqv4jLU2tn5L9oqSaETHVCdDo9f8mSCQEZQ==} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest-browser-svelte@3.0.0: + resolution: {integrity: sha512-BKM9iBGNsEQzigWuSZFAiw6AJDqQqMttApoD8gM8LTI6vxfd423rDMN8GqUHJyGbhH3XcOo9N9u/4hu47Nxhsw==} + peerDependencies: + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + vitest: ^4.0.0 + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + 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'} + + workerd@1.20260722.1: + resolution: {integrity: sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ==} + engines: {node: '>=16'} + hasBin: true + + worktop@0.8.0-next.18: + resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} + engines: {node: '>=12'} + + wrangler@4.114.0: + resolution: {integrity: sha512-M65P25t5UHA1TIJfgZXDcj+YzVobgKdRguM2QPz0xnxLFuOcuE3ErgllDht0iaho7MS4o0g/Bb4YK2+GT+bibg==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20260722.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@adobe/css-tools@4.5.0': {} + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.7.0 + tinyexec: 1.2.4 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@blazediff/core@1.9.1': {} + + '@capsizecss/unpack@4.0.1': + dependencies: + fontkitten: 1.0.3 + + '@chromatic-com/storybook@5.2.1(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))': + dependencies: + '@neoconfetti/react': 1.0.0 + chromatic: 16.10.0 + jsonfile: 6.2.1 + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + strip-ansi: 7.2.0 + transitivePeerDependencies: + - '@chromatic-com/cypress' + - '@chromatic-com/playwright' + - '@chromatic-com/vitest' + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260722.1 + + '@cloudflare/workerd-darwin-64@1.20260722.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260722.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260722.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260722.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260722.1': + optional: true + + '@cloudflare/workers-types@4.20260702.1': {} + + '@commitlint/cli@21.2.1(@types/node@24.13.3)(conventional-commits-parser@7.1.0)(typescript@6.0.3)': + dependencies: + '@commitlint/config-conventional': 21.2.0 + '@commitlint/format': 21.2.0 + '@commitlint/lint': 21.2.0 + '@commitlint/load': 21.2.0(@types/node@24.13.3)(typescript@6.0.3) + '@commitlint/read': 21.2.1(conventional-commits-parser@7.1.0) + '@commitlint/types': 21.2.0 + tinyexec: 1.2.4 + yargs: 18.0.0 + transitivePeerDependencies: + - '@types/node' + - conventional-commits-filter + - conventional-commits-parser + - typescript + + '@commitlint/config-conventional@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + conventional-changelog-conventionalcommits: 10.2.1 + + '@commitlint/config-validator@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + ajv: 8.20.0 + + '@commitlint/ensure@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + es-toolkit: 1.49.0 + + '@commitlint/execute-rule@21.0.1': {} + + '@commitlint/format@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + picocolors: 1.1.1 + + '@commitlint/is-ignored@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + semver: 7.8.5 + + '@commitlint/lint@21.2.0': + dependencies: + '@commitlint/is-ignored': 21.2.0 + '@commitlint/parse': 21.2.0 + '@commitlint/rules': 21.2.0 + '@commitlint/types': 21.2.0 + + '@commitlint/load@21.2.0(@types/node@24.13.3)(typescript@6.0.3)': + dependencies: + '@commitlint/config-validator': 21.2.0 + '@commitlint/execute-rule': 21.0.1 + '@commitlint/resolve-extends': 21.2.0 + '@commitlint/types': 21.2.0 + cosmiconfig: 9.0.2(typescript@6.0.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@24.13.3)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3) + es-toolkit: 1.49.0 + is-plain-obj: 4.1.0 + picocolors: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@21.2.0': {} + + '@commitlint/parse@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + conventional-changelog-angular: 9.2.1 + conventional-commits-parser: 7.1.0 + + '@commitlint/read@21.2.1(conventional-commits-parser@7.1.0)': + dependencies: + '@commitlint/top-level': 21.2.0 + '@commitlint/types': 21.2.0 + '@conventional-changelog/git-client': 3.1.0(conventional-commits-parser@7.1.0) + tinyexec: 1.2.4 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser + + '@commitlint/resolve-extends@21.2.0': + dependencies: + '@commitlint/config-validator': 21.2.0 + '@commitlint/types': 21.2.0 + es-toolkit: 1.49.0 + global-directory: 5.0.0 + resolve-from: 5.0.0 + + '@commitlint/rules@21.2.0': + dependencies: + '@commitlint/ensure': 21.2.0 + '@commitlint/message': 21.2.0 + '@commitlint/to-lines': 21.0.1 + '@commitlint/types': 21.2.0 + + '@commitlint/to-lines@21.0.1': {} + + '@commitlint/top-level@21.2.0': + dependencies: + escalade: 3.2.0 + + '@commitlint/types@21.2.0': + dependencies: + conventional-commits-parser: 7.1.0 + picocolors: 1.1.1 + + '@conventional-changelog/git-client@3.1.0(conventional-commits-parser@7.1.0)': + dependencies: + '@simple-libs/child-process-utils': 2.0.0 + '@simple-libs/stream-utils': 2.0.0 + semver: 7.8.5 + optionalDependencies: + conventional-commits-parser: 7.1.0 + + '@conventional-changelog/template@1.2.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.9.2': + 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/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0))': + dependencies: + eslint: 10.8.0(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.6': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0))': + optionalDependencies: + eslint: 10.8.0(jiti@2.7.0) + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@fontsource/redaction-35@5.3.0': {} + + '@fontsource/redaction-50@5.3.0': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify-json/mdi@1.2.3': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/pixel@1.2.2': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/pixelarticons@1.2.7': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.4': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.1 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.1 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.1': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.1': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.1 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.1 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.1 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.1 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.1 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-wasm32@0.35.2': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-arm64@0.35.2': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.35.2': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.35.2': + optional: true + + '@internationalized/date@3.12.2': + dependencies: + '@swc/helpers': 0.5.23 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@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.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': + dependencies: + '@types/mdx': 2.0.14 + '@types/react': 19.2.17 + react: 19.2.8 + + '@napi-rs/wasm-runtime@1.1.6(@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.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@neoconfetti/react@1.0.0': {} + + '@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.1 + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.131.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.131.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.131.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.131.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.131.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.131.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-project/types@0.131.0': {} + + '@oxc-project/types@0.139.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + optional: true + + '@oxc-resolver/binding-android-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + optional: true + + '@pkgr/core@0.3.6': {} + + '@polka/url@1.0.0-next.29': {} + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/pluginutils@5.4.0': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + + '@simple-libs/child-process-utils@2.0.0': + dependencies: + '@simple-libs/stream-utils': 2.0.0 + + '@simple-libs/stream-utils@2.0.0': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.17': {} + + '@standard-schema/spec@1.1.0': {} + + '@storybook/addon-a11y@10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))': + dependencies: + '@storybook/global': 5.0.0 + axe-core: 4.12.1 + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + + '@storybook/addon-docs@10.5.4(@types/react@19.2.17)(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) + '@storybook/csf-plugin': 10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@storybook/icons': 2.1.0(react@19.2.8) + '@storybook/react-dom-shim': 10.5.4(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8)) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + ts-dedent: 2.3.0 + optionalDependencies: + '@types/react': 19.2.17 + transitivePeerDependencies: + - '@types/react-dom' + - esbuild + - rollup + - vite + - webpack + + '@storybook/addon-svelte-csf@5.1.2(@storybook/svelte@10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0)))(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@storybook/csf': 0.1.13 + '@storybook/svelte': 10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + dedent: 1.7.2 + es-toolkit: 1.49.0 + esrap: 1.4.9 + magic-string: 0.30.21 + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte-ast-print: 0.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + zimmerframe: 1.1.4 + transitivePeerDependencies: + - babel-plugin-macros + + '@storybook/addon-vitest@10.5.4(@vitest/browser-playwright@4.1.10)(@vitest/browser@4.1.10)(@vitest/runner@4.1.10)(react@19.2.8)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vitest@4.1.10)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.1.0(react@19.2.8) + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + optionalDependencies: + '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))(vitest@4.1.10) + '@vitest/browser-playwright': 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))(vitest@4.1.10) + '@vitest/runner': 4.1.10 + vitest: 4.1.10(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + transitivePeerDependencies: + - react + + '@storybook/builder-vite@10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@storybook/csf-plugin': 10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + ts-dedent: 2.3.0 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/csf-plugin@10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.28.1 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + '@storybook/csf@0.1.13': + dependencies: + type-fest: 2.19.0 + + '@storybook/global@5.0.0': {} + + '@storybook/icons@2.1.0(react@19.2.8)': + dependencies: + react: 19.2.8 + + '@storybook/react-dom-shim@10.5.4(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))': + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.17 + + '@storybook/svelte-vite@10.5.4(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@storybook/builder-vite': 10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@storybook/svelte': 10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + magic-string: 0.30.21 + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte2tsx: 0.7.58(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@5.9.3) + typescript: 5.9.3 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/svelte@10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + dependencies: + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + ts-dedent: 2.3.0 + type-fest: 5.8.0 + + '@storybook/sveltekit@10.5.4(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@storybook/builder-vite': 10.5.4(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@storybook/svelte': 10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + '@storybook/svelte-vite': 10.5.4(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(esbuild@0.28.1)(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + transitivePeerDependencies: + - '@sveltejs/vite-plugin-svelte' + - esbuild + - rollup + - webpack + + '@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0))': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)) + '@typescript-eslint/types': 8.65.0 + eslint: 10.8.0(jiti@2.7.0) + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.5 + + '@sveltejs/acorn-typescript@1.0.11(acorn@8.17.0)': + dependencies: + acorn: 8.17.0 + + '@sveltejs/adapter-cloudflare@7.2.9(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(wrangler@4.114.0)': + dependencies: + '@cloudflare/workers-types': 4.20260702.1 + '@sveltejs/kit': 2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + worktop: 0.8.0-next.18 + wrangler: 4.114.0 + + '@sveltejs/enhanced-img@0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + magic-string: 0.30.21 + sharp: 0.34.5 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte-parse-markup: 0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + vite-imagetools: 9.0.3 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - rollup + + '@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@types/cookie': 0.6.0 + acorn: 8.17.0 + cookie: 0.6.0 + devalue: 5.8.2 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.1.2 + sirv: 3.0.2 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + optionalDependencies: + typescript: 6.0.3 + + '@sveltejs/load-config@0.2.0': {} + + '@sveltejs/mcp@0.1.25(jiti@2.7.0)(typescript@6.0.3)': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + sade: 1.8.1 + tmcp: 1.19.4(typescript@6.0.3) + transitivePeerDependencies: + - jiti + - supports-color + - typescript + + '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.3 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + vitefu: 1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/svelte-core@1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + dependencies: + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/cookie@0.6.0': {} + + '@types/culori@4.0.1': {} + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/mdx@2.0.14': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': {} + + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 + eslint: 10.8.0(jiti@2.7.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3 + eslint: 10.8.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.8.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.65.0': {} + + '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + eslint: 10.8.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + eslint-visitor-keys: 5.0.1 + + '@unocss/cli@66.7.5': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + '@unocss/preset-wind4': 66.7.5 + '@unocss/transformer-directives': 66.7.5 + cac: 7.0.0 + chokidar: 5.0.0 + colorette: 2.0.20 + consola: 3.4.2 + magic-string: 0.30.21 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + tinyglobby: 0.2.17 + unplugin-utils: 0.3.2 + + '@unocss/config@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + colorette: 2.0.20 + consola: 3.4.2 + unconfig: 7.5.0 + + '@unocss/core@66.7.5': {} + + '@unocss/eslint-config@66.7.5(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@unocss/eslint-plugin': 66.7.5(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@unocss/eslint-plugin@66.7.5(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + magic-string: 0.30.21 + synckit: 0.11.13 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@unocss/extractor-arbitrary-variants@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/extractor-svelte@66.7.5': {} + + '@unocss/inspector@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + colorette: 2.0.20 + gzip-size: 6.0.0 + sirv: 3.0.2 + + '@unocss/preset-attributify@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/preset-icons@66.7.5': + dependencies: + '@iconify/utils': 3.1.4 + '@unocss/core': 66.7.5 + ofetch: 1.5.1 + + '@unocss/preset-mini@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/extractor-arbitrary-variants': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-tagify@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/preset-typography@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-uno@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + + '@unocss/preset-web-fonts@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + ofetch: 1.5.1 + + '@unocss/preset-wind3@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-mini': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-wind4@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/extractor-arbitrary-variants': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-wind@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + + '@unocss/reset@66.7.5': {} + + '@unocss/rule-utils@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + magic-string: 0.30.21 + + '@unocss/svelte-scoped@66.7.5': + dependencies: + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/preset-uno': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + '@unocss/reset': 66.7.5 + acorn: 8.17.0 + css-tree: 3.2.1 + magic-string: 0.30.21 + pathe: 2.0.3 + zimmerframe: 1.1.4 + + '@unocss/transformer-attributify-jsx@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + oxc-parser: 0.131.0 + oxc-walker: 0.7.0(oxc-parser@0.131.0) + + '@unocss/transformer-compile-class@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/transformer-directives@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + css-tree: 3.2.1 + + '@unocss/transformer-variant-group@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/vite@66.7.5(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/inspector': 66.7.5 + chokidar: 5.0.0 + magic-string: 0.30.21 + pathe: 2.0.3 + tinyglobby: 0.2.17 + unplugin-utils: 0.3.2 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + '@vitest/browser-playwright@4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))(vitest@4.1.10)': + dependencies: + '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + playwright: 1.62.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))(vitest@4.1.10)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + ws: 8.21.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/coverage-v8@4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.3 + std-env: 4.2.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + optionalDependencies: + '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))(vitest@4.1.10) + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@webcontainer/env@1.1.1': {} + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.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 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + argue-cli@3.1.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.1: {} + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + ast-v8-to-istanbul@1.0.4: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + axe-core@4.12.1: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/dom': 1.8.0 + '@internationalized/date': 3.12.2 + esm-env: 1.2.2 + runed: 0.35.1(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + tabbable: 6.5.0 + transitivePeerDependencies: + - '@sveltejs/kit' + + blake3-wasm@2.1.5: {} + + brace-expansion@1.1.16: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cac@7.0.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 + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chromatic@16.10.0: + dependencies: + semver: 7.8.5 + + chromatic@18.1.0: + dependencies: + semver: 7.8.5 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + conventional-changelog-angular@9.2.1: + dependencies: + '@conventional-changelog/template': 1.2.1 + + conventional-changelog-conventionalcommits@10.2.1: + dependencies: + '@conventional-changelog/template': 1.2.1 + + conventional-commits-parser@7.1.0: + dependencies: + '@simple-libs/stream-utils': 2.0.0 + argue-cli: 3.1.0 + + convert-source-map@2.0.0: {} + + cookie@0.6.0: {} + + cookie@1.1.1: {} + + cosmiconfig-typescript-loader@6.3.0(@types/node@24.13.3)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): + dependencies: + '@types/node': 24.13.3 + cosmiconfig: 9.0.2(typescript@6.0.3) + jiti: 2.6.1 + typescript: 6.0.3 + + cosmiconfig@9.0.2(typescript@6.0.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 6.0.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + culori@4.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent-js@1.0.1: {} + + dedent@1.7.2: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + devalue@5.8.2: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + duplexer@0.1.2: {} + + emoji-regex@10.6.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@2.3.1: {} + + es-toolkit@1.49.0: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)): + dependencies: + eslint: 10.8.0(jiti@2.7.0) + + eslint-plugin-perfectionist@5.10.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.8.0(jiti@2.7.0) + natural-orderby: 5.0.0 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-storybook@10.5.4(eslint@10.8.0(jiti@2.7.0))(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8))(typescript@6.0.3): + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.8.0(jiti@2.7.0) + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-svelte@3.22.0(eslint@10.8.0(jiti@2.7.0))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 10.8.0(jiti@2.7.0) + esutils: 2.0.3 + globals: 16.5.0 + known-css-properties: 0.37.0 + postcss: 8.5.19 + postcss-load-config: 3.1.4(postcss@8.5.19) + postcss-safe-parser: 7.0.1(postcss@8.5.19) + semver: 7.8.5 + svelte-eslint-parser: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + optionalDependencies: + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + transitivePeerDependencies: + - ts-node + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + 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@10.8.0(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + eslint@9.39.5(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6 + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + esm-env@1.2.2: {} + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrap@1.2.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@types/estree': 1.0.9 + + esrap@1.4.9: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + esrap@2.3.0(@typescript-eslint/types@8.65.0): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.65.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + 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-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.3: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + 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@4.0.1: + dependencies: + flatted: 3.4.3 + keyv: 4.5.4 + + flatted@3.4.3: {} + + fontaine@0.8.0: + dependencies: + '@capsizecss/unpack': 4.0.1 + css-tree: 3.2.1 + magic-regexp: 0.10.0 + magic-string: 0.30.21 + pathe: 2.0.3 + ufo: 1.6.4 + unplugin: 2.3.11 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + global-directory@5.0.0: + dependencies: + ini: 6.0.0 + + globals@14.0.0: {} + + globals@16.5.0: {} + + globals@17.8.0: {} + + graceful-fs@4.2.11: + optional: true + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + has-flag@4.0.0: {} + + html-escaper@2.0.2: {} + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + imagetools-core@9.1.0: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.2.0: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + ini@6.0.0: {} + + inline-style-parser@0.2.7: {} + + is-arrayish@0.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jiti@2.6.1: {} + + jiti@2.7.0: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-rpc-2.0@1.7.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + known-css-properties@0.37.0: {} + + 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 + + lilconfig@2.1.0: {} + + lines-and-columns@1.2.4: {} + + locate-character@3.0.0: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loupe@3.2.1: {} + + lz-string@1.5.0: {} + + magic-regexp@0.10.0: + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.21 + mlly: 1.8.2 + regexp-tree: 0.1.27 + type-level-regexp: 0.1.17 + ufo: 1.6.4 + unplugin: 2.3.11 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + mdn-data@2.27.1: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + min-indent@1.0.1: {} + + miniflare@4.20260722.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.2 + undici: 7.28.0 + workerd: 1.20260722.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.16 + + mlly@1.8.2: + dependencies: + acorn: 8.17.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + natural-compare@1.4.0: {} + + natural-orderby@5.0.0: {} + + node-fetch-native@1.6.7: {} + + node@runtime:24.18.0: {} + + obug@2.1.3: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + 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 + + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-parser@0.131.0: + dependencies: + '@oxc-project/types': 0.131.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.131.0 + '@oxc-parser/binding-android-arm64': 0.131.0 + '@oxc-parser/binding-darwin-arm64': 0.131.0 + '@oxc-parser/binding-darwin-x64': 0.131.0 + '@oxc-parser/binding-freebsd-x64': 0.131.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.131.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.131.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.131.0 + '@oxc-parser/binding-linux-arm64-musl': 0.131.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.131.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.131.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.131.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.131.0 + '@oxc-parser/binding-linux-x64-gnu': 0.131.0 + '@oxc-parser/binding-linux-x64-musl': 0.131.0 + '@oxc-parser/binding-openharmony-arm64': 0.131.0 + '@oxc-parser/binding-wasm32-wasi': 0.131.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.131.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.131.0 + '@oxc-parser/binding-win32-x64-msvc': 0.131.0 + + oxc-resolver@11.24.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + + oxc-walker@0.7.0(oxc-parser@0.131.0): + dependencies: + magic-regexp: 0.10.0 + oxc-parser: 0.131.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-manager-detector@1.7.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.5: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + playwright-core@1.62.0: {} + + playwright@1.62.0: + dependencies: + playwright-core: 1.62.0 + optionalDependencies: + fsevents: 2.3.2 + + pngjs@7.0.0: {} + + postcss-load-config@3.1.4(postcss@8.5.19): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.3 + optionalDependencies: + postcss: 8.5.19 + + postcss-safe-parser@7.0.1(postcss@8.5.19): + dependencies: + postcss: 8.5.19 + + postcss-scss@4.0.9(postcss@8.5.19): + dependencies: + postcss: 8.5.19 + + postcss-selector-parser@7.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.19: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + prettier: 3.9.6 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + + prettier@3.9.6: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + punycode@2.3.1: {} + + quansync@1.0.0: {} + + queue-microtask@1.2.3: {} + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react@19.2.8: {} + + readdirp@4.1.2: {} + + readdirp@5.0.0: {} + + recast@0.23.12: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + regexp-tree@0.1.27: {} + + regexparam@3.0.0: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + runed@0.35.1(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + optionalDependencies: + '@sveltejs/kit': 2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + + runed@0.37.1(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + optionalDependencies: + '@sveltejs/kit': 2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + scheduler@0.27.0: {} + + scule@1.3.0: {} + + semver@7.8.5: {} + + set-cookie-parser@3.1.2: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + sharp@0.35.2: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + sqids@0.3.0: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + storybook-addon-pseudo-states@10.5.4(storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8)): + dependencies: + storybook: 10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8) + + storybook@10.5.4(@types/react@19.2.17)(prettier@3.9.6)(react@19.2.8): + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.1.0(react@19.2.8) + '@testing-library/dom': 10.4.1 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 + esbuild: 0.28.1 + jsonc-parser: 3.3.1 + open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.24.2 + recast: 0.23.12 + semver: 7.8.5 + use-sync-external-store: 1.6.0(react@19.2.8) + ws: 8.21.1 + optionalDependencies: + '@types/react': 19.2.17 + prettier: 3.9.6 + transitivePeerDependencies: + - bufferutil + - react + - utf-8-validate + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + supports-color@10.2.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + svelte-ast-print@0.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + esrap: 1.2.2 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + zimmerframe: 1.1.2 + + svelte-check@4.7.3(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.0 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.5) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte-eslint-parser@1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.19 + postcss-scss: 4.0.9(postcss@8.5.19) + postcss-selector-parser: 7.1.4 + semver: 7.8.5 + optionalDependencies: + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + + svelte-parse-markup@0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + dependencies: + clsx: 2.1.1 + runed: 0.35.1(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@6.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + style-to-object: 1.0.14 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + transitivePeerDependencies: + - '@sveltejs/kit' + + svelte2tsx@0.7.58(svelte@5.56.8(@typescript-eslint/types@8.65.0))(typescript@5.9.3): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + typescript: 5.9.3 + + svelte@5.56.8(@typescript-eslint/types@8.65.0): + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.17.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.8.2 + esm-env: 1.2.2 + esrap: 2.3.0(@typescript-eslint/types@8.65.0) + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + tabbable@6.5.0: {} + + tagged-tag@1.0.0: {} + + tiny-inflate@1.0.3: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@2.0.0: {} + + tinyrainbow@3.1.0: {} + + tinyspy@4.0.4: {} + + tmcp@1.19.4(typescript@6.0.3): + dependencies: + '@standard-schema/spec': 1.1.0 + json-rpc-2.0: 1.7.1 + sqids: 0.3.0 + uri-template-matcher: 1.1.2 + valibot: 1.4.2(typescript@6.0.3) + transitivePeerDependencies: + - typescript + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + ts-dedent@2.3.0: {} + + tslib@2.8.1: {} + + twgl.js@7.0.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@2.19.0: {} + + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + + type-level-regexp@0.1.17: {} + + typescript-eslint@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.8.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.7 + jiti: 2.7.0 + quansync: 1.0.0 + unconfig-core: 7.5.0 + + undici-types@7.18.2: {} + + undici@7.28.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + universalify@2.0.1: {} + + unocss@66.7.5(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)): + dependencies: + '@unocss/cli': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/preset-attributify': 66.7.5 + '@unocss/preset-icons': 66.7.5 + '@unocss/preset-mini': 66.7.5 + '@unocss/preset-tagify': 66.7.5 + '@unocss/preset-typography': 66.7.5 + '@unocss/preset-uno': 66.7.5 + '@unocss/preset-web-fonts': 66.7.5 + '@unocss/preset-wind': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + '@unocss/preset-wind4': 66.7.5 + '@unocss/transformer-attributify-jsx': 66.7.5 + '@unocss/transformer-compile-class': 66.7.5 + '@unocss/transformer-directives': 66.7.5 + '@unocss/transformer-variant-group': 66.7.5 + '@unocss/vite': 66.7.5(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + transitivePeerDependencies: + - vite + + unplugin-fonts@2.0.0(patch_hash=daf89586c1c46e1f69c3db9885a0abddd667d76a91ab3ff48091ec0b0b1c0706)(esbuild@0.28.1)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)): + dependencies: + css-tree: 3.2.1 + fast-glob: 3.3.3 + fontaine: 0.8.0 + magic-string: 0.30.21 + pathe: 2.0.3 + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - unloader + - webpack + + unplugin-utils@0.3.2: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.5 + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.17.0 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + + unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + esbuild: 0.28.1 + rolldown: 1.1.5 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uri-template-matcher@1.1.2: {} + + use-sync-external-store@1.6.0(react@19.2.8): + dependencies: + react: 19.2.8 + + util-deprecate@1.0.2: {} + + uuid@14.0.1: {} + + valibot@1.4.2(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + + vite-imagetools@9.0.3: + dependencies: + '@rollup/pluginutils': 5.4.0 + imagetools-core: 9.1.0 + sharp: 0.34.5 + transitivePeerDependencies: + - rollup + + vite-plugin-devtools-json@1.1.0(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)): + dependencies: + uuid: 14.0.1 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitefu@1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)): + optionalDependencies: + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + + vitest-browser-svelte@3.0.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vitest@4.1.10): + dependencies: + '@testing-library/svelte-core': 1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + vitest: 4.1.10(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + + vitest@4.1.10(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + '@vitest/browser-playwright': 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0))(vitest@4.1.10) + '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) + transitivePeerDependencies: + - msw + + webpack-virtual-modules@0.6.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: {} + + workerd@1.20260722.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260722.1 + '@cloudflare/workerd-darwin-arm64': 1.20260722.1 + '@cloudflare/workerd-linux-64': 1.20260722.1 + '@cloudflare/workerd-linux-arm64': 1.20260722.1 + '@cloudflare/workerd-windows-64': 1.20260722.1 + + worktop@0.8.0-next.18: + dependencies: + mrmime: 2.0.1 + regexparam: 3.0.0 + + wrangler@4.114.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260722.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260722.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + ws@8.21.0: {} + + ws@8.21.1: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + y18n@5.0.8: {} + + yaml@1.10.3: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yocto-queue@0.1.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.17 + cookie: 1.1.1 + youch-core: 0.3.3 + + zimmerframe@1.1.2: {} + + zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..ec91452 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,24 @@ +minimumReleaseAge: 1440 +trustPolicy: no-downgrade +trustPolicyExclude: + - 'chokidar@4.0.3' +onlyBuiltDependencies: + - workerd + - sharp + - esbuild +allowBuilds: + esbuild: true + sharp: true + workerd: true +overrides: + undici@>=7.0.0 <7.28.0: ^7.28.0 + undici@>=7.23.0 <7.28.0: ^7.28.0 + ws@>=8.0.0 <8.21.0: ^8.21.0 +minimumReleaseAgeExclude: + - ws@8.21.0 + - undici@7.28.0 +publicHoistPattern: + - 'react' + - 'react-dom' +patchedDependencies: + unplugin-fonts@2.0.0: patches/unplugin-fonts@2.0.0.patch diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..b8283d8 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,19 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + interface Platform { + env: Env; + ctx: ExecutionContext; + caches: CacheStorage; + cf?: IncomingRequestCfProperties; + } + + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..6a2bb58 --- /dev/null +++ b/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/docs/Typography.mdx b/src/docs/Typography.mdx new file mode 100644 index 0000000..fc467be --- /dev/null +++ b/src/docs/Typography.mdx @@ -0,0 +1,52 @@ +import { Meta, Typeset } from '@storybook/addon-docs/blocks'; +import { fonts, textScale } from '$lib/tokens'; + + + +# Typography + +Three font families, sized with UnoCSS's `text-*` scale (Wind4). Use `font-body`, +`font-header`, or `font-art` for family and `text-xs … text-9xl` for size. The sizes +below are read directly from that scale. + +export const sizes = Object.values(textScale).map((t) => t.fontSize); +export const sample = 'The quick brown fox jumps over the lazy dog'; + +## UI and Prose + +**Font:** {fonts.body} + +**Weights:** 100, 300, 400, 600, 800 (normal and italic) + + + +## Headers + +**Font:** {fonts.header} + +**Weights:** 400, 700 + + + +## Art + +**Font:** {fonts.art} + +**Weights:** 400, 700 + + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..460b054 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,7 @@ +import type { Handle } from '@sveltejs/kit'; + +export const handle: Handle = ({ event, resolve }) => + resolve(event, { + preload: ({ type, path }) => + type === 'css' || type === 'js' || (type === 'font' && /-latin-(?!ext-)/.test(path)), + }); diff --git a/src/lib/Button/Button.stories.svelte b/src/lib/Button/Button.stories.svelte new file mode 100644 index 0000000..3fe4847 --- /dev/null +++ b/src/lib/Button/Button.stories.svelte @@ -0,0 +1,285 @@ + + +{#snippet bell()} + +{/snippet} + +{#snippet template(args: Args)} +
+
+ + Default +
+
+ + Hover +
+
+ + Active +
+
+ + Focus Visible +
+
+{/snippet} + +{#snippet figmaReferenceVariants(args: Args)} +
+
+ + + +
+
+ +
+
+{/snippet} + +{#snippet linkTemplate(args: Args)} + +{/snippet} + +{#snippet largeCta(args: Args)} +
+ +
+{/snippet} + +{#snippet primary(args: Args)} +
+ +
+{/snippet} + +{#snippet secondary(args: Args)} +
+ +
+{/snippet} + +{#snippet underline(args: Args)} +
+
+ + Default +
+
+ + Hover +
+
+{/snippet} + +{#snippet layered(args: Args)} + +{/snippet} + +{#snippet semanticOnly(args: Args)} + +{/snippet} + +{#snippet presentationSurface()} + + Join the jam + +{/snippet} + + + + + + + + + + + + + + + + + + { + await expect(within(canvasElement).getByRole('link', { name: 'About' })).toHaveAttribute( + 'href', + '#about', + ); + }} +/> + + { + const canvas = within(canvasElement); + const controls = canvas.getAllByRole('button', { name: 'Layered control' }); + const button = canvas.getByRole('button', { name: 'Layered control' }); + const surfaces = button.querySelectorAll('[data-button-surface]'); + const base = requiredElement(button, '[data-button-surface="base"]'); + const overlay = requiredElement(button, '[data-button-surface="overlay"]'); + + await expect(controls).toHaveLength(1); + await expect(surfaces).toHaveLength(2); + await expect(base).not.toBeNull(); + await expect(overlay).toHaveAttribute('aria-hidden', 'true'); + await expect(overlay).toHaveAttribute('inert'); + await expect(getComputedStyle(overlay).pointerEvents).toBe('none'); + await expect(overlay.style.clipPath).not.toBe(''); + await expect(overlay.style.clipPath).not.toBe('none'); + await expect(overlay.style.clipPath).toContain('50%'); + + const baseRect = base.getBoundingClientRect(); + const overlayRect = overlay.getBoundingClientRect(); + await expect(overlayRect.left).toBeCloseTo(baseRect.left); + await expect(overlayRect.top).toBeCloseTo(baseRect.top); + await expect(overlayRect.width).toBeCloseTo(baseRect.width); + await expect(overlayRect.height).toBeCloseTo(baseRect.height); + + let activations = 0; + button.addEventListener('click', () => { + activations += 1; + }); + + await userEvent.click(button); + await expect(activations).toBe(1); + + button.focus(); + await userEvent.keyboard('{Enter}'); + await expect(activations).toBe(2); + await expect(overlay.matches(':focus')).toBe(false); + + button.blur(); + canvasElement.ownerDocument.body.focus(); + await userEvent.tab(); + await expect(button).toHaveFocus(); + }} +/> + + { + const surface = requiredElement(canvasElement, '[data-button-surface="base"]'); + const content = requiredElement(surface, '.button-surface-content'); + + await expect(content.scrollWidth).toBeLessThanOrEqual(surface.clientWidth); + }} +/> + + { + const button = within(canvasElement).getByRole('button', { name: 'Semantic only' }); + const label = requiredElement(button, '.button-layout'); + let activations = 0; + button.addEventListener('click', () => { + activations += 1; + }); + + await expect(button.querySelectorAll('[data-button-surface]')).toHaveLength(0); + await expect(label).not.toHaveAttribute('aria-hidden'); + + await userEvent.click(button); + await expect(activations).toBe(1); + }} +/> + + { + const button = within(canvasElement).getByRole('button', { name: 'Semantic only' }); + + await expect(button).toBeDisabled(); + await expect(button.querySelectorAll('[data-button-surface]')).toHaveLength(0); + }} +/> diff --git a/src/lib/Button/Button.svelte b/src/lib/Button/Button.svelte new file mode 100644 index 0000000..c290a19 --- /dev/null +++ b/src/lib/Button/Button.svelte @@ -0,0 +1,122 @@ + + + +} + data-button-variant={variant} + style={buttonStyle} + class={[ + 'summer-summit-button relative isolate h-[var(--button-height)] bg-transparent px-[var(--button-inline-padding)] text-base font-semibold tracking-normal outline-2 outline-transparent outline-offset--2 inline-flex gap-2 cursor-pointer select-none whitespace-nowrap uppercase transition-all duration-100 transition-ease-out items-center justify-center active:[transform:scale(0.98)] disabled:cursor-not-allowed focus-visible:not-disabled:(rounded outline-offset-3 [outline-color:var(--button-focus-ring)])', + 'font-body', + variant === 'underline' && 'border-0', + variant === 'spray' && + visuals && + 'disabled:[color:var(--button-disabled-content)] disabled:[background:var(--button-disabled-surface)]', + className, + ]} +> + + {@render children?.()} + {@render icon?.()} + + {#if visuals} + {#if variant === 'underline'} + + {:else} + + {/if} + {#if overlay} + {#if variant === 'underline'} + diff --git a/src/lib/Button/ButtonSurface.svelte b/src/lib/Button/ButtonSurface.svelte new file mode 100644 index 0000000..1fa048c --- /dev/null +++ b/src/lib/Button/ButtonSurface.svelte @@ -0,0 +1,113 @@ + + + + {#if presentation} + + {/if} + + + {@render icon?.()} + {@render children?.()} + + + + + diff --git a/src/lib/Button/ClipAwareButton.stories.svelte b/src/lib/Button/ClipAwareButton.stories.svelte new file mode 100644 index 0000000..b86081f --- /dev/null +++ b/src/lib/Button/ClipAwareButton.stories.svelte @@ -0,0 +1,236 @@ + + +{#snippet interactive(args: Args)} +
+
+ Join the jam +
+ +
+{/snippet} + +{#snippet presentation(args: Args)} +
+
+ Join the jam +
+ +
+{/snippet} + + { + const canvas = within(canvasElement); + const host = canvasElement.querySelector('[data-clip-aware-button]'); + await expect(host).not.toBeNull(); + if (!host) return; + + const baseLayer = host.querySelector('[data-story-button-layer="base"]'); + const inverseLayer = host.querySelector('[data-story-button-layer="inverse"]'); + const baseVisual = host.querySelector('[data-clip-aware-visual="base"]'); + const inverseVisual = host.querySelector('[data-clip-aware-visual="inverse"]'); + const button = canvas.getByRole('button', { name: 'Join the jam' }); + const surfaces = host.querySelectorAll('[data-button-surface]'); + const baseSurface = baseVisual?.querySelector('[data-button-surface]'); + const inverseSurface = inverseVisual?.querySelector('[data-button-surface]'); + + await expect(baseLayer).not.toBeNull(); + await expect(inverseLayer).not.toBeNull(); + await expect(baseVisual).not.toBeNull(); + await expect(inverseVisual).not.toBeNull(); + await expect(baseSurface).not.toBeNull(); + await expect(inverseSurface).not.toBeNull(); + if ( + !baseLayer || + !inverseLayer || + !baseVisual || + !inverseVisual || + !baseSurface || + !inverseSurface + ) + return; + + await expect(host.querySelectorAll(':scope > .layer')).toHaveLength(0); + await expect(surfaces).toHaveLength(2); + await expect(canvas.getAllByRole('button', { name: 'Join the jam' })).toHaveLength(1); + await expect(canvas.queryByRole('link', { name: 'Join the jam' })).toBeNull(); + await expect(baseVisual.closest('[data-story-button-layer]')).toBe(baseLayer); + await expect(inverseVisual.closest('[data-story-button-layer]')).toBe(inverseLayer); + await expect(button.closest('[data-story-button-layer]')).toBe(baseLayer); + await expect(inverseLayer).toHaveAttribute('aria-hidden', 'true'); + await expect(inverseLayer).toHaveAttribute('inert'); + await expect(inverseSurface).toHaveAttribute('aria-hidden', 'true'); + await expect(inverseSurface).toHaveAttribute('inert'); + await expect(getComputedStyle(baseLayer).clipPath).toBe('none'); + await expect(getComputedStyle(inverseLayer).clipPath).toBe('none'); + await expect(getComputedStyle(inverseLayer).pointerEvents).toBe('none'); + await expect(getComputedStyle(button).pointerEvents).toBe('auto'); + await expect(host.style.getPropertyValue('--clip-aware-path')).toBe(''); + + await expectMatchingBounds(baseSurface, inverseSurface); + await expectMatchingBounds(baseSurface, button); + + const idleTransforms = [...surfaces].map((surface) => getComputedStyle(surface).transform); + host.classList.add('pseudo-hover-all'); + await waitFor(async () => { + const hoverTransforms = [...surfaces].map((surface) => getComputedStyle(surface).transform); + + await expect(hoverTransforms[1]).toBe(hoverTransforms[0]); + for (const [index, transform] of hoverTransforms.entries()) { + const idleTransform = idleTransforms[index]; + if (idleTransform === undefined) throw new Error('Missing idle surface transform'); + + await expect(transformScale(transform)).toBeGreaterThan(transformScale(idleTransform)); + } + }); + host.classList.remove('pseudo-hover-all'); + await waitFor(async () => { + const resetTransforms = [...surfaces].map((surface) => getComputedStyle(surface).transform); + + await expect(resetTransforms).toEqual(idleTransforms); + }); + + host.classList.add('pseudo-active-all'); + const activeTransforms = [baseVisual, inverseVisual].map( + (visual) => getComputedStyle(visual).transform, + ); + await expect(activeTransforms[0]).not.toBe('none'); + await expect(activeTransforms[1]).toBe(activeTransforms[0]); + host.classList.remove('pseudo-active-all'); + + let activations = 0; + button.addEventListener('click', () => { + activations += 1; + }); + + await userEvent.click(button); + await expect(activations).toBe(1); + + button.focus(); + await userEvent.keyboard('{Enter}'); + await expect(activations).toBe(2); + + button.blur(); + canvasElement.ownerDocument.body.focus(); + await userEvent.tab(); + await expect(button).toHaveFocus(); + host.classList.add('pseudo-focus-visible-all'); + for (const visual of [baseVisual, inverseVisual]) { + await expect(getComputedStyle(visual).outlineColor).not.toBe('rgba(0, 0, 0, 0)'); + } + host.classList.remove('pseudo-focus-visible-all'); + + button.toggleAttribute('disabled', true); + await expect(button).toBeDisabled(); + }} +/> + + { + const canvas = within(canvasElement); + const host = canvasElement.querySelector('[data-clip-aware-button]'); + await expect(host).not.toBeNull(); + if (!host) return; + + await expect(canvas.queryByRole('button', { name: 'Join the jam' })).not.toBeInTheDocument(); + await expect(canvas.queryByRole('link', { name: 'Join the jam' })).not.toBeInTheDocument(); + await expect(host.querySelectorAll('[data-clip-aware-visual]')).toHaveLength(2); + await expect(host.querySelectorAll('[data-button-surface]')).toHaveLength(2); + await expect(host.querySelector('.semantic-control')).toBeNull(); + }} +/> + + { + const canvas = within(canvasElement); + const link = canvas.getByRole('link', { name: 'Join the jam' }); + const host = canvasElement.querySelector('[data-clip-aware-button]'); + + await expect(host).not.toBeNull(); + if (!host) return; + + await expect(canvas.queryByRole('button', { name: 'Join the jam' })).toBeNull(); + await expect(canvas.getAllByRole('link', { name: 'Join the jam' })).toHaveLength(1); + await expect(link).toHaveAttribute('href', '#join'); + await expect(link).toHaveAttribute('rel', 'external'); + await expect(link).toHaveAttribute('target', '_blank'); + await expect(getComputedStyle(link).pointerEvents).toBe('auto'); + await expect(host.querySelectorAll('[data-button-surface]')).toHaveLength(2); + }} +/> diff --git a/src/lib/Button/ClipAwareButton.svelte b/src/lib/Button/ClipAwareButton.svelte new file mode 100644 index 0000000..ae8d01a --- /dev/null +++ b/src/lib/Button/ClipAwareButton.svelte @@ -0,0 +1,320 @@ + + +{#snippet baseLayer()} + + + {#if control && layer !== 'inverse'} + {#if control.href != null} + + + L {swatch.color.l.toFixed(2)} · C {swatch.color.c.toFixed(3)} + {#if swatch.color.h !== undefined}· H {swatch.color.h.toFixed(1)}°{/if} + + + {/each} + + {:else} +

No colors were provided.

+ {/if} + + + diff --git a/src/lib/ColorPalette/HueChromaMinimap.svelte b/src/lib/ColorPalette/HueChromaMinimap.svelte new file mode 100644 index 0000000..3bd7d96 --- /dev/null +++ b/src/lib/ColorPalette/HueChromaMinimap.svelte @@ -0,0 +1,208 @@ + + +
+ + {title} + + + {#each map.gradientStops as stop (stop.hue)} + + {/each} + + + + + + + + + + + + {#each map.boundaries as boundary (boundary.gamut)} + + + + + {/each} + + {#each map.boundaries as boundary (boundary.gamut)} + gamutRank[gamut]} + class:smaller={gamutRank[boundary.gamut] < gamutRank[gamut]} + d={linePath(boundary.points, maximumChroma)} + /> + {/each} + + + {#if color.h !== undefined} + + {/if} + + + + C + H + + + +
+ + diff --git a/src/lib/ColorPalette/gamutMap.ts b/src/lib/ColorPalette/gamutMap.ts new file mode 100644 index 0000000..475ffa8 --- /dev/null +++ b/src/lib/ColorPalette/gamutMap.ts @@ -0,0 +1,115 @@ +import { formatCss, inGamut, type Mode, type Oklch } from 'culori'; + +export type ColorGamut = 'srgb' | 'display-p3' | 'rec2020' | 'beyond-rec2020'; +export type DisplayGamut = Exclude; + +export interface GamutBoundaryPoint { + hue: number; + chroma: number; +} + +export interface GamutBoundary { + gamut: DisplayGamut; + label: string; + points: GamutBoundaryPoint[]; +} + +export interface HueGradientStop extends GamutBoundaryPoint { + css: string; +} + +export interface HueChromaMap { + boundaries: GamutBoundary[]; + gradientStops: HueGradientStop[]; + neutralCss: string; +} + +export const gamutLabels: Record = { + srgb: 'sRGB', + 'display-p3': 'P3', + rec2020: 'Rec. 2020', + 'beyond-rec2020': 'Beyond Rec. 2020', +}; + +const boundaryDefinitions = [ + { gamut: 'rec2020', label: gamutLabels.rec2020, mode: 'rec2020' }, + { gamut: 'display-p3', label: gamutLabels['display-p3'], mode: 'p3' }, + { gamut: 'srgb', label: gamutLabels.srgb, mode: 'rgb' }, +] as const satisfies readonly { gamut: DisplayGamut; label: string; mode: Mode }[]; + +const gamutChecks = { + srgb: inGamut('rgb'), + 'display-p3': inGamut('p3'), + rec2020: inGamut('rec2020'), +} satisfies Record>; + +const HUE_STEP = 6; +const GRADIENT_STEP = 30; +const SEARCH_LIMIT = 0.8; +const SEARCH_STEPS = 12; +const mapCache = new Map(); + +export function classifyGamut(color: Oklch): ColorGamut { + if (gamutChecks.srgb(color)) return 'srgb'; + if (gamutChecks['display-p3'](color)) return 'display-p3'; + if (gamutChecks.rec2020(color)) return 'rec2020'; + return 'beyond-rec2020'; +} + +function maximumChroma(lightness: number, hue: number, mode: Mode): number { + const contains = inGamut(mode); + let lower = 0; + let upper = 0.1; + + while (upper < SEARCH_LIMIT && contains({ mode: 'oklch', l: lightness, c: upper, h: hue })) { + lower = upper; + upper = Math.min(upper * 2, SEARCH_LIMIT); + } + + if (upper === SEARCH_LIMIT && contains({ mode: 'oklch', l: lightness, c: upper, h: hue })) { + return upper; + } + + for (let index = 0; index < SEARCH_STEPS; index += 1) { + const candidate = (lower + upper) / 2; + if (contains({ mode: 'oklch', l: lightness, c: candidate, h: hue })) { + lower = candidate; + } else { + upper = candidate; + } + } + + return lower; +} + +export function createHueChromaMap(lightness: number): HueChromaMap { + const cacheKey = Math.round(lightness * 1_000_000); + const cached = mapCache.get(cacheKey); + if (cached) return cached; + + const boundaries = boundaryDefinitions.map(({ gamut, label, mode }) => ({ + gamut, + label, + points: Array.from({ length: 360 / HUE_STEP + 1 }, (_, index) => { + const hue = index * HUE_STEP; + return { hue, chroma: maximumChroma(lightness, hue, mode) }; + }), + })); + const rec2020Boundary = boundaries[0]; + if (!rec2020Boundary) throw new Error('Missing Rec. 2020 gamut boundary'); + const gradientStops = rec2020Boundary.points + .filter(({ hue }) => hue % GRADIENT_STEP === 0) + .map(({ hue, chroma }) => ({ + hue, + chroma, + css: formatCss({ mode: 'oklch', l: lightness, c: chroma, h: hue }), + })); + const map = { + boundaries, + gradientStops, + neutralCss: formatCss({ mode: 'oklch', l: lightness, c: 0, h: 0 }), + }; + + mapCache.set(cacheKey, map); + return map; +} diff --git a/src/lib/ColorPalette/palette.test.ts b/src/lib/ColorPalette/palette.test.ts new file mode 100644 index 0000000..0b3ebb6 --- /dev/null +++ b/src/lib/ColorPalette/palette.test.ts @@ -0,0 +1,141 @@ +import { inGamut, oklch, parse } from 'culori'; +import { describe, expect, it } from 'vitest'; + +import { classifyGamut, createHueChromaMap } from './gamutMap'; +import { createColorPalette, standardColorStops } from './palette'; + +describe('createColorPalette', () => { + const target = oklch('oklch(50% 0.2 180)'); + if (!target) throw new Error('Invalid test color'); + + const palette = createColorPalette({ + color: target, + hue: { stops: [0.25, -0.5], interpolator: 'interpolatorLinear' }, + chroma: { stops: [0.25, -0.25], interpolator: 'interpolatorLinear' }, + }); + + it('uses the standard lightness stops', () => { + expect(palette.map(({ stop }) => stop)).toEqual(standardColorStops); + palette.forEach(({ stop, color }) => { + expect(color.l).toBeCloseTo(1 - stop / 1000); + }); + }); + + it('interpolates one continuous curve through stop 500', () => { + const middle = palette.find(({ stop }) => stop === 500); + expect(middle?.color.h).toBeCloseTo(135); + expect(middle?.color.c).toBeCloseTo(0.2); + }); + + it('anchors start at the lightest stop and stop at the dimmest stop', () => { + const lightEdge = palette.find(({ stop }) => stop === 50); + const darkEdge = palette.find(({ stop }) => stop === 950); + + expect(lightEdge?.color.h).toBeCloseTo(270); + expect(lightEdge?.color.c).toBeCloseTo(0.3); + expect(darkEdge?.color.h).toBeCloseTo(0); + expect(darkEdge?.color.c).toBeCloseTo(0.1); + }); + + it('interpolates through additional evenly spaced stops', () => { + const paletteWithMiddleStop = createColorPalette({ + color: target, + hue: { stops: [0, 0.25, 0], interpolator: 'interpolatorLinear' }, + chroma: { stops: [0, 0.25, 0], interpolator: 'interpolatorLinear' }, + }); + const middle = paletteWithMiddleStop.find(({ stop }) => stop === 500); + + expect(middle?.color.h).toBeCloseTo(270); + expect(middle?.color.c).toBeCloseTo(0.3); + }); + + it('requires at least two finite interpolation stops', () => { + expect(() => + createColorPalette({ + color: target, + hue: { stops: [0], interpolator: 'interpolatorLinear' }, + chroma: { stops: [0, 0], interpolator: 'interpolatorLinear' }, + }), + ).toThrow('at least two stops'); + + expect(() => + createColorPalette({ + color: target, + hue: { stops: [0, Number.NaN], interpolator: 'interpolatorLinear' }, + chroma: { stops: [0, 0], interpolator: 'interpolatorLinear' }, + }), + ).toThrow('finite numbers'); + }); + + it('interprets Display-P3 inputs without clamping generated colors to P3', () => { + const wideTarget = { mode: 'p3', r: 0, g: 0.3, b: 0.9 } as const; + const convertedTarget = oklch(wideTarget); + const widePalette = createColorPalette({ + color: wideTarget, + hue: { stops: [0, 0], interpolator: 'interpolatorLinear' }, + chroma: { stops: [0, 0], interpolator: 'interpolatorLinear' }, + }); + const middle = widePalette.find(({ stop }) => stop === 500); + const isInDisplayP3 = inGamut('p3'); + + expect(middle?.color.c).toBeCloseTo(convertedTarget.c); + expect(middle?.color.h).toBeCloseTo(convertedTarget.h ?? 0); + expect(widePalette.some(({ color }) => !isInDisplayP3(color))).toBe(true); + expect(middle?.gamut).toBe('display-p3'); + expect(middle?.css).toMatch(/^oklch\(/); + }); + + it('interprets sRGB-derived inputs without clamping generated colors to sRGB', () => { + const rgbTarget = parse('#4f18ee'); + if (!rgbTarget) throw new Error('Invalid test color'); + const convertedTarget = oklch(rgbTarget); + const rgbPalette = createColorPalette({ + color: rgbTarget, + hue: { stops: [0, 0], interpolator: 'interpolatorLinear' }, + chroma: { stops: [0, 0], interpolator: 'interpolatorLinear' }, + }); + const middle = rgbPalette.find(({ stop }) => stop === 500); + const isInSrgb = inGamut('rgb'); + + expect(rgbTarget.mode).toBe('rgb'); + expect(middle?.color.c).toBeCloseTo(convertedTarget.c); + expect(middle?.color.h).toBeCloseTo(convertedTarget.h ?? 0); + expect(rgbPalette.some(({ color }) => !isInSrgb(color))).toBe(true); + }); + + it('does not clamp unbounded input spaces such as OKLCH', () => { + const middle = palette.find(({ stop }) => stop === 500); + const isInSrgb = inGamut('rgb'); + + expect(middle?.color.c).toBeCloseTo(0.2); + expect(middle && isInSrgb(middle.color)).toBe(false); + }); + + it('distinguishes Rec. 2020 colors from colors beyond Rec. 2020', () => { + const rec2020Color = oklch({ mode: 'rec2020', r: 0.99, g: 0.01, b: 0.01 }); + const unboundedColor = oklch('oklch(60% 0.7 20)'); + if (!unboundedColor) throw new Error('Invalid test color'); + + expect(classifyGamut(rec2020Color)).toBe('rec2020'); + expect(classifyGamut(unboundedColor)).toBe('beyond-rec2020'); + }); + + it('builds hue-chroma boundaries at a fixed lightness', () => { + const map = createHueChromaMap(0.5); + const [rec2020, displayP3, srgb] = map.boundaries; + + expect(rec2020?.points).toHaveLength(61); + expect(displayP3?.points).toHaveLength(61); + expect(srgb?.points).toHaveLength(61); + for (const boundary of map.boundaries) { + for (const point of boundary.points) { + expect(point.hue).toBeGreaterThanOrEqual(0); + expect(point.hue).toBeLessThanOrEqual(360); + expect(point.chroma).toBeGreaterThan(0); + expect(Number.isFinite(point.chroma)).toBe(true); + } + } + expect(rec2020?.points).not.toEqual(displayP3?.points); + expect(displayP3?.points).not.toEqual(srgb?.points); + }); +}); diff --git a/src/lib/ColorPalette/palette.ts b/src/lib/ColorPalette/palette.ts new file mode 100644 index 0000000..04e53c9 --- /dev/null +++ b/src/lib/ColorPalette/palette.ts @@ -0,0 +1,114 @@ +import { + formatCss, + formatHex, + interpolatorLinear, + interpolatorSplineBasis, + interpolatorSplineBasisClosed, + interpolatorSplineMonotone, + interpolatorSplineMonotone2, + interpolatorSplineMonotoneClosed, + interpolatorSplineNatural, + interpolatorSplineNaturalClosed, + oklch, + type Color, + type Oklch, +} from 'culori'; + +import { classifyGamut, type ColorGamut } from './gamutMap'; + +export const standardColorStops = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const; + +export type ColorStop = (typeof standardColorStops)[number]; + +export const interpolatorNames = [ + 'interpolatorLinear', + 'interpolatorSplineBasis', + 'interpolatorSplineBasisClosed', + 'interpolatorSplineNatural', + 'interpolatorSplineNaturalClosed', + 'interpolatorSplineMonotone', + 'interpolatorSplineMonotone2', + 'interpolatorSplineMonotoneClosed', +] as const; + +export type InterpolatorName = (typeof interpolatorNames)[number]; + +export interface OffsetInterpolation { + /** Evenly spaced normalized offsets from the lightest stop to the dimmest stop. */ + stops: readonly number[]; + interpolator: InterpolatorName; +} + +export interface PaletteConfig { + color: Color; + hue: OffsetInterpolation; + chroma: OffsetInterpolation; +} + +export interface PaletteSwatch { + stop: ColorStop; + gamut: ColorGamut; + color: Oklch; + css: string; + /** An sRGB fallback for browsers without CSS Color 4 support. */ + hex: string; +} + +const LIGHTEST_STOP = standardColorStops[0]; +const DIMMEST_STOP = standardColorStops.at(-1) ?? 950; +const FULL_HUE_TURN = 360; +const OKLCH_FULL_CHROMA = 0.4; + +const interpolators = { + interpolatorLinear, + interpolatorSplineBasis, + interpolatorSplineBasisClosed, + interpolatorSplineNatural, + interpolatorSplineNaturalClosed, + interpolatorSplineMonotone, + interpolatorSplineMonotone2, + interpolatorSplineMonotoneClosed, +} satisfies Record (progress: number) => number>; + +function wrapHue(hue: number): number { + return ((hue % 360) + 360) % 360; +} + +function createOffsetInterpolator(config: OffsetInterpolation): (progress: number) => number { + if (config.stops.length < 2) { + throw new RangeError('Offset interpolation requires at least two stops'); + } + if (!config.stops.every(Number.isFinite)) { + throw new RangeError('Offset interpolation stops must be finite numbers'); + } + + return interpolators[config.interpolator]([...config.stops]); +} + +export function createColorPalette(config: PaletteConfig): PaletteSwatch[] { + const { color: targetColor, hue: hueOffset, chroma: chromaOffset } = config; + const target = oklch(targetColor); + const interpolateHueOffset = createOffsetInterpolator(hueOffset); + const interpolateChromaOffset = createOffsetInterpolator(chromaOffset); + + return standardColorStops.map((stop) => { + const progress = (stop - LIGHTEST_STOP) / (DIMMEST_STOP - LIGHTEST_STOP); + const normalizedHueOffset = interpolateHueOffset(progress); + const normalizedChromaOffset = interpolateChromaOffset(progress); + const hue = target.h; + const color: Oklch = { + mode: 'oklch', + l: 1 - stop / 1000, + c: Math.max(0, target.c + normalizedChromaOffset * OKLCH_FULL_CHROMA), + ...(hue === undefined ? {} : { h: wrapHue(hue + normalizedHueOffset * FULL_HUE_TURN) }), + ...(target.alpha === undefined ? {} : { alpha: target.alpha }), + }; + return { + stop, + gamut: classifyGamut(color), + color, + css: formatCss(color), + hex: formatHex(color), + }; + }); +} diff --git a/src/lib/ErodedCheckerboard/ErodedCheckerboard.stories.svelte b/src/lib/ErodedCheckerboard/ErodedCheckerboard.stories.svelte new file mode 100644 index 0000000..781f039 --- /dev/null +++ b/src/lib/ErodedCheckerboard/ErodedCheckerboard.stories.svelte @@ -0,0 +1,42 @@ + + +{#snippet template(args: Args)} +
+ +
+{/snippet} + + diff --git a/src/lib/ErodedCheckerboard/ErodedCheckerboard.svelte b/src/lib/ErodedCheckerboard/ErodedCheckerboard.svelte new file mode 100644 index 0000000..1a74991 --- /dev/null +++ b/src/lib/ErodedCheckerboard/ErodedCheckerboard.svelte @@ -0,0 +1,74 @@ + + + + + diff --git a/src/lib/ErodedCheckerboard/erodedCheckerboardRenderer.ts b/src/lib/ErodedCheckerboard/erodedCheckerboardRenderer.ts new file mode 100644 index 0000000..9165a24 --- /dev/null +++ b/src/lib/ErodedCheckerboard/erodedCheckerboardRenderer.ts @@ -0,0 +1,284 @@ +import * as twgl from 'twgl.js'; + +const VERTEX_SHADER = /* glsl */ `#version 300 es +in vec2 position; + +out vec2 textureCoordinate; + +void main() { + textureCoordinate = position * 0.5 + 0.5; + gl_Position = vec4(position, 0.0, 1.0); +}`; + +const PATTERN_FRAGMENT_SHADER = /* glsl */ `#version 300 es +precision highp float; + +out vec4 fragmentColor; + +uniform vec2 uLogicalResolution; +uniform vec2 uSourceScale; +uniform vec2 uFieldSize; +uniform float uCheckerSize; +uniform float uPixelSize; +uniform float uSeed; +uniform float uMaxErosion; +uniform float uFalloff; + +float hash(vec2 point) { + point = fract(point * vec2(123.34, 456.21)); + point += dot(point, point + 45.32 + uSeed); + return fract(point.x * point.y); +} + +float valueNoise(vec2 point) { + vec2 cell = floor(point); + vec2 offset = fract(point); + offset = offset * offset * (3.0 - 2.0 * offset); + + float bottomLeft = hash(cell); + float bottomRight = hash(cell + vec2(1.0, 0.0)); + float topLeft = hash(cell + vec2(0.0, 1.0)); + float topRight = hash(cell + vec2(1.0, 1.0)); + + return mix( + mix(bottomLeft, bottomRight, offset.x), + mix(topLeft, topRight, offset.x), + offset.y + ); +} + +void main() { + vec2 pixel = gl_FragCoord.xy / uSourceScale; + vec2 checkerCoordinate = pixel / uCheckerSize; + vec2 checkerCell = floor(checkerCoordinate); + vec2 checkerPosition = fract(checkerCoordinate); + + vec2 fieldCenter = uLogicalResolution * 0.5; + vec2 fieldPosition = (pixel - fieldCenter) / (uFieldSize * 0.5); + float radialDistance = length(fieldPosition); + float broadFieldNoise = valueNoise( + checkerCell * 0.045 + vec2(31.7, -18.4) + ) - 0.5; + float detailFieldNoise = valueNoise( + checkerCell * 0.14 + vec2(-9.2, 47.1) + ) - 0.5; + float signedErosion = + uMaxErosion * + (1.0 - pow(radialDistance / 1.14276, uFalloff)) + + broadFieldNoise * 0.035 + + detailFieldNoise * 0.012; + float erosionAmount = abs(signedErosion); + if (signedErosion > 0.28) { + float excessErosion = signedErosion - 0.28; + erosionAmount = + 0.28 + + excessErosion * 0.426 + + excessErosion * excessErosion * 0.345; + } else if (signedErosion < -0.32) { + float excessErosion = -signedErosion - 0.32; + erosionAmount = + 0.32 + + excessErosion * 0.4 + + excessErosion * excessErosion * 0.2; + } + erosionAmount = max( + erosionAmount + + (hash(checkerCell + vec2(37.1, 89.7)) - 0.5) * 0.16, + 0.0 + ); + bool originalWhiteCell = mod(checkerCell.x + checkerCell.y, 2.0) < 1.0; + bool affectedCell = originalWhiteCell != (signedErosion >= 0.0); + + float macroGridSize = max(round(uCheckerSize / uPixelSize), 2.0); + vec2 macroPixel = min( + floor(checkerPosition * macroGridSize), + vec2(macroGridSize - 1.0) + ); + float maxCornerDepth = floor((macroGridSize - 1.0) * 0.5) * 2.0; + vec2 corner = step(vec2((macroGridSize - 1.0) * 0.5), macroPixel); + vec2 cornerPixel = mix( + macroPixel, + vec2(macroGridSize - 1.0) - macroPixel, + corner + ); + float horizontalWeight = mix( + 0.82, + 1.18, + hash(checkerCell * vec2(41.0, 17.0) + corner * vec2(131.0, 71.0) + 53.7) + ); + vec2 cornerWeights = vec2(horizontalWeight, 2.0 - horizontalWeight); + float frontierOrder = hash( + checkerCell * vec2(23.0, 29.0) + + corner * vec2(113.0, 197.0) + + cornerPixel * vec2(7.1, 13.7) + + 97.3 + ); + float cornerDepth = + dot(cornerPixel, cornerWeights) + frontierOrder * 0.7; + float removalThreshold = + (cornerDepth + 0.08) / (maxCornerDepth + 1.0); + + bool corePixel = erosionAmount < removalThreshold; + + bool whitePixel = originalWhiteCell; + if (affectedCell) { + whitePixel = originalWhiteCell ? corePixel : !corePixel; + } + + float coverage = whitePixel ? 1.0 : 0.0; + fragmentColor = vec4(coverage, coverage, coverage, 1.0); +}`; + +const PRESENT_FRAGMENT_SHADER = /* glsl */ `#version 300 es +precision highp float; + +in vec2 textureCoordinate; +out vec4 fragmentColor; + +uniform sampler2D uPattern; +uniform vec2 uPatternTexelSize; + +void main() { + vec2 sampleOffset = uPatternTexelSize * 2.0; + float coverage = + texture(uPattern, textureCoordinate + vec2(-sampleOffset.x, -sampleOffset.y)).r + + texture(uPattern, textureCoordinate + vec2(0.0, -sampleOffset.y)).r * 2.0 + + texture(uPattern, textureCoordinate + vec2(sampleOffset.x, -sampleOffset.y)).r + + texture(uPattern, textureCoordinate + vec2(-sampleOffset.x, 0.0)).r * 2.0 + + texture(uPattern, textureCoordinate).r * 4.0 + + texture(uPattern, textureCoordinate + vec2(sampleOffset.x, 0.0)).r * 2.0 + + texture(uPattern, textureCoordinate + vec2(-sampleOffset.x, sampleOffset.y)).r + + texture(uPattern, textureCoordinate + vec2(0.0, sampleOffset.y)).r * 2.0 + + texture(uPattern, textureCoordinate + sampleOffset).r; + coverage /= 16.0; + coverage = round(coverage * 8.0) / 8.0; + fragmentColor = vec4(vec3(coverage), 1.0); +}`; + +export interface ErodedCheckerboardRenderOptions { + width: number; + height: number; + devicePixelRatio: number; + maxFieldWidth?: number | undefined; + maxFieldHeight?: number | undefined; + checkerSize: number; + pixelSize: number; + seed: number; + maxErosion: number; + falloff: number; +} + +export interface ErodedCheckerboardRenderer { + render: (options: ErodedCheckerboardRenderOptions) => void; + destroy: () => void; +} + +export function createErodedCheckerboardRenderer( + canvas: HTMLCanvasElement, +): ErodedCheckerboardRenderer | undefined { + const gl = canvas.getContext('webgl2', { + alpha: false, + antialias: false, + depth: false, + powerPreference: 'low-power', + stencil: false, + }); + if (!gl) return; + + const patternProgramInfo = twgl.createProgramInfo(gl, [VERTEX_SHADER, PATTERN_FRAGMENT_SHADER]); + const presentProgramInfo = twgl.createProgramInfo(gl, [VERTEX_SHADER, PRESENT_FRAGMENT_SHADER]); + const bufferInfo = twgl.createBufferInfoFromArrays(gl, { + position: { + numComponents: 2, + data: [-1, -1, 3, -1, -1, 3], + }, + }); + const framebufferAttachments: twgl.AttachmentOptions[] = [ + { + format: gl.RGBA, + internalFormat: gl.RGBA8, + type: gl.UNSIGNED_BYTE, + min: gl.LINEAR, + mag: gl.NEAREST, + wrap: gl.CLAMP_TO_EDGE, + }, + ]; + const patternFramebuffer = twgl.createFramebufferInfo(gl, framebufferAttachments, 1, 1); + const patternTexture = patternFramebuffer.attachments[0] as WebGLTexture; + const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number; + + return { + render(options) { + const outputScale = Math.max(options.devicePixelRatio, 1); + const resolutionWidth = Math.max(1, Math.round(options.width * outputScale)); + const resolutionHeight = Math.max(1, Math.round(options.height * outputScale)); + const sourceScale = Math.max( + 1, + Math.min( + outputScale, + maxTextureSize / Math.max(options.width, 1), + maxTextureSize / Math.max(options.height, 1), + ), + ); + const sourceWidth = Math.max(1, Math.round(options.width * sourceScale)); + const sourceHeight = Math.max(1, Math.round(options.height * sourceScale)); + const fieldWidth = Math.max(options.maxFieldWidth ?? options.width, 1); + const fieldHeight = Math.max(options.maxFieldHeight ?? options.height, 1); + + if (canvas.width !== resolutionWidth || canvas.height !== resolutionHeight) { + canvas.width = resolutionWidth; + canvas.height = resolutionHeight; + } + if (patternFramebuffer.width !== sourceWidth || patternFramebuffer.height !== sourceHeight) { + twgl.resizeFramebufferInfo( + gl, + patternFramebuffer, + framebufferAttachments, + sourceWidth, + sourceHeight, + ); + } + + twgl.bindFramebufferInfo(gl, patternFramebuffer); + gl.disable(gl.BLEND); + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.useProgram(patternProgramInfo.program); + twgl.setBuffersAndAttributes(gl, patternProgramInfo, bufferInfo); + twgl.setUniforms(patternProgramInfo, { + uLogicalResolution: [options.width, options.height], + uSourceScale: [ + sourceWidth / Math.max(options.width, 1), + sourceHeight / Math.max(options.height, 1), + ], + uFieldSize: [fieldWidth, fieldHeight], + uCheckerSize: Math.max(options.checkerSize, 1), + uPixelSize: Math.max(Math.min(options.pixelSize, options.checkerSize), 0.25), + uSeed: options.seed, + uMaxErosion: Math.min(Math.max(options.maxErosion, 0), 1), + uFalloff: Math.max(options.falloff, 0.01), + }); + twgl.drawBufferInfo(gl, bufferInfo, gl.TRIANGLES); + + twgl.bindFramebufferInfo(gl, null); + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.useProgram(presentProgramInfo.program); + twgl.setBuffersAndAttributes(gl, presentProgramInfo, bufferInfo); + twgl.setUniforms(presentProgramInfo, { + uPattern: patternTexture, + uPatternTexelSize: [1 / sourceWidth, 1 / sourceHeight], + }); + twgl.drawBufferInfo(gl, bufferInfo, gl.TRIANGLES); + }, + destroy() { + for (const attribute of Object.values(bufferInfo.attribs ?? {})) { + gl.deleteBuffer(attribute.buffer); + } + gl.deleteTexture(patternTexture); + gl.deleteFramebuffer(patternFramebuffer.framebuffer); + gl.deleteProgram(patternProgramInfo.program); + gl.deleteProgram(presentProgramInfo.program); + }, + }; +} diff --git a/src/lib/Head.svelte b/src/lib/Head.svelte new file mode 100644 index 0000000..786e772 --- /dev/null +++ b/src/lib/Head.svelte @@ -0,0 +1,17 @@ + + + + {title} + + + + + + + diff --git a/src/lib/LandingHero/LandingHero.stories.svelte b/src/lib/LandingHero/LandingHero.stories.svelte new file mode 100644 index 0000000..c47d5f4 --- /dev/null +++ b/src/lib/LandingHero/LandingHero.stories.svelte @@ -0,0 +1,321 @@ + + + { + await expectAccessibleLayers(canvasElement); + await expectLandingSemantics(canvasElement); + await expectDestinationCta(canvasElement); + await expectPuddleProfile(canvasElement, 'compact'); + await expectNoHorizontalOverflow(canvasElement); + }} +/> + + { + await expectAccessibleLayers(canvasElement); + await expectLandingSemantics(canvasElement); + await expectDestinationCta(canvasElement); + await expectPuddleProfile(canvasElement, 'medium'); + await expectNoHorizontalOverflow(canvasElement); + }} +/> + + { + await expectAccessibleLayers(canvasElement); + await expectLandingSemantics(canvasElement); + await expectDestinationCta(canvasElement); + await expectPuddleProfile(canvasElement, 'expanded'); + await expectNoHorizontalOverflow(canvasElement); + }} +/> + + { + await expectAccessibleLayers(canvasElement); + await expectLandingSemantics(canvasElement); + await expectDestinationCta(canvasElement); + await expectPuddleProfile(canvasElement, 'large'); + await expectNoHorizontalOverflow(canvasElement); + }} +/> + + { + await expectDestinationCta(canvasElement); + await expectNoHorizontalOverflow(canvasElement); + }} +/> + + { + await expectAccessibleLayers(canvasElement); + await expectLandingSemantics(canvasElement); + await expectNoCta(canvasElement); + await expectPuddleProfile(canvasElement, 'expanded'); + await expectNoHorizontalOverflow(canvasElement); + }} +/> + + { + await expectAccessibleLayers(canvasElement); + await expectLandingSemantics(canvasElement); + await expectDestinationCta(canvasElement); + await expectPuddleProfile(canvasElement, 'expanded'); + await expectNoHorizontalOverflow(canvasElement); + }} +/> + + diff --git a/src/lib/LandingHero/LandingHero.svelte b/src/lib/LandingHero/LandingHero.svelte new file mode 100644 index 0000000..84fed3b --- /dev/null +++ b/src/lib/LandingHero/LandingHero.svelte @@ -0,0 +1,97 @@ + + + + +{#snippet heroActions(layer: LandingHeroLayer)} + {#if actions} + + {/if} +{/snippet} + +{#snippet inverseHero()} + +{/snippet} + +
+
+ + + + +
+
diff --git a/src/lib/LandingHero/LandingHeroContent.svelte b/src/lib/LandingHero/LandingHeroContent.svelte new file mode 100644 index 0000000..5ea7ab1 --- /dev/null +++ b/src/lib/LandingHero/LandingHeroContent.svelte @@ -0,0 +1,123 @@ + + + + +
+ {#if layer === 'base'} +

{dateLabel} {titleLines[0]} {titleLines[1]}

+ {/if} + +
+
+ +

+ SUMMER GAME JAM 2026 +

+
+ + {#if navItems.length > 0} + + {/if} +
+ +
+ +
+ {#if actions} + {@render actions(layer)} + {/if} +
+
+ +
+

{organizerLabel}

+

{locationLabel}

+
+
+ + diff --git a/src/lib/LandingHero/LandingHeroCtaGroup.svelte b/src/lib/LandingHero/LandingHeroCtaGroup.svelte new file mode 100644 index 0000000..775ff86 --- /dev/null +++ b/src/lib/LandingHero/LandingHeroCtaGroup.svelte @@ -0,0 +1,65 @@ + + +
+ + {props.actionSet.discord.label} + + + + {props.actionSet.tickets.label} + + +
+ + diff --git a/src/lib/LandingHero/LandingHeroLayers.svelte b/src/lib/LandingHero/LandingHeroLayers.svelte new file mode 100644 index 0000000..28b9424 --- /dev/null +++ b/src/lib/LandingHero/LandingHeroLayers.svelte @@ -0,0 +1,60 @@ + + +{#if layer === 'base' || layer === 'both'} +
+ +
+{/if} +{#if layer === 'inverse' || layer === 'both'} + +{/if} diff --git a/src/lib/LandingHero/puddleProfiles.ts b/src/lib/LandingHero/puddleProfiles.ts new file mode 100644 index 0000000..8533948 --- /dev/null +++ b/src/lib/LandingHero/puddleProfiles.ts @@ -0,0 +1,39 @@ +import type { PuddleProps } from '../Puddle/config'; +import { PUDDLE_DEFAULTS } from '../Puddle/config'; + +export type LandingPuddleProfileName = 'compact' | 'medium' | 'expanded' | 'large'; + +export type LandingPuddleProfile = Required< + Pick +>; + +export const LANDING_PUDDLE_PROFILES = { + compact: { + bowlWidth: 650, + bowlHeight: 300, + level: 0.4, + threshold: 0.05, + cellSize: 18, + }, + medium: { + bowlWidth: 700, + bowlHeight: 320, + level: 0.405, + threshold: 0.045, + cellSize: 26, + }, + expanded: { + bowlWidth: 760, + bowlHeight: 360, + level: PUDDLE_DEFAULTS.level, + threshold: PUDDLE_DEFAULTS.threshold, + cellSize: PUDDLE_DEFAULTS.cellSize, + }, + large: { + bowlWidth: PUDDLE_DEFAULTS.bowlWidth, + bowlHeight: PUDDLE_DEFAULTS.bowlHeight, + level: PUDDLE_DEFAULTS.level, + threshold: PUDDLE_DEFAULTS.threshold, + cellSize: PUDDLE_DEFAULTS.cellSize, + }, +} as const satisfies Record; diff --git a/src/lib/LandingHero/storyConfig.ts b/src/lib/LandingHero/storyConfig.ts new file mode 100644 index 0000000..9393b85 --- /dev/null +++ b/src/lib/LandingHero/storyConfig.ts @@ -0,0 +1,102 @@ +export interface LandingHeroStoryArgs { + readonly titleLines: readonly [string, string]; + readonly dateLabel: string; + readonly organizerLabel: string; + readonly locationLabel: string; + readonly navItems?: readonly { label: string; href: string }[]; + readonly actions?: + | { + discord: { label: string; href: string }; + tickets: { label: string; href: string }; + } + | undefined; + readonly class?: string; +} + +export const LANDING_HERO_EVENT_ARGS = { + titleLines: ['SUMMER SUMMIT', 'GAME JAM 2026'], + dateLabel: 'SEPTEMBER 4–6', + organizerLabel: 'GAME DEV CLUB X IATSU 2026', + locationLabel: 'SFU BURNABY CAMPUS', + navItems: [], + actions: { + discord: { label: 'Join the Discord', href: 'https://discord.gg/jmZ8jmWHBx' }, + tickets: { + label: 'Get your ticket', + href: 'https://www.eventbrite.ca/e/summer-summit-game-jam-2026-tickets-1994789136004', + }, + }, +} satisfies LandingHeroStoryArgs; + +export const LANDING_HERO_VIEWPORTS = { + compact: { + name: 'Compact portrait', + styles: { width: '390px', height: '844px' }, + type: 'mobile', + }, + medium: { + name: 'Medium', + styles: { width: '768px', height: '1024px' }, + type: 'tablet', + }, + expanded: { + name: 'Expanded', + styles: { width: '1280px', height: '800px' }, + type: 'desktop', + }, + large: { + name: 'Large desktop', + styles: { width: '1920px', height: '1080px' }, + type: 'desktop', + }, +} as const; + +type LandingHeroViewportName = keyof typeof LANDING_HERO_VIEWPORTS; + +interface LandingHeroStoryCase { + readonly name: string; + readonly screenshotName: string; + readonly viewport: LandingHeroViewportName; + readonly args: LandingHeroStoryArgs; +} + +export const LANDING_HERO_STORY_CASES = { + compact: { + name: 'Compact Portrait', + screenshotName: 'compact-portrait', + viewport: 'compact', + args: LANDING_HERO_EVENT_ARGS, + }, + medium: { + name: 'Medium', + screenshotName: 'medium', + viewport: 'medium', + args: LANDING_HERO_EVENT_ARGS, + }, + expanded: { + name: 'Expanded', + screenshotName: 'expanded', + viewport: 'expanded', + args: LANDING_HERO_EVENT_ARGS, + }, + large: { + name: 'Large Desktop', + screenshotName: 'large-desktop', + viewport: 'large', + args: LANDING_HERO_EVENT_ARGS, + }, + withoutCta: { + name: 'Without CTA', + screenshotName: 'without-cta', + viewport: 'expanded', + args: { ...LANDING_HERO_EVENT_ARGS, actions: undefined }, + }, + reducedMotion: { + name: 'Reduced Motion', + screenshotName: 'reduced-motion', + viewport: 'expanded', + args: LANDING_HERO_EVENT_ARGS, + }, +} as const satisfies Record; + +export const LANDING_HERO_VISUAL_CASES = Object.values(LANDING_HERO_STORY_CASES); diff --git a/src/lib/LandingHero/types.ts b/src/lib/LandingHero/types.ts new file mode 100644 index 0000000..d218751 --- /dev/null +++ b/src/lib/LandingHero/types.ts @@ -0,0 +1,24 @@ +export interface LandingHeroNavItem { + label: string; + href: string; +} + +export interface LandingHeroAction { + label: string; + href: string; +} + +export interface LandingHeroActions { + discord: LandingHeroAction; + tickets: LandingHeroAction; +} + +export interface LandingHeroProps { + titleLines: readonly [string, string]; + dateLabel: string; + organizerLabel: string; + locationLabel: string; + navItems?: readonly LandingHeroNavItem[]; + actions?: LandingHeroActions | undefined; + class?: string; +} diff --git a/src/lib/Puddle/Puddle.ssr.test.ts b/src/lib/Puddle/Puddle.ssr.test.ts new file mode 100644 index 0000000..d613ce2 --- /dev/null +++ b/src/lib/Puddle/Puddle.ssr.test.ts @@ -0,0 +1,45 @@ +import { render } from 'svelte/server'; + +import { expect, test } from 'vitest'; + +import { PUDDLE_SNAPSHOTS } from './generated/puddleSnapshots'; +import Puddle from './Puddle.svelte'; +import PuddleClipFixture from './tests/PuddleClipFixture.svelte'; + +test('SSR contains the generated no-JS silhouette in centered CSS-pixel space', () => { + const { body } = render(Puddle, { props: { animated: false, snapshot: 'compact' } }); + + expect(body).toContain('data-puddle-renderer'); + expect(body).toContain('data-puddle-shape'); + expect(body).toContain(`d="${PUDDLE_SNAPSHOTS.compact.path}"`); + expect(body).toContain(PUDDLE_SNAPSHOTS.compact.clip); + expect(body).toMatch( + /]*style="transform: translate\(50%, 50%\) translate\(/, + ); +}); + +test('SSR wires clipped content to the same local SVG shape as the puddle fill', () => { + const { body } = render(PuddleClipFixture); + + expect(body).toContain('data-puddle-clipped-content'); + expect(body).toContain('data-puddle-clipped-probe'); + expect(body).not.toContain('/); + expect(body).toMatch(/ { + const { body } = render(Puddle, { + props: { animated: false, snapshot: 'compact', responsiveSnapshots: true }, + }); + + for (const name of ['compact', 'medium', 'expanded', 'large']) { + expect(body).toContain(`data-puddle-static-profile="${name}"`); + const snapshot = PUDDLE_SNAPSHOTS[name as keyof typeof PUDDLE_SNAPSHOTS]; + expect(body).toContain(snapshot.path); + expect(body).toContain(snapshot.clip); + } + expect(body).toContain('--puddle-clip-compact:'); + expect(body).toContain('--puddle-clip-large:'); +}); diff --git a/src/lib/Puddle/Puddle.svelte b/src/lib/Puddle/Puddle.svelte new file mode 100644 index 0000000..0f4f4f3 --- /dev/null +++ b/src/lib/Puddle/Puddle.svelte @@ -0,0 +1,261 @@ + + + + + +
+ + + {#if responsiveSnapshots} + {#each responsiveSnapshotNames as name (name)} + {@const staticSnapshot = PUDDLE_SNAPSHOTS[name]} + {@const staticTransform = `translate(50%, 50%) translate(${((-staticSnapshot.nx * staticSnapshot.cellSize) / 2).toString()}px, ${((-staticSnapshot.ny * staticSnapshot.cellSize) / 2).toString()}px)`} + + {/each} + {/if} + {@render children?.()} + {#if clippedChildren} +
+ {@render clippedChildren()} +
+ {/if} +
+ + diff --git a/src/lib/Puddle/config.ts b/src/lib/Puddle/config.ts new file mode 100644 index 0000000..ab25c94 --- /dev/null +++ b/src/lib/Puddle/config.ts @@ -0,0 +1,160 @@ +import type { Snippet } from 'svelte'; +import type { HTMLAttributes } from 'svelte/elements'; + +import type { Color } from 'culori'; + +import { brandColorValues } from '$lib/tokens'; + +import type { IntegratorId } from './sim'; +import type { PuddleSnapshotName } from './snapshot/types'; + +// Omit the deprecated HTML `color` attribute; intersecting with it would +// collapse the prop type to `string` and reject culori Color objects. +/** Descendants inherit `--puddle-clip`; full-host overlays can use it as their `clip-path`. */ +export type PuddleProps = Omit, 'color'> & { + /** Puddle fill color. Any CSS color string or culori Color. */ + color?: string | Color; + /** Display size of one grid cell, in CSS px. Bigger = chunkier blocks. */ + cellSize?: number; + /** Depth above which a cell is painted. */ + threshold?: number; + /** Resting water level; higher fills more of the bowl (bigger puddle). */ + level?: number; + /** Terrain seed; changes the organic edge shape. */ + seed?: number; + /** Low-frequency edge roughness. */ + noiseAmp?: number; + /** Terrain bowl width in CSS pixels. */ + bowlWidth?: number; + /** Terrain bowl height in CSS pixels. */ + bowlHeight?: number; + /** Terrain bowl depth. */ + bowlAmp?: number; + /** Width of the terrain bowl's softened rim. */ + bowlRim?: number; + /** Ripple with raindrops instead of holding the settled shape. Ignored under prefers-reduced-motion. */ + animated?: boolean; + /** Gently tilt gravity toward the pointer so the water leans after the cursor. Needs `animated`; ignored under prefers-reduced-motion. */ + followCursor?: boolean; + /** Simulation pipeline: pipe core, or the momentum second implementation. */ + integrator?: IntegratorId; + /** Momentum-only: grid-scale oscillation damping (0 = off, 1 = full). Applies to `integrator="pipes+momentum"`. */ + momentumSmoothing?: number; + /** Momentum-only: how much carried momentum survives each substep (1 = lossless, lower = calmer). Applies to `integrator="pipes+momentum"`. */ + momentumRetention?: number; + /** Animation speed: sim-seconds advanced per real-second. Needs `animated`. */ + timeScale?: number; + /** Target physics substep size (sim-seconds); smaller = more accurate, costlier. */ + baseSubstep?: number; + /** Per-frame substep ceiling (spiral-of-death guard). Needs `animated`. */ + maxSubsteps?: number; + /** Gravity-wave CFL safety margin (0–1); higher is faster but risks instability. */ + cflSafety?: number; + /** Depth floor in the CFL estimate. */ + minWaveDepth?: number; + /** Sub-steps run to settle poured water before the first paint. */ + settleSubsteps?: number; + /** Drift the puddle's lean with noise so it gently sloshes direction. Needs `animated`. */ + gravityDrift?: boolean; + /** Noise-drift magnitude (only with `gravityDrift`). */ + driftAmp?: number; + /** Noise-drift rate in Hz (only with `gravityDrift`). */ + driftRateHz?: number; + /** Raindrop cadence in sim-seconds (only when `animated`). */ + rainInterval?: number; + /** Depth each raindrop adds (only when `animated`). */ + rainAmount?: number; + /** Raindrop radius in cells (only when `animated`). */ + rainRadius?: number; + /** Resolution guardrail: max grid cells per axis. */ + maxCells?: number; + /** Cursor-follow tilt strength (only with `followCursor`). */ + cursorTilt?: number; + /** Cursor-follow easing time-constant in seconds (only with `followCursor`). */ + cursorEase?: number; + /** Slosh with device rotation after permission is requested. Needs `animated`; ignored under prefers-reduced-motion. */ + deviceGravity?: boolean; + /** Maximum device-motion slosh force. */ + deviceTilt?: number; + /** Device-motion easing time-constant in seconds. */ + deviceEase?: number; + /** Build-generated settled shape/state used before the live worker starts. */ + snapshot?: PuddleSnapshotName; + /** Render CSS-selected landing snapshots until the measured worker shape is ready. */ + responsiveSnapshots?: boolean; + /** Inert visual content clipped to the same local SVG path as the live puddle fill. */ + clippedChildren?: Snippet; + children?: Snippet; +}; + +type PuddleDefaultKey = + | 'color' + | 'cellSize' + | 'threshold' + | 'level' + | 'seed' + | 'noiseAmp' + | 'bowlWidth' + | 'bowlHeight' + | 'bowlAmp' + | 'bowlRim' + | 'animated' + | 'followCursor' + | 'integrator' + | 'momentumSmoothing' + | 'momentumRetention' + | 'timeScale' + | 'baseSubstep' + | 'maxSubsteps' + | 'cflSafety' + | 'minWaveDepth' + | 'settleSubsteps' + | 'gravityDrift' + | 'driftAmp' + | 'driftRateHz' + | 'rainInterval' + | 'rainAmount' + | 'rainRadius' + | 'maxCells' + | 'cursorTilt' + | 'cursorEase' + | 'deviceGravity' + | 'deviceTilt' + | 'deviceEase'; + +/** Default values for every Puddle-specific prop. Spread into stories to match the component. */ +export const PUDDLE_DEFAULTS = { + color: brandColorValues.shade['900'], + cellSize: 28, + threshold: 0.035, + level: 0.42, + seed: 7, + noiseAmp: 0.48, + bowlWidth: 820, + bowlHeight: 400, + bowlAmp: 2.9, + bowlRim: 0.31, + animated: true, + followCursor: true, + integrator: 'pipes+momentum', + momentumSmoothing: 0.1, + momentumRetention: 0, + timeScale: 2, + baseSubstep: 0.08, + maxSubsteps: 8, + cflSafety: 0.9, + minWaveDepth: 0.001, + settleSubsteps: 90, + gravityDrift: true, + driftAmp: 3, + driftRateHz: 0.5, + rainInterval: 0.6, + rainAmount: 0.11, + rainRadius: 1.5, + maxCells: 200, + cursorTilt: 2.5, + cursorEase: 0.05, + deviceGravity: false, + deviceTilt: 6, + deviceEase: 0.05, +} as const satisfies Required>; diff --git a/src/lib/Puddle/device/deviceMotion.test.ts b/src/lib/Puddle/device/deviceMotion.test.ts new file mode 100644 index 0000000..9b7868e --- /dev/null +++ b/src/lib/Puddle/device/deviceMotion.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it } from 'vitest'; + +import { + createDeviceMotionEstimator, + projectDeviceMotion, + requestDeviceGravityPermission, + type DeviceMotionReading, +} from './index'; + +const reading = ( + rotation: [number, number, number] | null, + interval = 16, +): DeviceMotionReading => ({ + rotationRate: rotation ? { alpha: rotation[0], beta: rotation[1], gamma: rotation[2] } : null, + interval, +}); + +describe('projectDeviceMotion', () => { + it('maps device axes to the simulation sign convention in portrait', () => { + expect(projectDeviceMotion({ x: 0, y: 1 }, 0, 6)).toEqual({ x: 0, y: -6 }); + const right = projectDeviceMotion({ x: -1, y: 0 }, 0, 6); + expect(right.x).toBe(-6); + expect(right.y).toBeCloseTo(0); + }); + + it('rotates the device axes into the current screen orientation', () => { + const expectedByAngle = [ + { angle: 0, x: 6, y: 0 }, + { angle: 90, x: 0, y: -6 }, + { angle: 180, x: -6, y: 0 }, + { angle: 270, x: 0, y: 6 }, + ]; + for (const expected of expectedByAngle) { + const tilt = projectDeviceMotion({ x: 1, y: 0 }, expected.angle, 6); + expect(tilt.x).toBeCloseTo(expected.x); + expect(tilt.y).toBeCloseTo(expected.y); + } + }); + + it('sanitizes invalid or negative strength', () => { + for (const tilt of [ + projectDeviceMotion({ x: 1, y: 1 }, 0, -1), + projectDeviceMotion({ x: NaN, y: 1 }, NaN, NaN), + ]) { + expect(tilt.x).toBeCloseTo(0); + expect(tilt.y).toBeCloseTo(0); + } + }); +}); + +describe('requestDeviceGravityPermission', () => { + it('reports unsupported outside a browser', async () => { + await expect(requestDeviceGravityPermission()).resolves.toBe('unsupported'); + }); +}); + +describe('createDeviceMotionEstimator', () => { + it('makes angular acceleration the dominant impulse and retains a smaller velocity term', () => { + const estimator = createDeviceMotionEstimator(); + estimator.update(reading([0, 0, 0]), 0); + estimator.update(reading([0, 90, 0]), 16); + const impulse = estimator.tilt(16, 0, 8)?.x ?? 0; + + for (let now = 32; now <= 400; now += 16) estimator.update(reading([0, 90, 0]), now); + const steadyRotation = estimator.tilt(400, 0, 8)?.x ?? 0; + + expect(impulse).toBeLessThan(-4); + expect(steadyRotation).toBeLessThan(0); + expect(Math.abs(impulse)).toBeGreaterThan(Math.abs(steadyRotation) * 3); + }); + + it('projects the motion into the current screen orientation', () => { + const estimator = createDeviceMotionEstimator(); + estimator.update(reading([0, 0, 0]), 0); + estimator.update(reading([0, 90, 0]), 16); + const portrait = estimator.tilt(16, 0, 8); + const landscape = estimator.tilt(16, 90, 8); + expect(portrait?.x).toBeLessThan(-4); + expect(landscape?.y).toBeGreaterThan(4); + }); + + it('clamps extreme values, filters noise, and expires stale readings', () => { + const quiet = createDeviceMotionEstimator(); + quiet.update(reading([0, 0, 0]), 0); + quiet.update(reading([0, 1, 0]), 16); + expect(quiet.tilt(16, 0, 8)?.x).toBeCloseTo(0); + expect(quiet.tilt(16, 0, 8)?.y).toBeCloseTo(0); + + const extreme = createDeviceMotionEstimator(); + extreme.update(reading([0, 0, 0]), 0); + extreme.update(reading([0, 100_000, 0]), 16); + const tilt = extreme.tilt(16, 0, 8); + expect(Math.hypot(tilt?.x ?? 0, tilt?.y ?? 0)).toBeLessThanOrEqual(8); + expect(extreme.tilt(517, 0, 8)).toBeNull(); + }); + + it('returns to rest after one complete rotation instead of retaining the final side', () => { + const estimator = createDeviceMotionEstimator(); + estimator.update(reading([0, 0, 0]), 0); + for (let now = 16; now <= 2000; now += 16) { + estimator.update(reading([0, 180, 0]), now); + } + const rotating = estimator.tilt(2000, 0, 8)?.x ?? 0; + expect(rotating).toBeLessThan(-1); + + estimator.update(reading([0, 0, 0]), 2016); + const braking = estimator.tilt(2016, 0, 8)?.x ?? 0; + expect(braking).toBeGreaterThan(1); + for (let now = 2032; now <= 2512; now += 16) { + estimator.update(reading([0, 0, 0]), now); + } + const resting = estimator.tilt(2512, 0, 8); + expect(Math.abs(resting?.x ?? Infinity)).toBeLessThan(0.01); + expect(Math.abs(resting?.y ?? Infinity)).toBeLessThan(0.01); + }); + + it('clears rate and derivative history on reset', () => { + const estimator = createDeviceMotionEstimator(); + estimator.update(reading([0, 0, 0]), 0); + estimator.update(reading([0, 90, 0]), 16); + const impulse = Math.abs(estimator.tilt(16, 0, 8)?.x ?? 0); + estimator.reset(); + expect(estimator.tilt(16, 0, 8)).toBeNull(); + + estimator.update(reading([0, 90, 0]), 32); + const afterReset = Math.abs(estimator.tilt(32, 0, 8)?.x ?? Infinity); + expect(afterReset).toBeLessThan(impulse * 0.25); + }); +}); diff --git a/src/lib/Puddle/device/estimator.ts b/src/lib/Puddle/device/estimator.ts new file mode 100644 index 0000000..35164c2 --- /dev/null +++ b/src/lib/Puddle/device/estimator.ts @@ -0,0 +1,86 @@ +import { + angularAcceleration, + blendVector, + projectDeviceMotion, + rotationRateVector, + sloshVector, +} from './math'; +import type { + DeviceGravityEstimator, + DeviceMotionEstimator, + DeviceMotionTilt, + DeviceMotionReading, + Vector2, + Vector3, +} from './types'; + +const RATE_FILTER_TAU = 0.04; +const DEFAULT_SENSOR_DT = 1 / 60; +const MIN_SENSOR_DT = 1 / 240; +const MAX_SENSOR_DT = 0.1; +const STALE_AFTER_MS = 500; +const STALE_FADE_TAU_MS = 180; + +export function createDeviceMotionEstimator(): DeviceMotionEstimator { + let filteredRate: Vector3 | null = null; + let motion: Vector2 | null = null; + let previousReadingAt: number | null = null; + let lastUsefulReadingAt: number | null = null; + + const update = (reading: DeviceMotionReading, nowMs: number): void => { + if (!Number.isFinite(nowMs)) return; + const measuredRate = rotationRateVector(reading); + if (!measuredRate) return; + const intervalSeconds = + Number.isFinite(reading.interval) && reading.interval > 0 + ? reading.interval / 1000 + : DEFAULT_SENSOR_DT; + const elapsedSeconds = + previousReadingAt === null + ? intervalSeconds + : Math.max(0, (nowMs - previousReadingAt) / 1000); + const dt = Math.max(MIN_SENSOR_DT, Math.min(MAX_SENSOR_DT, elapsedSeconds)); + previousReadingAt = nowMs; + + if (!filteredRate) { + filteredRate = measuredRate; + motion = sloshVector(measuredRate, { x: 0, y: 0, z: 0 }); + } else { + const previousRate = filteredRate; + const filterAmount = 1 - Math.exp(-dt / RATE_FILTER_TAU); + filteredRate = blendVector(previousRate, measuredRate, filterAmount); + motion = sloshVector(filteredRate, angularAcceleration(previousRate, filteredRate, dt)); + } + lastUsefulReadingAt = nowMs; + }; + + const tilt = (nowMs: number, screenAngle: number, strength: number): DeviceMotionTilt | null => { + if ( + !motion || + lastUsefulReadingAt === null || + !Number.isFinite(nowMs) || + nowMs - lastUsefulReadingAt > STALE_AFTER_MS + ) + return null; + const age = Math.max(0, nowMs - lastUsefulReadingAt); + const freshness = Math.exp(-age / STALE_FADE_TAU_MS); + return projectDeviceMotion( + { x: motion.x * freshness, y: motion.y * freshness }, + screenAngle, + strength, + ); + }; + + const reset = (): void => { + filteredRate = null; + motion = null; + previousReadingAt = null; + lastUsefulReadingAt = null; + }; + + return { update, tilt, reset }; +} + +/** Compatibility name for code written against the earlier gravity estimator. */ +export const createDeviceGravityEstimator: () => DeviceGravityEstimator = + createDeviceMotionEstimator; diff --git a/src/lib/Puddle/device/index.ts b/src/lib/Puddle/device/index.ts new file mode 100644 index 0000000..da57ed2 --- /dev/null +++ b/src/lib/Puddle/device/index.ts @@ -0,0 +1,12 @@ +export { createDeviceGravityEstimator, createDeviceMotionEstimator } from './estimator'; +export { projectDeviceGravity, projectDeviceMotion } from './math'; +export { requestDeviceGravityPermission } from './permission'; +export { getScreenAngle } from './screen'; +export type { + DeviceGravityEstimator, + DeviceGravityPermission, + DeviceGravityTilt, + DeviceMotionEstimator, + DeviceMotionReading, + DeviceMotionTilt, +} from './types'; diff --git a/src/lib/Puddle/device/math.ts b/src/lib/Puddle/device/math.ts new file mode 100644 index 0000000..f8e329d --- /dev/null +++ b/src/lib/Puddle/device/math.ts @@ -0,0 +1,97 @@ +import type { DeviceMotionReading, DeviceMotionTilt, Vector2, Vector3 } from './types'; + +const MAX_GYRO_RATE = 720; +const RATE_DEAD_ZONE = 1.5; +const RATE_REFERENCE = 180; +const ACCELERATION_DEAD_ZONE = 30; +const ACCELERATION_REFERENCE = 900; +const ACCELERATION_WEIGHT = 0.85; +const VELOCITY_WEIGHT = 0.2; + +const finite = (value: number | null | undefined): value is number => + value !== null && value !== undefined && Number.isFinite(value); + +const clamp = (value: number, minimum: number, maximum: number): number => + Math.max(minimum, Math.min(maximum, value)); + +const applyDeadZone = (value: number, deadZone: number): number => { + const magnitude = Math.abs(value); + return magnitude <= deadZone ? 0 : Math.sign(value) * (magnitude - deadZone); +}; + +const normalizeResponse = (value: number, deadZone: number, reference: number): number => + clamp(applyDeadZone(value, deadZone) / reference, -1, 1); + +export function rotationRateVector(reading: DeviceMotionReading): Vector3 | null { + const rate = reading.rotationRate; + if (!rate) return null; + const values = [rate.alpha, rate.beta, rate.gamma]; + if (!values.some(finite)) return null; + // The current W3C spec maps alpha/beta/gamma to device x/y/z. + const sanitized = values.map((value) => + finite(value) ? clamp(value, -MAX_GYRO_RATE, MAX_GYRO_RATE) : 0, + ); + return { x: sanitized[0] ?? 0, y: sanitized[1] ?? 0, z: sanitized[2] ?? 0 }; +} + +export function blendVector(from: Vector3, to: Vector3, amount: number): Vector3 { + const t = clamp(amount, 0, 1); + return { + x: from.x + (to.x - from.x) * t, + y: from.y + (to.y - from.y) * t, + z: from.z + (to.z - from.z) * t, + }; +} + +export function angularAcceleration(from: Vector3, to: Vector3, dt: number): Vector3 { + const safeDt = Number.isFinite(dt) && dt > 0 ? dt : 1; + return { + x: (to.x - from.x) / safeDt, + y: (to.y - from.y) / safeDt, + z: (to.z - from.z) / safeDt, + }; +} + +export function sloshVector(rate: Vector3, acceleration: Vector3): Vector2 { + const rateX = normalizeResponse(rate.x, RATE_DEAD_ZONE, RATE_REFERENCE); + const rateY = normalizeResponse(rate.y, RATE_DEAD_ZONE, RATE_REFERENCE); + const accelerationX = normalizeResponse( + acceleration.x, + ACCELERATION_DEAD_ZONE, + ACCELERATION_REFERENCE, + ); + const accelerationY = normalizeResponse( + acceleration.y, + ACCELERATION_DEAD_ZONE, + ACCELERATION_REFERENCE, + ); + const x = -accelerationY * ACCELERATION_WEIGHT - rateY * VELOCITY_WEIGHT; + const y = accelerationX * ACCELERATION_WEIGHT + rateX * VELOCITY_WEIGHT; + const magnitude = Math.hypot(x, y); + if (magnitude <= 1) return { x, y }; + return { x: x / magnitude, y: y / magnitude }; +} + +export function projectDeviceMotion( + vector: Vector2, + screenAngle: number, + strength: number, +): DeviceMotionTilt { + const magnitude = Number.isFinite(strength) ? Math.max(0, strength) : 0; + const angle = Number.isFinite(screenAngle) ? screenAngle * (Math.PI / 180) : 0; + const vectorX = Number.isFinite(vector.x) ? vector.x : 0; + const vectorY = Number.isFinite(vector.y) ? vector.y : 0; + const vectorMagnitude = Math.hypot(vectorX, vectorY); + const scale = vectorMagnitude > 1 ? magnitude / vectorMagnitude : magnitude; + const naturalX = vectorX * scale; + const naturalY = -vectorY * scale; + const cosine = Math.cos(angle); + const sine = Math.sin(angle); + return { + x: cosine * naturalX + sine * naturalY, + y: -sine * naturalX + cosine * naturalY, + }; +} + +/** Compatibility name for code written against the earlier gravity projection. */ +export const projectDeviceGravity = projectDeviceMotion; diff --git a/src/lib/Puddle/device/permission.ts b/src/lib/Puddle/device/permission.ts new file mode 100644 index 0000000..6ab97f0 --- /dev/null +++ b/src/lib/Puddle/device/permission.ts @@ -0,0 +1,23 @@ +import type { DeviceGravityPermission } from './types'; + +type MotionEventConstructor = typeof DeviceMotionEvent & { + requestPermission?: () => Promise; +}; + +/** Request motion access. Call from an intentional user gesture on browsers that prompt. */ +export async function requestDeviceGravityPermission(): Promise { + if ( + typeof window === 'undefined' || + !window.isSecureContext || + typeof window.DeviceMotionEvent !== 'function' + ) + return 'unsupported'; + + const motionEvent = window.DeviceMotionEvent as MotionEventConstructor; + if (!motionEvent.requestPermission) return 'granted'; + try { + return (await motionEvent.requestPermission()) === 'granted' ? 'granted' : 'denied'; + } catch { + return 'denied'; + } +} diff --git a/src/lib/Puddle/device/screen.test.ts b/src/lib/Puddle/device/screen.test.ts new file mode 100644 index 0000000..9d2fec7 --- /dev/null +++ b/src/lib/Puddle/device/screen.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from 'vitest'; + +import { getScreenAngle } from './screen'; + +test('reads the current screen orientation angle', () => { + expect(getScreenAngle({ orientation: { angle: 270 } })).toBe(270); +}); + +test('falls back when the screen orientation API is unavailable', () => { + expect(getScreenAngle({})).toBe(0); +}); diff --git a/src/lib/Puddle/device/screen.ts b/src/lib/Puddle/device/screen.ts new file mode 100644 index 0000000..ad9b0c8 --- /dev/null +++ b/src/lib/Puddle/device/screen.ts @@ -0,0 +1,9 @@ +export interface ScreenOrientationSource { + readonly orientation?: { + readonly angle?: number; + }; +} + +export function getScreenAngle(source: ScreenOrientationSource = screen): number { + return source.orientation?.angle ?? 0; +} diff --git a/src/lib/Puddle/device/types.ts b/src/lib/Puddle/device/types.ts new file mode 100644 index 0000000..8e9685d --- /dev/null +++ b/src/lib/Puddle/device/types.ts @@ -0,0 +1,38 @@ +export type DeviceGravityPermission = 'granted' | 'denied' | 'unsupported'; + +export interface DeviceMotionReading { + readonly rotationRate: { + readonly alpha: number | null; + readonly beta: number | null; + readonly gamma: number | null; + } | null; + readonly interval: number; +} + +export interface DeviceMotionTilt { + readonly x: number; + readonly y: number; +} + +export interface DeviceMotionEstimator { + readonly update: (reading: DeviceMotionReading, nowMs: number) => void; + readonly tilt: (nowMs: number, screenAngle: number, strength: number) => DeviceMotionTilt | null; + readonly reset: () => void; +} + +/** Compatibility name for code written against the earlier gravity estimator. */ +export type DeviceGravityTilt = DeviceMotionTilt; + +/** Compatibility name for code written against the earlier gravity estimator. */ +export type DeviceGravityEstimator = DeviceMotionEstimator; + +export interface Vector3 { + readonly x: number; + readonly y: number; + readonly z: number; +} + +export interface Vector2 { + readonly x: number; + readonly y: number; +} diff --git a/src/lib/Puddle/generated/puddleSnapshots.ts b/src/lib/Puddle/generated/puddleSnapshots.ts new file mode 100644 index 0000000..211e085 --- /dev/null +++ b/src/lib/Puddle/generated/puddleSnapshots.ts @@ -0,0 +1,55 @@ +// Generated by tools/generate-puddle-snapshots.ts. Do not edit. +import type { PuddleSnapshotAsset, PuddleSnapshotName } from '../snapshot/types'; + +export const PUDDLE_SNAPSHOTS = { + default: { + name: 'default', + url: '/puddle/default.bin', + nx: 66, + ny: 51, + cellSize: 28, + compatibilityHash: 1528846460, + path: 'M868 420h420v28h-420zM560 448h784v28h-784zM532 476h840v28h-840zM504 504h868v28h-868zM476 532h924v28h-924zM476 560h924v28h-924zM476 588h924v28h-924zM476 616h896v28h-896zM504 644h868v28h-868zM504 672h868v28h-868zM504 700h868v28h-868zM504 728h868v28h-868zM504 756h868v28h-868zM504 784h840v28h-840zM504 812h812v28h-812zM532 840h784v28h-784zM560 868h728v28h-728zM672 896h336v28h-336z', + clip: 'shape(from calc(50% - 56px) calc(50% - 294px),hline by 420px,vline by 28px,hline by -420px,close,move to calc(50% - 364px) calc(50% - 266px),hline by 784px,vline by 28px,hline by -784px,close,move to calc(50% - 392px) calc(50% - 238px),hline by 840px,vline by 28px,hline by -840px,close,move to calc(50% - 420px) calc(50% - 210px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 448px) calc(50% - 182px),hline by 924px,vline by 28px,hline by -924px,close,move to calc(50% - 448px) calc(50% - 154px),hline by 924px,vline by 28px,hline by -924px,close,move to calc(50% - 448px) calc(50% - 126px),hline by 924px,vline by 28px,hline by -924px,close,move to calc(50% - 448px) calc(50% - 98px),hline by 896px,vline by 28px,hline by -896px,close,move to calc(50% - 420px) calc(50% - 70px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% - 42px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% - 14px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% + 14px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% + 42px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% + 70px),hline by 840px,vline by 28px,hline by -840px,close,move to calc(50% - 420px) calc(50% + 98px),hline by 812px,vline by 28px,hline by -812px,close,move to calc(50% - 392px) calc(50% + 126px),hline by 784px,vline by 28px,hline by -784px,close,move to calc(50% - 364px) calc(50% + 154px),hline by 728px,vline by 28px,hline by -728px,close,move to calc(50% - 252px) calc(50% + 182px),hline by 336px,vline by 28px,hline by -336px,close)', + }, + compact: { + name: 'compact', + url: '/puddle/compact.bin', + nx: 77, + ny: 57, + cellSize: 18, + compatibilityHash: 2276277711, + path: 'M666 288h306v18h-306zM414 306h594v18h-594zM396 324h630v18h-630zM378 342h666v18h-666zM360 360h702v18h-702zM342 378h720v18h-720zM342 396h720v18h-720zM342 414h720v18h-720zM342 432h720v18h-720zM360 450h702v18h-702zM360 468h684v18h-684zM360 486h684v18h-684zM360 504h684v18h-684zM360 522h684v18h-684zM360 540h684v18h-684zM360 558h666v18h-666zM360 576h666v18h-666zM378 594h630v18h-630zM396 612h594v18h-594zM432 630h522v18h-522zM486 648h270v18h-270zM576 666h36v18h-36z', + clip: 'shape(from calc(50% - 27px) calc(50% - 225px),hline by 306px,vline by 18px,hline by -306px,close,move to calc(50% - 279px) calc(50% - 207px),hline by 594px,vline by 18px,hline by -594px,close,move to calc(50% - 297px) calc(50% - 189px),hline by 630px,vline by 18px,hline by -630px,close,move to calc(50% - 315px) calc(50% - 171px),hline by 666px,vline by 18px,hline by -666px,close,move to calc(50% - 333px) calc(50% - 153px),hline by 702px,vline by 18px,hline by -702px,close,move to calc(50% - 351px) calc(50% - 135px),hline by 720px,vline by 18px,hline by -720px,close,move to calc(50% - 351px) calc(50% - 117px),hline by 720px,vline by 18px,hline by -720px,close,move to calc(50% - 351px) calc(50% - 99px),hline by 720px,vline by 18px,hline by -720px,close,move to calc(50% - 351px) calc(50% - 81px),hline by 720px,vline by 18px,hline by -720px,close,move to calc(50% - 333px) calc(50% - 63px),hline by 702px,vline by 18px,hline by -702px,close,move to calc(50% - 333px) calc(50% - 45px),hline by 684px,vline by 18px,hline by -684px,close,move to calc(50% - 333px) calc(50% - 27px),hline by 684px,vline by 18px,hline by -684px,close,move to calc(50% - 333px) calc(50% - 9px),hline by 684px,vline by 18px,hline by -684px,close,move to calc(50% - 333px) calc(50% + 9px),hline by 684px,vline by 18px,hline by -684px,close,move to calc(50% - 333px) calc(50% + 27px),hline by 684px,vline by 18px,hline by -684px,close,move to calc(50% - 333px) calc(50% + 45px),hline by 666px,vline by 18px,hline by -666px,close,move to calc(50% - 333px) calc(50% + 63px),hline by 666px,vline by 18px,hline by -666px,close,move to calc(50% - 315px) calc(50% + 81px),hline by 630px,vline by 18px,hline by -630px,close,move to calc(50% - 297px) calc(50% + 99px),hline by 594px,vline by 18px,hline by -594px,close,move to calc(50% - 261px) calc(50% + 117px),hline by 522px,vline by 18px,hline by -522px,close,move to calc(50% - 207px) calc(50% + 135px),hline by 270px,vline by 18px,hline by -270px,close,move to calc(50% - 117px) calc(50% + 153px),hline by 36px,vline by 18px,hline by -36px,close)', + }, + medium: { + name: 'medium', + url: '/puddle/medium.bin', + nx: 61, + ny: 47, + cellSize: 26, + compatibilityHash: 2744450851, + path: 'M806 364h26v26h-26zM910 364h182v26h-182zM494 390h624v26h-624zM442 416h728v26h-728zM442 442h754v26h-754zM416 468h780v26h-780zM416 494h780v26h-780zM416 520h780v26h-780zM416 546h754v26h-754zM442 572h728v26h-728zM442 598h728v26h-728zM442 624h728v26h-728zM442 650h728v26h-728zM442 676h702v26h-702zM468 702h650v26h-650zM572 728h494v26h-494zM598 754h182v26h-182z', + clip: 'shape(from calc(50% + 13px) calc(50% - 247px),hline by 26px,vline by 26px,hline by -26px,close,move to calc(50% + 117px) calc(50% - 247px),hline by 182px,vline by 26px,hline by -182px,close,move to calc(50% - 299px) calc(50% - 221px),hline by 624px,vline by 26px,hline by -624px,close,move to calc(50% - 351px) calc(50% - 195px),hline by 728px,vline by 26px,hline by -728px,close,move to calc(50% - 351px) calc(50% - 169px),hline by 754px,vline by 26px,hline by -754px,close,move to calc(50% - 377px) calc(50% - 143px),hline by 780px,vline by 26px,hline by -780px,close,move to calc(50% - 377px) calc(50% - 117px),hline by 780px,vline by 26px,hline by -780px,close,move to calc(50% - 377px) calc(50% - 91px),hline by 780px,vline by 26px,hline by -780px,close,move to calc(50% - 377px) calc(50% - 65px),hline by 754px,vline by 26px,hline by -754px,close,move to calc(50% - 351px) calc(50% - 39px),hline by 728px,vline by 26px,hline by -728px,close,move to calc(50% - 351px) calc(50% - 13px),hline by 728px,vline by 26px,hline by -728px,close,move to calc(50% - 351px) calc(50% + 13px),hline by 728px,vline by 26px,hline by -728px,close,move to calc(50% - 351px) calc(50% + 39px),hline by 728px,vline by 26px,hline by -728px,close,move to calc(50% - 351px) calc(50% + 65px),hline by 702px,vline by 26px,hline by -702px,close,move to calc(50% - 325px) calc(50% + 91px),hline by 650px,vline by 26px,hline by -650px,close,move to calc(50% - 221px) calc(50% + 117px),hline by 494px,vline by 26px,hline by -494px,close,move to calc(50% - 195px) calc(50% + 143px),hline by 182px,vline by 26px,hline by -182px,close)', + }, + expanded: { + name: 'expanded', + url: '/puddle/expanded.bin', + nx: 62, + ny: 47, + cellSize: 28, + compatibilityHash: 3996849826, + path: 'M812 392h392v28h-392zM532 420h728v28h-728zM476 448h812v28h-812zM476 476h812v28h-812zM448 504h840v28h-840zM448 532h840v28h-840zM448 560h840v28h-840zM476 588h812v28h-812zM476 616h812v28h-812zM476 644h812v28h-812zM476 672h812v28h-812zM476 700h784v28h-784zM476 728h784v28h-784zM504 756h728v28h-728zM532 784h672v28h-672zM616 812h364v28h-364zM756 840h28v28h-28zM812 840h28v28h-28z', + clip: 'shape(from calc(50% - 56px) calc(50% - 266px),hline by 392px,vline by 28px,hline by -392px,close,move to calc(50% - 336px) calc(50% - 238px),hline by 728px,vline by 28px,hline by -728px,close,move to calc(50% - 392px) calc(50% - 210px),hline by 812px,vline by 28px,hline by -812px,close,move to calc(50% - 392px) calc(50% - 182px),hline by 812px,vline by 28px,hline by -812px,close,move to calc(50% - 420px) calc(50% - 154px),hline by 840px,vline by 28px,hline by -840px,close,move to calc(50% - 420px) calc(50% - 126px),hline by 840px,vline by 28px,hline by -840px,close,move to calc(50% - 420px) calc(50% - 98px),hline by 840px,vline by 28px,hline by -840px,close,move to calc(50% - 392px) calc(50% - 70px),hline by 812px,vline by 28px,hline by -812px,close,move to calc(50% - 392px) calc(50% - 42px),hline by 812px,vline by 28px,hline by -812px,close,move to calc(50% - 392px) calc(50% - 14px),hline by 812px,vline by 28px,hline by -812px,close,move to calc(50% - 392px) calc(50% + 14px),hline by 812px,vline by 28px,hline by -812px,close,move to calc(50% - 392px) calc(50% + 42px),hline by 784px,vline by 28px,hline by -784px,close,move to calc(50% - 392px) calc(50% + 70px),hline by 784px,vline by 28px,hline by -784px,close,move to calc(50% - 364px) calc(50% + 98px),hline by 728px,vline by 28px,hline by -728px,close,move to calc(50% - 336px) calc(50% + 126px),hline by 672px,vline by 28px,hline by -672px,close,move to calc(50% - 252px) calc(50% + 154px),hline by 364px,vline by 28px,hline by -364px,close,move to calc(50% - 112px) calc(50% + 182px),hline by 28px,vline by 28px,hline by -28px,close,move to calc(50% - 56px) calc(50% + 182px),hline by 28px,vline by 28px,hline by -28px,close)', + }, + large: { + name: 'large', + url: '/puddle/large.bin', + nx: 66, + ny: 51, + cellSize: 28, + compatibilityHash: 1528846460, + path: 'M868 420h420v28h-420zM560 448h784v28h-784zM532 476h840v28h-840zM504 504h868v28h-868zM476 532h924v28h-924zM476 560h924v28h-924zM476 588h924v28h-924zM476 616h896v28h-896zM504 644h868v28h-868zM504 672h868v28h-868zM504 700h868v28h-868zM504 728h868v28h-868zM504 756h868v28h-868zM504 784h840v28h-840zM504 812h812v28h-812zM532 840h784v28h-784zM560 868h728v28h-728zM672 896h336v28h-336z', + clip: 'shape(from calc(50% - 56px) calc(50% - 294px),hline by 420px,vline by 28px,hline by -420px,close,move to calc(50% - 364px) calc(50% - 266px),hline by 784px,vline by 28px,hline by -784px,close,move to calc(50% - 392px) calc(50% - 238px),hline by 840px,vline by 28px,hline by -840px,close,move to calc(50% - 420px) calc(50% - 210px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 448px) calc(50% - 182px),hline by 924px,vline by 28px,hline by -924px,close,move to calc(50% - 448px) calc(50% - 154px),hline by 924px,vline by 28px,hline by -924px,close,move to calc(50% - 448px) calc(50% - 126px),hline by 924px,vline by 28px,hline by -924px,close,move to calc(50% - 448px) calc(50% - 98px),hline by 896px,vline by 28px,hline by -896px,close,move to calc(50% - 420px) calc(50% - 70px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% - 42px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% - 14px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% + 14px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% + 42px),hline by 868px,vline by 28px,hline by -868px,close,move to calc(50% - 420px) calc(50% + 70px),hline by 840px,vline by 28px,hline by -840px,close,move to calc(50% - 420px) calc(50% + 98px),hline by 812px,vline by 28px,hline by -812px,close,move to calc(50% - 392px) calc(50% + 126px),hline by 784px,vline by 28px,hline by -784px,close,move to calc(50% - 364px) calc(50% + 154px),hline by 728px,vline by 28px,hline by -728px,close,move to calc(50% - 252px) calc(50% + 182px),hline by 336px,vline by 28px,hline by -336px,close)', + }, +} as const satisfies Record; diff --git a/src/lib/Puddle/geometry.test.ts b/src/lib/Puddle/geometry.test.ts new file mode 100644 index 0000000..cb46d52 --- /dev/null +++ b/src/lib/Puddle/geometry.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from 'vitest'; + +import { resolvePuddleGeometry } from './geometry'; + +function expectCoverage( + width: number, + height: number, + options: { cellSize: number; maxCells: number }, +): void { + const geometry = resolvePuddleGeometry(width, height, options); + expect((geometry.cols - 2) * geometry.cellSize).toBeGreaterThanOrEqual(width); + expect((geometry.rows - 2) * geometry.cellSize).toBeGreaterThanOrEqual(height); + expect(geometry.cols * geometry.cellSize - width).toBeGreaterThanOrEqual(2 * geometry.cellSize); + expect(geometry.rows * geometry.cellSize - height).toBeGreaterThanOrEqual(2 * geometry.cellSize); + expect(geometry.cols).toBeLessThanOrEqual(options.maxCells); + expect(geometry.rows).toBeLessThanOrEqual(options.maxCells); +} + +describe('resolvePuddleGeometry', () => { + test('covers fractional dimensions with a full dry guard cell on each side', () => { + expectCoverage(1829.14, 999.3, { cellSize: 28, maxCells: 200 }); + }); + + test('adds two guard cells when the visible span is an exact cell multiple', () => { + const geometry = resolvePuddleGeometry(760, 418, { cellSize: 19, maxCells: 200 }); + + expect(geometry.cols).toBe(42); + expect(geometry.rows).toBe(24); + expect(geometry.cellSize).toBe(19); + expectCoverage(760, 418, { cellSize: 19, maxCells: 200 }); + }); + + test('increases effective cell size under maxCells pressure on either axis', () => { + const wide = resolvePuddleGeometry(10_000, 200, { cellSize: 10, maxCells: 52 }); + const tall = resolvePuddleGeometry(200, 10_000, { cellSize: 10, maxCells: 52 }); + + expect(wide.cellSize).toBe(200); + expect(wide.cols).toBe(52); + expect(tall.cellSize).toBe(200); + expect(tall.rows).toBe(52); + expectCoverage(10_000, 200, { cellSize: 10, maxCells: 52 }); + expectCoverage(200, 10_000, { cellSize: 10, maxCells: 52 }); + }); + + test('never exceeds maxCells at floating-point extremes', () => { + const options = { cellSize: 1, maxCells: 52 }; + + expectCoverage(Number.MAX_VALUE, Number.MAX_VALUE, options); + const geometry = resolvePuddleGeometry(Number.MAX_VALUE, Number.MAX_VALUE, options); + expect(geometry.cols).toBe(options.maxCells); + expect(geometry.rows).toBe(options.maxCells); + }); + + test('uses safe zero geometry before the host is measured', () => { + const geometry = resolvePuddleGeometry(0, 0, { cellSize: 19, maxCells: 200 }); + + expect(geometry).toEqual({ + width: 0, + height: 0, + cols: 0, + rows: 0, + cellSize: 19, + ready: false, + }); + }); +}); diff --git a/src/lib/Puddle/geometry.ts b/src/lib/Puddle/geometry.ts new file mode 100644 index 0000000..7d9a31e --- /dev/null +++ b/src/lib/Puddle/geometry.ts @@ -0,0 +1,49 @@ +import { PUDDLE_DEFAULTS } from './config'; + +export interface PuddleGeometryOptions { + readonly cellSize: number; + readonly maxCells: number; +} + +export interface PuddleGeometry { + readonly width: number; + readonly height: number; + readonly cols: number; + readonly rows: number; + readonly cellSize: number; + readonly ready: boolean; +} + +export function resolvePuddleGeometry( + width: number, + height: number, + options: PuddleGeometryOptions, +): PuddleGeometry { + const measuredWidth = Number.isFinite(width) && width > 0 ? width : 0; + const measuredHeight = Number.isFinite(height) && height > 0 ? height : 0; + const cellCap = + Number.isFinite(options.maxCells) && options.maxCells >= 3 + ? Math.floor(options.maxCells) + : PUDDLE_DEFAULTS.maxCells; + const configuredCell = + Number.isFinite(options.cellSize) && options.cellSize >= 1 + ? options.cellSize + : PUDDLE_DEFAULTS.cellSize; + const visibleCellCap = cellCap - 2; + const cell = Math.max( + configuredCell, + measuredWidth / visibleCellCap, + measuredHeight / visibleCellCap, + ); + const cols = measuredWidth > 0 ? Math.min(cellCap, Math.ceil(measuredWidth / cell) + 2) : 0; + const rows = measuredHeight > 0 ? Math.min(cellCap, Math.ceil(measuredHeight / cell) + 2) : 0; + + return { + width: measuredWidth, + height: measuredHeight, + cols, + rows, + cellSize: cell, + ready: cols > 0 && rows > 0, + }; +} diff --git a/src/lib/Puddle/render/puddleRenderer.svelte.test.ts b/src/lib/Puddle/render/puddleRenderer.svelte.test.ts new file mode 100644 index 0000000..b91e4e6 --- /dev/null +++ b/src/lib/Puddle/render/puddleRenderer.svelte.test.ts @@ -0,0 +1,58 @@ +import { expect, test, vi } from 'vitest'; + +import { createPuddleRenderer, puddleClipShape, puddlePath } from './puddleRenderer'; + +test('compacts adjacent wet cells into horizontal rectangular runs', () => { + expect(puddlePath([1, 1, 0, 1, 0, 0, 1, 1, 1, 0], 5, 2)).toBe( + 'M0 0h2v1h-2zM3 0h1v1h-1zM1 1h3v1h-3z', + ); +}); + +test('scales every run into stable CSS-pixel cells', () => { + expect(puddlePath([0, 1, 1, 0], 4, 1, 18)).toBe('M18 0h36v18h-36z'); +}); + +test('centers CSS-pixel runs relative to the clip reference box', () => { + const clip = puddleClipShape([0, 1, 1, 0], 4, 1, 18); + + expect(clip).toBe( + 'shape(from calc(50% - 18px) calc(50% - 9px),hline by 36px,vline by 18px,hline by -36px,close)', + ); + expect(CSS.supports('clip-path', clip)).toBe(true); + expect(puddleClipShape([0, 0], 2, 1, 18)).toBe('inset(50%)'); +}); + +test('only updates the path when the binary mask or target changes', () => { + const renderer = createPuddleRenderer(); + const target = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const setAttribute = vi.spyOn(target, 'setAttribute'); + + expect(renderer.render(target, { nx: 3, ny: 1, height: [0.2, 0, 0.8], threshold: 0.1 })).toBe( + true, + ); + expect(target.getAttribute('d')).toBe('M0 0h1v1h-1zM2 0h1v1h-1z'); + expect(setAttribute).toHaveBeenCalledTimes(1); + + // Height changes that do not cross the threshold keep the same path. + renderer.render(target, { nx: 3, ny: 1, height: [0.4, 0.05, 1], threshold: 0.1 }); + expect(setAttribute).toHaveBeenCalledTimes(1); + + renderer.render(target, { nx: 3, ny: 1, height: [0.4, 0.2, 1], threshold: 0.1 }); + expect(target.getAttribute('d')).toBe('M0 0h3v1h-3z'); + expect(setAttribute).toHaveBeenCalledTimes(2); + + const replacement = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + renderer.render(replacement, { nx: 3, ny: 1, height: [0.4, 0.2, 1], threshold: 0.1 }); + expect(replacement.getAttribute('d')).toBe('M0 0h3v1h-3z'); +}); + +test('rebuilds the path when grid dimensions change', () => { + const renderer = createPuddleRenderer(); + const target = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + + renderer.render(target, { nx: 2, ny: 1, height: [1, 1], threshold: 0 }); + expect(target.getAttribute('d')).toBe('M0 0h2v1h-2z'); + + renderer.render(target, { nx: 1, ny: 2, height: [1, 1], threshold: 0 }); + expect(target.getAttribute('d')).toBe('M0 0h1v1h-1zM0 1h1v1h-1z'); +}); diff --git a/src/lib/Puddle/render/puddleRenderer.ts b/src/lib/Puddle/render/puddleRenderer.ts new file mode 100644 index 0000000..e972738 --- /dev/null +++ b/src/lib/Puddle/render/puddleRenderer.ts @@ -0,0 +1,112 @@ +export interface PuddleRenderParams { + nx: number; + ny: number; + height: ArrayLike; + /** Depth above which a cell is painted. */ + threshold: number; +} + +export interface PuddleRenderer { + /** Update the thresholded puddle path. Returns false for invalid grid dimensions. */ + render: (target: SVGPathElement, params: PuddleRenderParams) => boolean; +} + +function centeredCoordinate(position: number, extent: number): string { + const offset = position - extent / 2; + if (offset === 0) return '50%'; + const operator = offset > 0 ? '+' : '-'; + return `calc(50% ${operator} ${Math.abs(offset).toString()}px)`; +} + +/** Build one rectangular subpath per horizontal run of wet cells. */ +export function puddlePath(mask: ArrayLike, nx: number, ny: number, cellSize = 1): string { + let path = ''; + for (let y = 0; y < ny; y++) { + const row = y * nx; + for (let x = 0; x < nx;) { + if (mask[row + x] !== 1) { + x++; + continue; + } + + const start = x; + while (x < nx && mask[row + x] === 1) x++; + const width = x - start; + path += `M${(start * cellSize).toString()} ${(y * cellSize).toString()}h${( + width * cellSize + ).toString()}v${cellSize.toString()}h-${(width * cellSize).toString()}z`; + } + } + return path; +} + +/** + * Build the same wet-cell runs as a reference-box-relative CSS shape. + * CSS shape coordinates can mix percentages and pixels, unlike SVG URL clips on HTML elements. + */ +export function puddleClipShape( + mask: ArrayLike, + nx: number, + ny: number, + cellSize = 1, +): string { + const commands: string[] = []; + const width = nx * cellSize; + const height = ny * cellSize; + for (let y = 0; y < ny; y++) { + const row = y * nx; + for (let x = 0; x < nx;) { + if (mask[row + x] !== 1) { + x++; + continue; + } + + const start = x; + while (x < nx && mask[row + x] === 1) x++; + const runWidth = (x - start) * cellSize; + const position = `${centeredCoordinate(start * cellSize, width)} ${centeredCoordinate(y * cellSize, height)}`; + commands.push( + `${commands.length === 0 ? 'from' : 'move to'} ${position}`, + `hline by ${runWidth.toString()}px`, + `vline by ${cellSize.toString()}px`, + `hline by -${runWidth.toString()}px`, + 'close', + ); + } + } + return commands.length === 0 ? 'inset(50%)' : `shape(${commands.join(',')})`; +} + +export function createPuddleRenderer(): PuddleRenderer { + let mask = new Uint8Array(0); + let maskNx = 0; + let maskNy = 0; + let targetPath: SVGPathElement | null = null; + + const render = (target: SVGPathElement, params: PuddleRenderParams): boolean => { + const { nx, ny, height, threshold } = params; + if (!Number.isInteger(nx) || !Number.isInteger(ny) || nx <= 0 || ny <= 0) return false; + + const dimensionsChanged = nx !== maskNx || ny !== maskNy; + if (dimensionsChanged) { + mask = new Uint8Array(nx * ny); + maskNx = nx; + maskNy = ny; + } + + let maskChanged = dimensionsChanged; + for (let k = 0; k < mask.length; k++) { + const wet = (height[k] ?? 0) > threshold ? 1 : 0; + if (mask[k] !== wet) { + mask[k] = wet; + maskChanged = true; + } + } + + if (maskChanged || targetPath !== target) target.setAttribute('d', puddlePath(mask, nx, ny)); + targetPath = target; + return true; + }; + + return { render }; +} diff --git a/src/lib/Puddle/runtime/puddleLoop.ts b/src/lib/Puddle/runtime/puddleLoop.ts new file mode 100644 index 0000000..5094156 --- /dev/null +++ b/src/lib/Puddle/runtime/puddleLoop.ts @@ -0,0 +1,119 @@ +import type { DeviceMotionEstimator } from '../device'; +import type { WaterSim } from '../sim'; + +export interface PointerPosition { + readonly x: number; + readonly y: number; +} + +export interface PuddleLoopOptions { + readonly host: HTMLElement; + readonly sim: Pick; + readonly deviceMotionEstimator: Pick; + readonly draw: () => void; + readonly getPointer: () => PointerPosition | null; + readonly followCursor: boolean; + readonly cursorTilt: number; + readonly cursorEase: number; + readonly deviceGravity: boolean; + readonly deviceTilt: number; + readonly deviceEase: number; + readonly screenAngle: () => number; +} + +/** + * Runs the animated Puddle lifecycle independently from Svelte. Every option is + * captured by the caller before this starts, so RAF callbacks never depend on + * untracked component state. + */ +export function startPuddleLoop(options: PuddleLoopOptions): () => void { + const { + host, + sim, + deviceMotionEstimator, + draw, + getPointer, + followCursor, + cursorTilt, + cursorEase, + deviceGravity, + deviceTilt, + deviceEase, + screenAngle, + } = options; + const deviceStrength = Number.isFinite(deviceTilt) ? Math.max(0, deviceTilt) : 6; + const deviceTau = Number.isFinite(deviceEase) && deviceEase > 0 ? deviceEase : 0.18; + let tiltX = 0; + let tiltY = 0; + let deviceX = 0; + let deviceY = 0; + const applyInteractiveTilt = (dt: number, now: number): void => { + let gx = 0; + let gy = 0; + const pointer = getPointer(); + if (followCursor && pointer) { + const rect = host.getBoundingClientRect(); + const dx = pointer.x - (rect.left + rect.width / 2); + const dy = pointer.y - (rect.top + rect.height / 2); + const length = Math.hypot(dx, dy); + // Full strength half a box away, proportionally less near the center. + const half = Math.max(rect.width, rect.height) / 2; + const magnitude = Math.min(1, length / Math.max(half, 1)); + if (length > 0) { + gx = (dx / length) * magnitude; + gy = (dy / length) * magnitude; + } + } + const cursorK = 1 - Math.exp(-dt / cursorEase); + tiltX += (gx - tiltX) * cursorK; + tiltY += (gy - tiltY) * cursorK; + + const deviceTarget = deviceGravity + ? deviceMotionEstimator.tilt(now, screenAngle(), deviceStrength) + : null; + const deviceK = 1 - Math.exp(-dt / deviceTau); + deviceX += ((deviceTarget?.x ?? 0) - deviceX) * deviceK; + deviceY += ((deviceTarget?.y ?? 0) - deviceY) * deviceK; + // Negative tilt drives flow toward +axis, and grid +y is screen-down. + sim.setTiltOffset(-cursorTilt * tiltX + deviceX, -cursorTilt * tiltY + deviceY); + }; + + const massCap = sim.totalMass(); + let raf = 0; + let running = false; + let last = 0; + const loop = (time: number): void => { + const dt = last > 0 ? (time - last) / 1000 : 0; + last = time; + if (followCursor || deviceGravity) applyInteractiveTilt(dt, performance.now()); + const frameStats = sim.advance(dt); + sim.clampMass(massCap, frameStats); + draw(); + raf = requestAnimationFrame(loop); + }; + const start = (): void => { + if (running) return; + running = true; + last = 0; + raf = requestAnimationFrame(loop); + }; + const stop = (): void => { + running = false; + cancelAnimationFrame(raf); + deviceX = 0; + deviceY = 0; + deviceMotionEstimator.reset(); + sim.setTiltOffset(0, 0); + }; + + const observer = new IntersectionObserver(([entry]) => { + if (entry?.isIntersecting) start(); + else stop(); + }); + observer.observe(host); + return () => { + stop(); + observer.disconnect(); + sim.setTiltOffset(0, 0); + }; +} diff --git a/src/lib/Puddle/runtime/puddleRuntime.svelte.ts b/src/lib/Puddle/runtime/puddleRuntime.svelte.ts new file mode 100644 index 0000000..aaba041 --- /dev/null +++ b/src/lib/Puddle/runtime/puddleRuntime.svelte.ts @@ -0,0 +1,261 @@ +import { MediaQuery } from 'svelte/reactivity'; + +import { ElementSize } from 'runed'; + +import { createDeviceMotionEstimator, getScreenAngle } from '../device'; +import { resolvePuddleGeometry, type PuddleGeometryOptions } from '../geometry'; +import { snapshotCompatibilityHash } from '../snapshot/compatibility'; +import type { PuddleSnapshotAsset } from '../snapshot/types'; +import type { PuddleWorkerInput, PuddleWorkerOutput } from '../worker/protocol'; +import type { PuddleSimulationOptions } from './puddleSimulation'; + +export interface PuddleRuntimeOptions { + readonly getInitialSnapshot: () => PuddleSnapshotAsset; + readonly getSnapshotUrl: () => string; + readonly getGeometryOptions: () => PuddleGeometryOptions; + readonly getSimulationOptions: () => PuddleSimulationOptions; + readonly getSettleSubsteps: () => number; + readonly getThreshold: () => number; + readonly getAnimated: () => boolean; + readonly getFollowCursor: () => boolean; + readonly getCursorTilt: () => number; + readonly getCursorEase: () => number; + readonly getDeviceGravity: () => boolean; + readonly getDeviceTilt: () => number; + readonly getDeviceEase: () => number; +} + +export interface PuddleRuntime { + host: HTMLElement | null; + readonly painted: boolean; + readonly live: boolean; + readonly path: string; + readonly centerTransform: string; + readonly clipTransform: string; + readonly cols: number; + readonly rows: number; + readonly cellSize: number; + readonly followCursor: boolean; + readonly deviceGravity: boolean; + readonly deviceMotionEnabled: boolean; + readonly onPointerMove: (event: PointerEvent) => void; + readonly clearPointer: () => void; + readonly onDeviceMotion: (event: DeviceMotionEvent) => void; + readonly onVisibilityChange: () => void; +} + +export function createPuddleRuntime(options: PuddleRuntimeOptions): PuddleRuntime { + let host = $state(null); + const initialSnapshot = options.getInitialSnapshot(); + let activeSnapshot = initialSnapshot; + let renderedCols = $state(initialSnapshot.nx); + let renderedRows = $state(initialSnapshot.ny); + let renderedCellSize = $state(initialSnapshot.cellSize); + let renderedPath = $state(initialSnapshot.path); + let live = $state(false); + const hostSize = new ElementSize(() => host); + const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)'); + const deviceMotionEstimator = createDeviceMotionEstimator(); + const geometry = $derived( + resolvePuddleGeometry(hostSize.width, hostSize.height, options.getGeometryOptions()), + ); + let worker: Worker | null = null; + let visible = true; + let pendingPath: string | null = null; + let pendingDimensions: { cols: number; rows: number; cellSize: number } | null = null; + let applyFrame = 0; + let generation = 0; + + const post = (message: PuddleWorkerInput): void => worker?.postMessage(message); + const updateActive = (): void => { + post({ type: 'active', active: visible && !document.hidden }); + }; + const applyPendingPath = (): void => { + applyFrame = 0; + const path = pendingPath; + if (path === null) return; + renderedPath = path; + pendingPath = null; + if (pendingDimensions) { + renderedCols = pendingDimensions.cols; + renderedRows = pendingDimensions.rows; + renderedCellSize = pendingDimensions.cellSize; + pendingDimensions = null; + live = true; + } + post({ type: 'frame-consumed' }); + }; + const queuePath = ( + path: string, + dimensions?: { cols: number; rows: number; cellSize: number }, + ): void => { + pendingPath = path; + if (dimensions) pendingDimensions = dimensions; + if (applyFrame === 0) applyFrame = requestAnimationFrame(applyPendingPath); + }; + + const onPointerMove = (event: PointerEvent): void => { + if (!host) return; + const rect = host.getBoundingClientRect(); + const halfWidth = Math.max(rect.width / 2, 1); + const halfHeight = Math.max(rect.height / 2, 1); + post({ + type: 'pointer', + x: Math.max(-1, Math.min(1, (event.clientX - rect.left - halfWidth) / halfWidth)), + y: Math.max(-1, Math.min(1, (event.clientY - rect.top - halfHeight) / halfHeight)), + }); + }; + const clearPointer = (): void => { + post({ type: 'clear-pointer' }); + }; + const onDeviceMotion = (event: DeviceMotionEvent): void => { + deviceMotionEstimator.update(event, performance.now()); + const tilt = deviceMotionEstimator.tilt( + performance.now(), + getScreenAngle(), + options.getDeviceTilt(), + ); + post({ type: 'device-tilt', x: tilt?.x ?? 0, y: tilt?.y ?? 0 }); + }; + const onVisibilityChange = (): void => { + if (document.hidden) { + deviceMotionEstimator.reset(); + post({ type: 'device-tilt', x: 0, y: 0 }); + } + updateActive(); + }; + + $effect(() => { + if (!host || !geometry.ready) return; + const target = host; + const simulation = options.getSimulationOptions(); + const snapshot = options.getInitialSnapshot(); + const snapshotUrl = options.getSnapshotUrl(); + const settleSubsteps = options.getSettleSubsteps(); + const geometryOptions = options.getGeometryOptions(); + const depthThreshold = options.getThreshold(); + const animated = options.getAnimated() && !reducedMotion.current; + const cursorTilt = options.getCursorTilt(); + const cursorEase = options.getCursorEase(); + const deviceEase = options.getDeviceEase(); + const currentGeneration = ++generation; + if (snapshot !== activeSnapshot) { + activeSnapshot = snapshot; + renderedPath = snapshot.path; + renderedCols = snapshot.nx; + renderedRows = snapshot.ny; + renderedCellSize = snapshot.cellSize; + live = false; + } + // Vite statically extracts a worker only from this native URL expression. + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const nextWorker = new Worker(new URL('../worker/puddle.worker.ts', import.meta.url), { + type: 'module', + }); + worker = nextWorker; + const observer = new IntersectionObserver(([entry]) => { + visible = entry?.isIntersecting ?? false; + updateActive(); + }); + observer.observe(target); + nextWorker.onmessage = (event: MessageEvent): void => { + const message = event.data; + if ( + message.generation !== currentGeneration || + currentGeneration !== generation || + worker !== nextWorker + ) { + return; + } + if (message.type === 'ready') { + queuePath(message.path, { + cols: message.nx, + rows: message.ny, + cellSize: message.cellSize, + }); + } else if (message.type === 'path') { + queuePath(message.path); + } + }; + nextWorker.postMessage({ + type: 'init', + generation: currentGeneration, + width: geometry.width, + height: geometry.height, + geometry: geometryOptions, + simulation, + settleSubsteps, + threshold: depthThreshold, + animated, + snapshotUrl, + compatibilityHash: snapshotCompatibilityHash({ + cellSize: geometryOptions.cellSize, + settleSubsteps, + simulation, + }), + cursorTilt, + cursorEase, + deviceEase, + } satisfies PuddleWorkerInput); + updateActive(); + return () => { + observer.disconnect(); + nextWorker.terminate(); + if (worker === nextWorker) worker = null; + if (applyFrame !== 0) cancelAnimationFrame(applyFrame); + applyFrame = 0; + pendingPath = null; + pendingDimensions = null; + }; + }); + + return { + get host() { + return host; + }, + set host(value) { + host = value; + }, + get painted() { + return true; + }, + get live() { + return live; + }, + get path() { + return renderedPath; + }, + get centerTransform() { + return `translate(50%, 50%) translate(${((-renderedCols * renderedCellSize) / 2).toString()}px, ${((-renderedRows * renderedCellSize) / 2).toString()}px)`; + }, + get clipTransform() { + const width = Math.max(geometry.width, 1); + const height = Math.max(geometry.height, 1); + const x = (width - renderedCols * renderedCellSize) / 2; + const y = (height - renderedRows * renderedCellSize) / 2; + return `translate(${x.toString()} ${y.toString()})`; + }, + get cols() { + return renderedCols; + }, + get rows() { + return renderedRows; + }, + get cellSize() { + return renderedCellSize; + }, + get followCursor() { + return options.getFollowCursor(); + }, + get deviceGravity() { + return options.getDeviceGravity(); + }, + get deviceMotionEnabled() { + return options.getDeviceGravity() && options.getAnimated() && !reducedMotion.current; + }, + onPointerMove, + clearPointer, + onDeviceMotion, + onVisibilityChange, + }; +} diff --git a/src/lib/Puddle/runtime/puddleSimulation.ts b/src/lib/Puddle/runtime/puddleSimulation.ts new file mode 100644 index 0000000..4b6ab05 --- /dev/null +++ b/src/lib/Puddle/runtime/puddleSimulation.ts @@ -0,0 +1,67 @@ +import type { PuddleProps } from '../config'; +import type { PuddleGeometry } from '../geometry'; +import { createWaterSim, type WaterSim } from '../sim'; + +type PuddleSimulationProp = + | 'seed' + | 'level' + | 'noiseAmp' + | 'bowlWidth' + | 'bowlHeight' + | 'bowlAmp' + | 'bowlRim' + | 'integrator' + | 'momentumSmoothing' + | 'momentumRetention' + | 'timeScale' + | 'baseSubstep' + | 'cflSafety' + | 'minWaveDepth' + | 'maxSubsteps' + | 'gravityDrift' + | 'driftAmp' + | 'driftRateHz' + | 'rainInterval' + | 'rainAmount' + | 'rainRadius'; + +export type PuddleSimulationOptions = Required>; + +export function createPuddleSimulation( + geometry: Pick, + options: PuddleSimulationOptions, +): WaterSim { + return createWaterSim({ + nx: geometry.cols, + ny: geometry.rows, + seed: options.seed, + level: options.level, + noiseAmp: options.noiseAmp, + bowlHalfX: options.bowlWidth / geometry.width / 2, + bowlHalfY: options.bowlHeight / geometry.height / 2, + bowlAmp: options.bowlAmp, + bowlRim: options.bowlRim, + integrator: options.integrator, + momentumSmoothing: options.momentumSmoothing, + momentumRetention: options.momentumRetention, + timeScale: options.timeScale, + baseSubstep: options.baseSubstep, + cflSafety: options.cflSafety, + minWaveDepth: options.minWaveDepth, + maxSubsteps: options.maxSubsteps, + gravityDrift: options.gravityDrift, + driftAmp: options.driftAmp, + driftRateHz: options.driftRateHz, + rain: { + intervalSec: options.rainInterval, + amount: options.rainAmount, + radius: options.rainRadius, + }, + worldTerrain: { + cellSize: geometry.cellSize, + bowlWidth: options.bowlWidth, + bowlHeight: options.bowlHeight, + bowlRim: options.bowlRim, + }, + }); +} diff --git a/src/lib/Puddle/sim/brands.ts b/src/lib/Puddle/sim/brands.ts new file mode 100644 index 0000000..63f0ccc --- /dev/null +++ b/src/lib/Puddle/sim/brands.ts @@ -0,0 +1,30 @@ +export type Role = 'height' | 'flux' | 'scale' | 'terrain'; + +declare const roleBrand: unique symbol; +export interface Tex { + readonly [roleBrand]: RoleType; +} + +declare const secondsBrand: unique symbol; +export type Seconds = number & { readonly [secondsBrand]: 'Seconds' }; +export type SubstepSeconds = number & { readonly [secondsBrand]: 'SubstepSeconds' }; + +declare const worldUnitsBrand: unique symbol; +declare const texelCoordinateBrand: unique symbol; +declare const texelDistanceBrand: unique symbol; +declare const domainCoordinateBrand: unique symbol; +export type WorldUnits = number & { readonly [worldUnitsBrand]: 'WorldUnits' }; +export type TexelCoordinate = number & { readonly [texelCoordinateBrand]: 'TexelCoordinate' }; +export type TexelDistance = number & { readonly [texelDistanceBrand]: 'TexelDistance' }; +export type DomainCoordinate = number & { readonly [domainCoordinateBrand]: 'DomainCoordinate' }; + +export const seconds = (value: number): Seconds => value as Seconds; +export const substepSeconds = (value: number): SubstepSeconds => value as SubstepSeconds; +export const worldUnits = (value: number): WorldUnits => value as WorldUnits; +export const texelCoordinate = (value: number): TexelCoordinate => value as TexelCoordinate; +export const texelDistance = (value: number): TexelDistance => value as TexelDistance; +export const domainCoordinate = (value: number): DomainCoordinate => value as DomainCoordinate; + +export function assertNever(value: never, context = 'value'): never { + throw new Error(`Unexpected ${context}: ${JSON.stringify(value)}`); +} diff --git a/src/lib/Puddle/sim/emitters.ts b/src/lib/Puddle/sim/emitters.ts new file mode 100644 index 0000000..25125b8 --- /dev/null +++ b/src/lib/Puddle/sim/emitters.ts @@ -0,0 +1,84 @@ +import { assertNever, texelCoordinate, texelDistance, worldUnits } from './brands'; +import type { Resources } from './resources'; +import type { EmitCtx, Grid, SourceCommand, SourceEmitter } from './types'; + +function transformInPlace( + data: Float32Array, + transform: (value: number, dataIndex: number) => number, +): void { + data.set(data.map(transform)); +} + +function splatInto( + heightData: Float32Array, + grid: Grid, + centerX: number, + centerY: number, + amount: number, + radius: number, +): void { + const radiusSquared = Math.max(radius * radius, 1e-6); + transformInPlace(heightData, (height, dataIndex) => { + const distanceX = (dataIndex % grid.nx) - centerX; + const distanceY = Math.floor(dataIndex / grid.nx) - centerY; + return ( + height + amount * Math.exp(-(distanceX * distanceX + distanceY * distanceY) / radiusSquared) + ); + }); +} + +export function applyCommand(resources: Resources, command: SourceCommand): void { + switch (command.kind) { + case 'splat': + resources.applyToHeight((heightData, grid) => { + splatInto(heightData, grid, command.x, command.y, command.amount, command.radius); + }); + return; + case 'rain': + resources.applyToHeight((heightData) => { + transformInPlace(heightData, (height) => height + command.amount); + }); + return; + case 'fill': { + const terrain = resources.terrain().data; + resources.applyToHeight((heightData) => { + transformInPlace(heightData, (_, dataIndex) => + Math.max(0, command.level - (terrain[dataIndex] ?? 0)), + ); + }); + return; + } + case 'wipe': + resources.applyToHeight((heightData) => { + heightData.fill(0); + }); + return; + default: + assertNever(command, 'source command'); + } +} + +/** + * Rain as periodic raindrop splats. A drop fires each time sim-time crosses an + * `intervalSec` boundary, and the rng advances exactly once per drop — so the + * same seed yields the same sequence regardless of frame rate. + */ +export function rainEmitter(options: { + intervalSec: number; + amount: number; + radius: number; +}): SourceEmitter { + const { intervalSec, amount, radius } = options; + return ({ grid, time, dt, rng }: EmitCtx): readonly SourceCommand[] => { + const previousDropIndex = Math.floor(time / intervalSec); + const currentDropIndex = Math.floor((time + dt) / intervalSec); + const dropCount = Math.max(0, currentDropIndex - previousDropIndex); + return Array.from({ length: dropCount }, (): SourceCommand => ({ + kind: 'splat', + x: texelCoordinate(rng.range(0, grid.nx)), + y: texelCoordinate(rng.range(0, grid.ny)), + amount: worldUnits(amount), + radius: texelDistance(radius), + })); + }; +} diff --git a/src/lib/Puddle/sim/engine.ts b/src/lib/Puddle/sim/engine.ts new file mode 100644 index 0000000..a7c36da --- /dev/null +++ b/src/lib/Puddle/sim/engine.ts @@ -0,0 +1,203 @@ +// `advance` uses a capped time accumulator so simulation speed is display +// refresh-rate independent and backgrounded frames cannot create unbounded work. + +import { seconds, substepSeconds, type WorldUnits } from './brands'; +import { applyCommand } from './emitters'; +import { runSubstep } from './executor'; +import { DEFAULT_MAX_SUBSTEPS, maxFrameBudget, planSubsteps } from './governor'; +import type { Params } from './params'; +import { createResources, type Resources } from './resources'; +import { createRng, type Rng } from './rng'; +import type { + FrameStats, + GravityDriver, + Grid, + Integrator, + SourceCommand, + SourceEmitter, + StateStats, +} from './types'; + +export interface EngineConfig { + readonly grid: Grid; + readonly terrain: Float32Array; + readonly initialHeight: (xIndex: number, yIndex: number) => WorldUnits; + readonly params: Params; + readonly seed: number; + readonly integrator: Integrator; + readonly gravity: GravityDriver; + readonly emitters: readonly SourceEmitter[]; + /** Per-frame substep cap; defaults to `DEFAULT_MAX_SUBSTEPS`. */ + readonly maxSubsteps?: number | undefined; +} + +function scaleInPlace(values: Float32Array, scale: number): void { + values.set(values.map((value) => value * scale)); +} + +export class Engine { + readonly grid: Grid; + + private readonly initialHeight: EngineConfig['initialHeight']; + private readonly params: Params; + private readonly seed: number; + private readonly integrator: Integrator; + private readonly gravity: GravityDriver; + private readonly emitters: readonly SourceEmitter[]; + private readonly maxSubsteps: number; + private readonly resources: Resources; + private rng: Rng; + + private simClock = 0; // sim-seconds integrated + private accumulatedTime = 0; // unspent sim-time budget + private emittedUpTo = 0; // sim-time sources are emitted through (leads simClock by <= accumulatedTime) + + constructor(config: EngineConfig) { + this.grid = config.grid; + this.initialHeight = config.initialHeight; + this.params = config.params; + this.seed = config.seed; + this.integrator = config.integrator; + this.gravity = config.gravity; + this.emitters = config.emitters; + this.maxSubsteps = config.maxSubsteps ?? DEFAULT_MAX_SUBSTEPS; + this.resources = createResources(config.grid, config.terrain); + this.rng = createRng(config.seed); + this.resources.reset(config.initialHeight); + } + + // Emit across a sim-time window before planning so new depth affects the CFL bound. + private runEmitters(startTime: number, duration: number): void { + if (this.emitters.length === 0 || duration <= 0) return; + const context = { + grid: this.grid, + time: seconds(startTime), + dt: seconds(duration), + rng: this.rng, + }; + const commands = this.emitters.flatMap((emitter) => emitter(context)); + for (const command of commands) applyCommand(this.resources, command); + } + + /** Live ping-pong buffer; callers must not cache or write it. */ + height(): ArrayLike { + return this.resources.height().data; + } + + flux(): { readonly fx: ArrayLike; readonly fy: ArrayLike } { + const { fx, fy } = this.resources.currentFlux(); + return { fx, fy }; + } + + totalMass(): number { + return this.resources.stats().mass; + } + + stats(): StateStats { + return this.resources.stats(); + } + + advance(realDtSeconds: number): FrameStats { + const elapsedTime = Number.isFinite(realDtSeconds) ? Math.max(0, realDtSeconds) : 0; + this.accumulatedTime += elapsedTime * this.params.timeScale; + const frameBudget = maxFrameBudget(this.maxSubsteps, this.params.baseSubstep); + if (this.accumulatedTime > frameBudget) this.accumulatedTime = frameBudget; // Prevent a long frame from creating a runaway backlog. + // Emit over the full horizon so accumulator remainders are not emitted twice. + const horizon = this.simClock + this.accumulatedTime; + if (horizon > this.emittedUpTo) { + this.runEmitters(this.emittedUpTo, horizon - this.emittedUpTo); + this.emittedUpTo = horizon; + } + const plan = planSubsteps( + seconds(this.accumulatedTime), + this.resources.stats(), + this.integrator, + this.params, + this.maxSubsteps, + ); + let completedSubsteps = 0; + for (let substepIndex = 0; substepIndex < plan.substeps; substepIndex++) { + runSubstep( + this.resources, + this.integrator, + plan.dt, + this.params, + this.gravity.sample(seconds(this.simClock)), + ); + this.simClock += plan.dt; + completedSubsteps++; + } + this.accumulatedTime = Math.max(0, this.accumulatedTime - completedSubsteps * plan.dt); + const { mass, maxDepth } = this.resources.stats(); + return { + mass, + maxDepth, + substeps: completedSubsteps, + }; + } + + settle(substeps: number): void { + const gravitySample = this.gravity.sample(seconds(0)); + const dt = substepSeconds( + Math.min( + this.params.baseSubstep, + this.integrator.maxStableDt(this.resources.stats(), this.params), + ), + ); + for (let substepIndex = 0; substepIndex < substeps; substepIndex++) { + runSubstep(this.resources, this.integrator, dt, this.params, gravitySample); + } + } + + step(dt: number): void { + runSubstep( + this.resources, + this.integrator, + substepSeconds(dt), + this.params, + this.gravity.sample(seconds(0)), + ); + } + + emit(command: SourceCommand): void { + applyCommand(this.resources, command); + } + + clampMass(targetMass: number, knownStats?: Pick): void { + const knownMass = knownStats?.mass; + const currentMass = + knownMass !== undefined && Number.isFinite(knownMass) && knownMass >= 0 + ? knownMass + : this.resources.stats().mass; + if (currentMass <= targetMass || currentMass === 0) return; + const massScale = targetMass / currentMass; + this.resources.applyToHeight((heightData) => { + scaleInPlace(heightData, massScale); + }); + // Scale flux too, preserving velocity while reducing volume. + this.resources.applyToFlux((flux) => { + scaleInPlace(flux.fx, massScale); + scaleInPlace(flux.fy, massScale); + }); + } + + reset(): void { + this.resources.reset(this.initialHeight); + this.rng = createRng(this.seed); + this.simClock = 0; + this.accumulatedTime = 0; + this.emittedUpTo = 0; + } + + loadHeight(height: ArrayLike): void { + this.resources.loadHeight(height); + this.rng = createRng(this.seed); + this.simClock = 0; + this.accumulatedTime = 0; + this.emittedUpTo = 0; + } +} + +export function createEngine(config: EngineConfig): Engine { + return new Engine(config); +} diff --git a/src/lib/Puddle/sim/executor.ts b/src/lib/Puddle/sim/executor.ts new file mode 100644 index 0000000..1238796 --- /dev/null +++ b/src/lib/Puddle/sim/executor.ts @@ -0,0 +1,79 @@ +import type { Role, SubstepSeconds } from './brands'; +import type { Params } from './params'; +import { runKernel } from './passes'; +import type { Resources } from './resources'; +import type { GravitySample, Integrator, PassCtx, PassDesc, ScalarRole } from './types'; + +function bindPassContext( + resources: Resources, + passDescription: PassDesc, + substepSeconds: SubstepSeconds, + params: Params, + gravity: GravitySample, +): PassCtx { + const assertReadDeclared = (role: Role): void => { + if (!passDescription.reads.includes(role)) { + throw new Error(`pass ${passDescription.kind} does not declare a read of '${role}'`); + } + }; + return { + grid: resources.grid, + dt: substepSeconds, + params, + gravity, + readScalar: (role: ScalarRole) => { + assertReadDeclared(role); + return resources.currentScalar(role); + }, + readPrevScalar: (role: ScalarRole) => { + // Previous-generation reads still require a declared dependency. + assertReadDeclared(role); + return resources.previousScalar(role); + }, + readFlux: () => { + assertReadDeclared('flux'); + return resources.currentFlux(); + }, + writeScalar: () => { + if (passDescription.writes === 'flux') { + throw new Error(`pass ${passDescription.kind} writes flux, not a scalar`); + } + return resources.targetScalar(passDescription.writes); + }, + writeFlux: () => { + if (passDescription.writes !== 'flux') { + throw new Error(`pass ${passDescription.kind} does not write flux`); + } + return resources.targetFlux(); + }, + }; +} + +export function runSubstep( + resources: Resources, + integrator: Integrator, + substepSeconds: SubstepSeconds, + params: Params, + gravity: GravitySample, +): void { + for (const passDescription of integrator.passes) { + runKernel( + passDescription.kind, + bindPassContext(resources, passDescription, substepSeconds, params, gravity), + ); + resources.swap(passDescription.writes); + } +} + +export function runSubsteps( + resources: Resources, + integrator: Integrator, + substepCount: number, + substepSeconds: SubstepSeconds, + params: Params, + gravity: GravitySample, +): void { + for (let substepIndex = 0; substepIndex < substepCount; substepIndex++) { + runSubstep(resources, integrator, substepSeconds, params, gravity); + } +} diff --git a/src/lib/Puddle/sim/facade/config.ts b/src/lib/Puddle/sim/facade/config.ts new file mode 100644 index 0000000..f4897ce --- /dev/null +++ b/src/lib/Puddle/sim/facade/config.ts @@ -0,0 +1,84 @@ +import { texelDistance, worldUnits } from '../brands'; +import { rainEmitter } from '../emitters'; +import { createEngine, type EngineConfig } from '../engine'; +import { DEFAULT_MAX_SUBSTEPS } from '../governor'; +import { constantGravity, noiseGravity, offsetGravity } from '../gravity'; +import { integrators } from '../integrators'; +import { resolveParams } from '../params'; +import { makeTerrain } from '../terrain'; +import type { Grid, SourceEmitter } from '../types'; +import { SIM_DEFAULTS } from './defaults'; +import { + clampDimension, + finiteOrFallback, + MAX_DIM, + MIN_DIM, + nonnegativeIntegerOrZero, + type WaterSimOptions, +} from './options'; + +export interface WaterSimConfig { + readonly grid: Grid; + readonly engine: EngineConfig; +} + +export function createWaterSimConfig( + options: WaterSimOptions, + tiltOffset: Readonly<{ x: number; y: number }>, +): WaterSimConfig { + const minDim = Math.max(1, Math.floor(finiteOrFallback(options.minDim, MIN_DIM))); + const maxDim = Math.max(minDim, Math.floor(finiteOrFallback(options.maxDim, MAX_DIM))); + const grid: Grid = { + nx: clampDimension(options.nx, minDim, maxDim), + ny: clampDimension(options.ny, minDim, maxDim), + }; + const seed = finiteOrFallback(options.seed, 7); + const params = resolveParams({ ...SIM_DEFAULTS, ...options }); + const terrain = makeTerrain(grid, params, seed, options.worldTerrain); + const integrator = integrators[options.integrator ?? SIM_DEFAULTS.integrator]; + const baseGravity = + (options.gravityDrift ?? SIM_DEFAULTS.gravityDrift) + ? noiseGravity({ + perp: params.gravity, + seed, + amp: finiteOrFallback(options.driftAmp, SIM_DEFAULTS.driftAmp), + rateHz: options.driftRateHz, + }) + : constantGravity(params.gravity, params.tiltX, params.tiltY); + const gravity = offsetGravity(baseGravity, () => tiltOffset); + + const rainOptions = typeof options.rain === 'object' ? options.rain : {}; + const emitters: SourceEmitter[] = + options.rain === true || typeof options.rain === 'object' + ? [ + rainEmitter({ + intervalSec: finiteOrFallback(rainOptions.intervalSec, 0.6), + amount: worldUnits(finiteOrFallback(rainOptions.amount, 0.06)), + radius: texelDistance(finiteOrFallback(rainOptions.radius, 1.5)), + }), + ] + : []; + + const maxSubsteps = finiteOrFallback(options.maxSubsteps, DEFAULT_MAX_SUBSTEPS); + const substepCap = nonnegativeIntegerOrZero(maxSubsteps); + + return { + grid, + engine: { + grid, + terrain, + initialHeight: (xIndex, yIndex) => + worldUnits(Math.max(0, params.level - (terrain[yIndex * grid.nx + xIndex] ?? 0))), + params, + seed, + integrator, + gravity, + emitters, + maxSubsteps: substepCap, + }, + }; +} + +export function createConfiguredEngine(config: WaterSimConfig) { + return createEngine(config.engine); +} diff --git a/src/lib/Puddle/sim/facade/defaults.ts b/src/lib/Puddle/sim/facade/defaults.ts new file mode 100644 index 0000000..53e61ac --- /dev/null +++ b/src/lib/Puddle/sim/facade/defaults.ts @@ -0,0 +1,20 @@ +import type { IntegratorId } from '../integrators'; + +/** Defaults that affect simulation construction, independent of the Puddle UI. */ +export const SIM_DEFAULTS = { + level: 0.42, + integrator: 'pipes+momentum', + momentumSmoothing: 0.1, + momentumRetention: 0.98, + gravityDrift: true, + driftAmp: 1, +} as const satisfies { + readonly level: number; + readonly integrator: IntegratorId; + readonly momentumSmoothing: number; + readonly momentumRetention: number; + readonly gravityDrift: boolean; + readonly driftAmp: number; +}; + +export const DEFAULT_SETTLE_SUBSTEPS = 90; diff --git a/src/lib/Puddle/sim/facade/index.ts b/src/lib/Puddle/sim/facade/index.ts new file mode 100644 index 0000000..4c6dd1d --- /dev/null +++ b/src/lib/Puddle/sim/facade/index.ts @@ -0,0 +1,3 @@ +export { createWaterSim, WaterSim } from './waterSim'; +export type { WaterSimOptions } from './options'; +export { DEFAULT_SETTLE_SUBSTEPS, SIM_DEFAULTS } from './defaults'; diff --git a/src/lib/Puddle/sim/facade/options.ts b/src/lib/Puddle/sim/facade/options.ts new file mode 100644 index 0000000..16e64ae --- /dev/null +++ b/src/lib/Puddle/sim/facade/options.ts @@ -0,0 +1,44 @@ +import type { IntegratorId } from '../integrators'; +import type { ParamOverrides } from '../params'; +import type { WorldTerrainOptions } from '../terrain'; + +export interface WaterSimOptions extends ParamOverrides { + nx: number; + ny: number; + seed?: number; + /** `'pipes'` or the default momentum-enabled variant `'pipes+momentum'`. */ + integrator?: IntegratorId; + /** Install a rain emitter (raindrop ripples) for animated use. */ + rain?: boolean | { intervalSec?: number; amount?: number; radius?: number }; + /** Drift the gravity tilt with noise instead of holding it constant. */ + gravityDrift?: boolean; + /** Noise-drift tilt magnitude (only with `gravityDrift`). */ + driftAmp?: number; + /** Noise-drift rate in Hz (only with `gravityDrift`). */ + driftRateHz?: number; + /** Per-frame substep cap (spiral-of-death guard). */ + maxSubsteps?: number; + /** Grid dimension floor the nx/ny inputs are clamped to. */ + minDim?: number; + /** Grid dimension ceiling the nx/ny inputs are clamped to (guards against a giant grid). */ + maxDim?: number; + /** Stable, centered CSS-pixel terrain domain used by portable snapshots. */ + worldTerrain?: WorldTerrainOptions; +} + +export const MIN_DIM = 4; +export const MAX_DIM = 512; + +export function finiteOrFallback(value: number | undefined, fallback: number): number { + return value !== undefined && Number.isFinite(value) ? value : fallback; +} + +export function nonnegativeIntegerOrZero(value: number): number { + return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; +} + +export function clampDimension(dimension: number, minimum: number, maximum: number): number { + return Number.isFinite(dimension) + ? Math.max(minimum, Math.min(maximum, Math.floor(dimension))) + : minimum; +} diff --git a/src/lib/Puddle/sim/facade/waterSim.ts b/src/lib/Puddle/sim/facade/waterSim.ts new file mode 100644 index 0000000..6d65be4 --- /dev/null +++ b/src/lib/Puddle/sim/facade/waterSim.ts @@ -0,0 +1,93 @@ +import { texelCoordinate, texelDistance, worldUnits } from '../brands'; +import { params as paramSchema } from '../params'; +import type { FrameStats, StateStats } from '../types'; +import { createConfiguredEngine, createWaterSimConfig } from './config'; +import { DEFAULT_SETTLE_SUBSTEPS } from './defaults'; +import { type WaterSimOptions, nonnegativeIntegerOrZero } from './options'; + +const MAX_TILT = paramSchema.tiltX.max; + +function clampTilt(value: number): number { + return Math.max(-MAX_TILT, Math.min(MAX_TILT, value)); +} + +export class WaterSim { + readonly nx: number; + readonly ny: number; + + private readonly engine: ReturnType; + private readonly tiltOffset = { x: 0, y: 0 }; + + constructor(options: WaterSimOptions) { + const config = createWaterSimConfig(options, this.tiltOffset); + this.nx = config.grid.nx; + this.ny = config.grid.ny; + this.engine = createConfiguredEngine(config); + } + + /** Current per-cell depth for rendering. Copy it to retain a frame's values. */ + get height(): ArrayLike { + return this.engine.height(); + } + + get flux(): { readonly fx: ArrayLike; readonly fy: ArrayLike } { + return this.engine.flux(); + } + + /** Run substeps to bring poured water to rest (deterministic). */ + settle(substeps = DEFAULT_SETTLE_SUBSTEPS): void { + this.engine.settle(nonnegativeIntegerOrZero(substeps)); + } + + /** Advance by real elapsed seconds (framerate-independent). Returns this frame's stats. */ + advance(realDtSeconds: number): FrameStats { + return this.engine.advance(realDtSeconds); + } + + step(dt: number): void { + if (Number.isFinite(dt)) this.engine.step(dt); + } + + splat(x: number, y: number, amount: number, radius: number): void { + if (![x, y, amount, radius].every(Number.isFinite)) return; + this.engine.emit({ + kind: 'splat', + x: texelCoordinate(x), + y: texelCoordinate(y), + amount: worldUnits(amount), + radius: texelDistance(radius), + }); + } + + /** + * In-plane gravity tilt added on top of the configured driver (pointer / + * device-orientation interaction). Same sign convention as the `tiltX`/`tiltY` + * params: a negative x offset drives flow toward +x. Clamped to the tilt range. + */ + setTiltOffset(x: number, y: number): void { + if (![x, y].every(Number.isFinite)) return; + this.tiltOffset.x = clampTilt(x); + this.tiltOffset.y = clampTilt(y); + } + + clampMass(target: number, knownStats?: Pick): void { + if (Number.isFinite(target)) this.engine.clampMass(Math.max(0, target), knownStats); + } + + totalMass(): number { + return this.engine.totalMass(); + } + + reset(): void { + this.engine.reset(); + } + + /** Replace depth while resetting flux and clocks; used for portable settled snapshots. */ + loadHeight(height: ArrayLike): void { + this.engine.loadHeight(height); + } +} + +export function createWaterSim(options: WaterSimOptions): WaterSim { + return new WaterSim(options); +} diff --git a/src/lib/Puddle/sim/field.ts b/src/lib/Puddle/sim/field.ts new file mode 100644 index 0000000..541c836 --- /dev/null +++ b/src/lib/Puddle/sim/field.ts @@ -0,0 +1,19 @@ +// CPU realization of the "textures" the passes read and write. Scalar fields are +// cell-centered; flux is the MAC-staggered face pair. + +import type { FluxField, Grid, ScalarField } from './types'; + +export function makeScalar(grid: Grid): ScalarField { + const { nx, ny } = grid; + return { nx, ny, data: new Float32Array(nx * ny) }; +} + +export function makeFlux(grid: Grid): FluxField { + const { nx, ny } = grid; + return { + nx, + ny, + fx: new Float32Array((nx + 1) * ny), + fy: new Float32Array(nx * (ny + 1)), + }; +} diff --git a/src/lib/Puddle/sim/governor.ts b/src/lib/Puddle/sim/governor.ts new file mode 100644 index 0000000..457df02 --- /dev/null +++ b/src/lib/Puddle/sim/governor.ts @@ -0,0 +1,30 @@ +import { type Seconds, type SubstepSeconds, substepSeconds } from './brands'; +import type { Params } from './params'; +import type { Integrator, StateStats } from './types'; + +export const DEFAULT_MAX_SUBSTEPS = 8; + +function substepCount(frameDt: number, substepDt: number, maxSubsteps: number): number { + const desiredSubsteps = substepDt > 0 ? Math.floor(frameDt / substepDt) : 0; + return Math.max(0, Math.min(maxSubsteps, desiredSubsteps)); +} + +export function planSubsteps( + frameDt: Seconds, + stats: StateStats, + integrator: Integrator, + params: Params, + maxSubsteps: number, +) { + const stableDt = integrator.maxStableDt(stats, params); + const substepDt = Math.min(params.baseSubstep, stableDt); + // Floor preserves the accumulator remainder; rounding would overdraw its budget. + return { + substeps: substepCount(frameDt, substepDt, maxSubsteps), + dt: substepSeconds(substepDt), + }; +} + +export function maxFrameBudget(maxSubsteps: number, baseSubstep: number): SubstepSeconds { + return substepSeconds(maxSubsteps * baseSubstep); +} diff --git a/src/lib/Puddle/sim/gravity.ts b/src/lib/Puddle/sim/gravity.ts new file mode 100644 index 0000000..fd517cc --- /dev/null +++ b/src/lib/Puddle/sim/gravity.ts @@ -0,0 +1,56 @@ +import type { Seconds } from './brands'; +import { valueNoise } from './noise'; +import type { GravityDriver, GravitySample } from './types'; + +const finiteOrFallback = (value: number | undefined, fallback: number): number => + value !== undefined && Number.isFinite(value) ? value : fallback; + +/** Fixed hydrostatic gravity plus a constant in-plane tilt. */ +export function constantGravity(perpendicular: number, tiltX = 0, tiltY = 0): GravityDriver { + const sample: GravitySample = { perp: perpendicular, tiltX, tiltY }; + return { + sample: () => sample, + serialize: () => ({ kind: 'constant', perp: perpendicular, tiltX, tiltY }), + }; +} + +export function offsetGravity( + baseGravity: GravityDriver, + getOffset: () => { readonly x: number; readonly y: number }, +): GravityDriver { + const sample = (time: Seconds): GravitySample => { + const baseSample = baseGravity.sample(time); + const offset = getOffset(); + return { + perp: baseSample.perp, + tiltX: baseSample.tiltX + offset.x, + tiltY: baseSample.tiltY + offset.y, + }; + }; + return { + sample, + serialize: () => ({ kind: 'offset', base: baseGravity.serialize(), offset: getOffset() }), + }; +} + +export function noiseGravity(options: { + perp: number; + amp?: number | undefined; + rateHz?: number | undefined; + seed?: number | undefined; +}): GravityDriver { + const { perp } = options; + // finite-guard, not `??`: a NaN drift amp/rate would poison the tilt forever. + const amplitude = finiteOrFallback(options.amp, 2); + const rateHz = finiteOrFallback(options.rateHz, 0.05); + const seed = options.seed ?? 1; + const sample = (time: Seconds): GravitySample => { + const phase = time * rateHz; + return { + perp, + tiltX: (valueNoise(phase, 0, seed) - 0.5) * 2 * amplitude, + tiltY: (valueNoise(0, phase, seed + 7) - 0.5) * 2 * amplitude, + }; + }; + return { sample, serialize: () => ({ kind: 'noise', perp, amp: amplitude, rateHz, seed }) }; +} diff --git a/src/lib/Puddle/sim/index.ts b/src/lib/Puddle/sim/index.ts new file mode 100644 index 0000000..c38c767 --- /dev/null +++ b/src/lib/Puddle/sim/index.ts @@ -0,0 +1,41 @@ +export { createWaterSim, DEFAULT_SETTLE_SUBSTEPS, SIM_DEFAULTS, WaterSim } from './facade'; +export type { WaterSimOptions } from './facade'; + +export { integrators } from './integrators'; +export type { IntegratorId } from './integrators'; +export { params as paramSchema, resolveParams } from './params'; +export type { ParamKey, ParamOverrides, Params } from './params'; +export { Engine, createEngine } from './engine'; +export type { EngineConfig } from './engine'; +export { constantGravity, noiseGravity, offsetGravity } from './gravity'; +export { DEFAULT_MAX_SUBSTEPS } from './governor'; +export { rainEmitter } from './emitters'; +export { makeTerrain } from './terrain'; +export { + domainCoordinate, + seconds, + substepSeconds, + texelCoordinate, + texelDistance, + worldUnits, +} from './brands'; +export type { + DomainCoordinate, + Seconds, + SubstepSeconds, + TexelCoordinate, + TexelDistance, + WorldUnits, +} from './brands'; +export type { Rng } from './rng'; +export type { + EmitCtx, + FrameStats, + GravityDriver, + GravitySample, + Grid, + Integrator, + SourceCommand, + SourceEmitter, + StateStats, +} from './types'; diff --git a/src/lib/Puddle/sim/integrators.ts b/src/lib/Puddle/sim/integrators.ts new file mode 100644 index 0000000..8f1191a --- /dev/null +++ b/src/lib/Puddle/sim/integrators.ts @@ -0,0 +1,29 @@ +// Passes are swapped as whole pipelines because their state-layout contracts are +// not compiler-visible. Momentum extends the pipes pipeline with an optional appended pass. + +import { type SubstepSeconds, substepSeconds } from './brands'; +import type { Params } from './params'; +import { PASSES } from './passes'; +import type { Integrator, StateStats } from './types'; + +// Gravity-wave CFL: dt < dx / sqrt(g * hMax), dx = 1. `cflSafety` keeps a margin; +// `minWaveDepth` floors hMax so an empty domain doesn't yield an infinite dt. +function cflStableDt(stats: StateStats, params: Params): SubstepSeconds { + const maxWaveDepth = Math.max(stats.maxDepth, params.minWaveDepth); + return substepSeconds(params.cflSafety / Math.sqrt(params.gravity * maxWaveDepth)); +} + +export const pipes: Integrator = { + id: 'pipes', + passes: [PASSES.fluxUpdate, PASSES.outflowScale, PASSES.heightUpdate], + maxStableDt: cflStableDt, +}; + +export const pipesMomentum: Integrator = { + id: 'pipes+momentum', + passes: [...pipes.passes, PASSES.momentum], + maxStableDt: cflStableDt, +}; + +export const integrators = { pipes, 'pipes+momentum': pipesMomentum } as const; +export type IntegratorId = keyof typeof integrators; diff --git a/src/lib/Puddle/sim/noise.ts b/src/lib/Puddle/sim/noise.ts new file mode 100644 index 0000000..1cbfa85 --- /dev/null +++ b/src/lib/Puddle/sim/noise.ts @@ -0,0 +1,67 @@ +import type { DomainCoordinate } from './brands'; + +function smooth(value: number): number { + return value * value * (3 - 2 * value); +} + +// Maps lattice coordinates to a deterministic value in [0, 1). +function hashLatticePoint(latticeX: number, latticeY: number, seed: number): number { + const hash = (latticeX * 374761393 + latticeY * 668265263 + seed * 2246822519) | 0; + const mixedHash = Math.imul(hash ^ (hash >>> 13), 1274126177); + return ((mixedHash ^ (mixedHash >>> 16)) >>> 0) / 4294967296; +} + +export function valueNoise(x: number, y: number, seed: number): number { + // Lattice-cell origin and smoothed local coordinates within that cell. + const latticeX = Math.floor(x); + const latticeY = Math.floor(y); + const blendX = smooth(x - latticeX); + const blendY = smooth(y - latticeY); + // Deterministic values at the cell's four corners, bilinearly interpolated below. + const corner00 = hashLatticePoint(latticeX, latticeY, seed); + const corner10 = hashLatticePoint(latticeX + 1, latticeY, seed); + const corner01 = hashLatticePoint(latticeX, latticeY + 1, seed); + const corner11 = hashLatticePoint(latticeX + 1, latticeY + 1, seed); + return ( + (corner00 * (1 - blendX) + corner10 * blendX) * (1 - blendY) + + (corner01 * (1 - blendX) + corner11 * blendX) * blendY + ); +} + +export function fbm(x: number, y: number, seed: number, octaves = 4): number { + const octaveIndices = Array.from( + { length: Math.max(0, Math.ceil(octaves)) }, + (_, octave) => octave, + ); + const { weightedSum, amplitudeSum } = octaveIndices.reduce( + (sums, octave) => { + const amplitude = 0.5 ** (octave + 1); + const frequency = 2 ** octave; + return { + weightedSum: + sums.weightedSum + + amplitude * valueNoise(x * frequency, y * frequency, seed + octave * 101), + amplitudeSum: sums.amplitudeSum + amplitude, + }; + }, + { weightedSum: 0, amplitudeSum: 0 }, + ); + return weightedSum / amplitudeSum; +} + +// Signed distance to a rounded box; negative inside. +export function sdRoundRect( + x: DomainCoordinate, + y: DomainCoordinate, + halfWidth: number, + halfHeight: number, + radius: number, +): number { + const roundedDistanceX = Math.abs(x) - halfWidth + radius; + const roundedDistanceY = Math.abs(y) - halfHeight + radius; + return ( + Math.min(Math.max(roundedDistanceX, roundedDistanceY), 0) + + Math.hypot(Math.max(roundedDistanceX, 0), Math.max(roundedDistanceY, 0)) - + radius + ); +} diff --git a/src/lib/Puddle/sim/params.ts b/src/lib/Puddle/sim/params.ts new file mode 100644 index 0000000..eb176a5 --- /dev/null +++ b/src/lib/Puddle/sim/params.ts @@ -0,0 +1,58 @@ +import { worldUnits, type WorldUnits } from './brands'; + +export interface ParamSpec { + readonly default: number; + readonly min: number; + readonly max: number; +} +export type ParamSchema = Record; + +export const params = { + gravity: { default: 9.8, min: 0.1, max: 40 }, + damping: { default: 0.96, min: 0, max: 0.9999 }, + level: { default: worldUnits(0.58), min: 0.05, max: 4 }, + noiseAmp: { default: worldUnits(0.4), min: 0, max: 2 }, + noiseFreq: { default: 3.4, min: 0.1, max: 16 }, + tiltX: { default: 0, min: -20, max: 20 }, + tiltY: { default: 0, min: -20, max: 20 }, + // Prevents unstable velocity near a dry shoreline. + depthFloor: { default: worldUnits(0.02), min: 1e-4, max: 1 }, + // Blend toward the 4-neighbor mean to suppress grid-scale oscillation. + momentumSmoothing: { default: 0.5, min: 0, max: 1 }, + // Per-step momentum retention; 1 is lossless. + momentumRetention: { default: 0.82, min: 0, max: 1 }, + // CFL margin in dt = cflSafety * dx / sqrt(g * hMax). + cflSafety: { default: 0.9, min: 0.1, max: 1 }, + // Avoids an infinite CFL timestep in a dry domain. + minWaveDepth: { default: worldUnits(0.001), min: 1e-4, max: 1 }, + // Target timestep, further limited by CFL. + baseSubstep: { default: 0.08, min: 0.005, max: 0.5 }, + // Simulated seconds per real second. + timeScale: { default: 6, min: 0.1, max: 30 }, + // Terrain bowl dimensions in normalized [-0.5, 0.5] space. + bowlHalfX: { default: 0.34, min: 0.05, max: 0.5 }, + bowlHalfY: { default: 0.3, min: 0.05, max: 0.5 }, + bowlRadius: { default: 0.16, min: 0, max: 0.5 }, + bowlRim: { default: 0.22, min: 0.02, max: 0.5 }, + bowlAmp: { default: worldUnits(1.4), min: 0.1, max: 8 }, +} as const satisfies ParamSchema; + +export type ParamKey = keyof typeof params; +export type Params = { + [Key in ParamKey]: (typeof params)[Key]['default'] extends WorldUnits ? WorldUnits : number; +}; +export type ParamOverrides = Partial>; + +function clamp(value: number, parameterSpec: ParamSpec): number { + if (!Number.isFinite(value)) return parameterSpec.default; + return Math.min(parameterSpec.max, Math.max(parameterSpec.min, value)); +} + +export function resolveParams(overrides: ParamOverrides = {}): Params { + return Object.fromEntries( + (Object.keys(params) as ParamKey[]).map((key) => { + const override = overrides[key]; + return [key, override === undefined ? params[key].default : clamp(override, params[key])]; + }), + ) as Params; +} diff --git a/src/lib/Puddle/sim/passes/common.ts b/src/lib/Puddle/sim/passes/common.ts new file mode 100644 index 0000000..644bf05 --- /dev/null +++ b/src/lib/Puddle/sim/passes/common.ts @@ -0,0 +1,65 @@ +export type FieldTransform = (value: number, index: number, column: number, row: number) => number; + +export function writeField(field: Float32Array, width: number, transform: FieldTransform): void { + for (let index = 0; index < field.length; index += 1) { + field[index] = transform(field[index] ?? 0, index, index % width, Math.floor(index / width)); + } +} + +export function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +export function sampleFace( + field: Float32Array, + width: number, + height: number, + positionX: number, + positionY: number, + offsetX: number, + offsetY: number, +): number { + const gridX = clamp(positionX - offsetX, 0, width - 1); + const gridY = clamp(positionY - offsetY, 0, height - 1); + const leftColumn = Math.floor(gridX); + const bottomRow = Math.floor(gridY); + const rightColumn = Math.min(width - 1, leftColumn + 1); + const topRow = Math.min(height - 1, bottomRow + 1); + const xWeight = gridX - leftColumn; + const yWeight = gridY - bottomRow; + const bottomLeft = field[bottomRow * width + leftColumn] ?? 0; + const bottomRight = field[bottomRow * width + rightColumn] ?? 0; + const topLeft = field[topRow * width + leftColumn] ?? 0; + const topRight = field[topRow * width + rightColumn] ?? 0; + return ( + (bottomLeft * (1 - xWeight) + bottomRight * xWeight) * (1 - yWeight) + + (topLeft * (1 - xWeight) + topRight * xWeight) * yWeight + ); +} + +export function smoothField( + field: Float32Array, + scratch: Float32Array, + width: number, + height: number, + minColumn: number, + maxColumn: number, + minRow: number, + maxRow: number, + strength: number, +): void { + scratch.set(field.subarray(0, width * height)); + writeField(field, width, (value, index, column, row) => { + if (column < minColumn || column >= maxColumn || row < minRow || row >= maxRow) { + return value; + } + + const mean = + ((scratch[index - 1] ?? 0) + + (scratch[index + 1] ?? 0) + + (scratch[index - width] ?? 0) + + (scratch[index + width] ?? 0)) * + 0.25; + return (scratch[index] ?? 0) * (1 - strength) + mean * strength; + }); +} diff --git a/src/lib/Puddle/sim/passes/index.ts b/src/lib/Puddle/sim/passes/index.ts new file mode 100644 index 0000000..7fadba0 --- /dev/null +++ b/src/lib/Puddle/sim/passes/index.ts @@ -0,0 +1,57 @@ +import type { PassCtx, PassDesc, PassKind } from '../types'; +import { runMomentum } from './momentum'; +import { runFluxUpdate, runHeightUpdate, runOutflowScale } from './pipes'; + +interface PassRegistration { + readonly reads: PassDesc['reads']; + readonly writes: PassDesc['writes']; + readonly label: string; + readonly run: (ctx: PassCtx) => void; +} + +const PASS_REGISTRY = { + flux_update: { + reads: ['height', 'terrain', 'flux'], + writes: 'flux', + label: 'flux update (virtual pipes)', + run: runFluxUpdate, + }, + outflow_scale: { + reads: ['flux', 'height'], + writes: 'scale', + label: 'outflow scaling', + run: runOutflowScale, + }, + height_update: { + reads: ['flux', 'scale', 'height'], + writes: 'height', + label: 'height update', + run: runHeightUpdate, + }, + momentum: { + reads: ['flux', 'height', 'scale'], + writes: 'flux', + label: 'momentum transport (semi-Lagrangian)', + run: runMomentum, + }, +} as const satisfies Record; + +function descriptor(kind: PassKind): PassDesc { + const { reads, writes } = PASS_REGISTRY[kind]; + return { kind, reads, writes }; +} + +export const PASSES = { + fluxUpdate: descriptor('flux_update'), + outflowScale: descriptor('outflow_scale'), + heightUpdate: descriptor('height_update'), + momentum: descriptor('momentum'), +} as const; + +export function runKernel(kind: PassKind, ctx: PassCtx): void { + PASS_REGISTRY[kind].run(ctx); +} + +export function describePass(kind: PassKind): string { + return PASS_REGISTRY[kind].label; +} diff --git a/src/lib/Puddle/sim/passes/momentum.ts b/src/lib/Puddle/sim/passes/momentum.ts new file mode 100644 index 0000000..c8ff8e5 --- /dev/null +++ b/src/lib/Puddle/sim/passes/momentum.ts @@ -0,0 +1,108 @@ +import type { Grid, PassCtx } from '../types'; +import { clamp, sampleFace, smoothField, writeField } from './common'; + +interface MomentumScratch { + xVelocity: Float32Array; + yVelocity: Float32Array; + smoothingScratch: Float32Array; +} + +const scratchByGrid = new WeakMap(); + +function getMomentumScratch(grid: Grid): MomentumScratch { + const { nx, ny } = grid; + const cached = scratchByGrid.get(grid); + if (cached) return cached; + const scratch = { + xVelocity: new Float32Array((nx + 1) * ny), + yVelocity: new Float32Array(nx * (ny + 1)), + smoothingScratch: new Float32Array(Math.max((nx + 1) * ny, nx * (ny + 1))), + }; + scratchByGrid.set(grid, scratch); + return scratch; +} + +export function runMomentum(ctx: PassCtx): void { + const { nx, ny } = ctx.grid; + const inputFlux = ctx.readFlux(); + const updatedDepth = ctx.readScalar('height').data; + // Flux-to-velocity uses the pre-update upwind depth to avoid a dry-cell velocity spike. + const previousDepth = ctx.readPrevScalar('height').data; + const scale = ctx.readScalar('scale').data; + const outputFlux = ctx.writeFlux(); + const { dt, params } = ctx; + const { depthFloor, momentumSmoothing, momentumRetention } = params; + const { xVelocity, yVelocity, smoothingScratch } = getMomentumScratch(ctx.grid); + const maxVelocity = dt > 0 ? 1 / dt : 0; + + writeField(xVelocity, nx + 1, (_value, faceIndex, column, row) => { + if (column === 0 || column === nx) return 0; + const faceFlux = inputFlux.fx[faceIndex] ?? 0; + const upstreamCell = faceFlux > 0 ? row * nx + (column - 1) : row * nx + column; + const sourceDepth = previousDepth[upstreamCell] ?? 0; + return sourceDepth > depthFloor + ? clamp((faceFlux * (scale[upstreamCell] ?? 0)) / sourceDepth, -maxVelocity, maxVelocity) + : 0; + }); + writeField(yVelocity, nx, (_value, faceIndex, column, row) => { + if (row === 0 || row === ny) return 0; + const faceFlux = inputFlux.fy[faceIndex] ?? 0; + const upstreamCell = faceFlux > 0 ? (row - 1) * nx + column : row * nx + column; + const sourceDepth = previousDepth[upstreamCell] ?? 0; + return sourceDepth > depthFloor + ? clamp((faceFlux * (scale[upstreamCell] ?? 0)) / sourceDepth, -maxVelocity, maxVelocity) + : 0; + }); + + writeField(outputFlux.fx, nx + 1, (_value, faceIndex, column, row) => { + if (column === 0 || column === nx) return 0; + const normalVelocity = xVelocity[faceIndex] ?? 0; + const tangentialVelocity = + ((yVelocity[row * nx + (column - 1)] ?? 0) + + (yVelocity[row * nx + column] ?? 0) + + (yVelocity[(row + 1) * nx + (column - 1)] ?? 0) + + (yVelocity[(row + 1) * nx + column] ?? 0)) * + 0.25; + const advectedVelocity = sampleFace( + xVelocity, + nx + 1, + ny, + column - dt * normalVelocity, + row + 0.5 - dt * tangentialVelocity, + 0, + 0.5, + ); + const sourceDepth = + advectedVelocity > 0 + ? (updatedDepth[row * nx + (column - 1)] ?? 0) + : (updatedDepth[row * nx + column] ?? 0); + return sourceDepth > depthFloor ? momentumRetention * advectedVelocity * sourceDepth : 0; + }); + writeField(outputFlux.fy, nx, (_value, faceIndex, column, row) => { + if (row === 0 || row === ny) return 0; + const normalVelocity = yVelocity[faceIndex] ?? 0; + const tangentialVelocity = + ((xVelocity[(row - 1) * (nx + 1) + column] ?? 0) + + (xVelocity[(row - 1) * (nx + 1) + column + 1] ?? 0) + + (xVelocity[row * (nx + 1) + column] ?? 0) + + (xVelocity[row * (nx + 1) + column + 1] ?? 0)) * + 0.25; + const advectedVelocity = sampleFace( + yVelocity, + nx, + ny + 1, + column + 0.5 - dt * tangentialVelocity, + row - dt * normalVelocity, + 0.5, + 0, + ); + const sourceDepth = + advectedVelocity > 0 + ? (updatedDepth[(row - 1) * nx + column] ?? 0) + : (updatedDepth[row * nx + column] ?? 0); + return sourceDepth > depthFloor ? momentumRetention * advectedVelocity * sourceDepth : 0; + }); + + smoothField(outputFlux.fx, smoothingScratch, nx + 1, ny, 1, nx, 1, ny - 1, momentumSmoothing); + smoothField(outputFlux.fy, smoothingScratch, nx, ny + 1, 1, nx - 1, 1, ny, momentumSmoothing); +} diff --git a/src/lib/Puddle/sim/passes/pipes.ts b/src/lib/Puddle/sim/passes/pipes.ts new file mode 100644 index 0000000..387fc2f --- /dev/null +++ b/src/lib/Puddle/sim/passes/pipes.ts @@ -0,0 +1,107 @@ +import type { PassCtx } from '../types'; +import { writeField } from './common'; + +function scaleDirectionalFlux( + flux: number, + positiveSourceScale: number, + negativeSourceScale: number, +): number { + return flux * (flux > 0 ? positiveSourceScale : negativeSourceScale); +} + +// Clearing boundary faces keeps them at zero: no-flow walls must not retain stale ping-pong fluxes. +// Interior faces accelerate from free-surface elevation plus in-plane gravity. +export function runFluxUpdate(ctx: PassCtx): void { + const { nx, ny } = ctx.grid; + const depth = ctx.readScalar('height').data; + const terrain = ctx.readScalar('terrain').data; + const inputFlux = ctx.readFlux(); + const outputFlux = ctx.writeFlux(); + const { dt, gravity, params } = ctx; + + writeField(outputFlux.fx, nx + 1, (_value, faceIndex, column, row) => { + if (column === 0 || column === nx) return 0; + const leftCell = row * nx + (column - 1); + const rightCell = row * nx + column; + const surfaceDelta = + (terrain[leftCell] ?? 0) + + (depth[leftCell] ?? 0) - + (terrain[rightCell] ?? 0) - + (depth[rightCell] ?? 0); + return ( + params.damping * (inputFlux.fx[faceIndex] ?? 0) + + dt * (gravity.perp * surfaceDelta - gravity.tiltX) + ); + }); + writeField(outputFlux.fy, nx, (_value, faceIndex, column, row) => { + if (row === 0 || row === ny) return 0; + const bottomCell = (row - 1) * nx + column; + const topCell = row * nx + column; + const surfaceDelta = + (terrain[bottomCell] ?? 0) + + (depth[bottomCell] ?? 0) - + (terrain[topCell] ?? 0) - + (depth[topCell] ?? 0); + return ( + params.damping * (inputFlux.fy[faceIndex] ?? 0) + + dt * (gravity.perp * surfaceDelta - gravity.tiltY) + ); + }); +} + +// Uniformly cap each cell's outflow to its available volume, conserving mass. +export function runOutflowScale(ctx: PassCtx): void { + const { nx } = ctx.grid; + const flux = ctx.readFlux(); + const depth = ctx.readScalar('height').data; + const scale = ctx.writeScalar().data; + const { dt } = ctx; + writeField(scale, nx, (_value, cell, column, row) => { + const outflow = + Math.max(0, -(flux.fx[row * (nx + 1) + column] ?? 0)) + + Math.max(0, flux.fx[row * (nx + 1) + column + 1] ?? 0) + + Math.max(0, -(flux.fy[row * nx + column] ?? 0)) + + Math.max(0, flux.fy[(row + 1) * nx + column] ?? 0); + const outflowVolume = outflow * dt; + const availableDepth = depth[cell] ?? 0; + return outflowVolume > availableDepth && outflowVolume > 0 ? availableDepth / outflowVolume : 1; + }); +} + +// A face is scaled by its source cell, preserving the outflow cap above. +export function runHeightUpdate(ctx: PassCtx): void { + const { nx } = ctx.grid; + const flux = ctx.readFlux(); + const scale = ctx.readScalar('scale').data; + const depth = ctx.readScalar('height').data; + const nextDepth = ctx.writeScalar().data; + const { dt } = ctx; + writeField(nextDepth, nx, (_value, cell, column, row) => { + const leftFlux = flux.fx[row * (nx + 1) + column] ?? 0; + const rightFlux = flux.fx[row * (nx + 1) + column + 1] ?? 0; + const bottomFlux = flux.fy[row * nx + column] ?? 0; + const topFlux = flux.fy[(row + 1) * nx + column] ?? 0; + const cellScale = scale[cell] ?? 0; + const scaledLeftFlux = scaleDirectionalFlux( + leftFlux, + column > 0 ? (scale[cell - 1] ?? 0) : 0, + cellScale, + ); + const scaledRightFlux = scaleDirectionalFlux( + rightFlux, + cellScale, + column < nx - 1 ? (scale[cell + 1] ?? 0) : 0, + ); + const scaledBottomFlux = scaleDirectionalFlux( + bottomFlux, + row > 0 ? (scale[cell - nx] ?? 0) : 0, + cellScale, + ); + const scaledTopFlux = scaleDirectionalFlux(topFlux, cellScale, scale[cell + nx] ?? 0); + return Math.max( + 0, + (depth[cell] ?? 0) + + dt * (scaledLeftFlux - scaledRightFlux + scaledBottomFlux - scaledTopFlux), + ); + }); +} diff --git a/src/lib/Puddle/sim/resources.ts b/src/lib/Puddle/sim/resources.ts new file mode 100644 index 0000000..b367385 --- /dev/null +++ b/src/lib/Puddle/sim/resources.ts @@ -0,0 +1,149 @@ +import { worldUnits, type Role, type WorldUnits } from './brands'; +import { makeFlux, makeScalar } from './field'; +import type { FluxField, Grid, ScalarField, ScalarRole, StateStats } from './types'; + +type MutableRole = Exclude; +type MutableScalarRole = Exclude; +type BufferPair = readonly [T, T]; +type BufferSide = 'current' | 'other'; + +const makeBufferPair = (createBuffer: () => T): BufferPair => [ + createBuffer(), + createBuffer(), +]; + +export class Resources { + readonly grid: Grid; + + private readonly heightBuffers: BufferPair; + private readonly fluxBuffers: BufferPair; + private readonly scaleBuffers: BufferPair; + private readonly scalarBuffers: Record>; + private readonly terrainField: ScalarField; + // For each role, 0 or 1 identifies which buffer in its pair is current. + private readonly currentBufferIndices: Record = { + height: 0, + flux: 0, + scale: 0, + }; + + constructor(grid: Grid, terrainData: Float32Array) { + this.grid = grid; + this.heightBuffers = makeBufferPair(() => makeScalar(grid)); + this.fluxBuffers = makeBufferPair(() => makeFlux(grid)); + this.scaleBuffers = makeBufferPair(() => makeScalar(grid)); + this.scalarBuffers = { + height: this.heightBuffers, + scale: this.scaleBuffers, + }; + this.terrainField = { nx: grid.nx, ny: grid.ny, data: terrainData }; + } + + currentScalar(role: ScalarRole): ScalarField { + return this.selectScalarBuffer(role, 'current'); + } + + currentFlux(): FluxField { + return Resources.selectBuffer(this.fluxBuffers, this.currentBufferIndices.flux, 'current'); + } + + targetScalar(role: ScalarRole): ScalarField { + if (role === 'terrain') throw new Error("role 'terrain' is static and has no write target"); + return this.selectScalarBuffer(role, 'other'); + } + + targetFlux(): FluxField { + return Resources.selectBuffer(this.fluxBuffers, this.currentBufferIndices.flux, 'other'); + } + + /** + * The other buffer holds the role's value before its last swap. It aliases + * `targetScalar`, whose name is intentionally reserved for writes. + */ + previousScalar(role: ScalarRole): ScalarField { + return this.selectScalarBuffer(role, 'other'); + } + + swap(role: Role): void { + if (role === 'terrain') return; + this.currentBufferIndices[role] = this.currentBufferIndices[role] === 0 ? 1 : 0; + } + + height(): ScalarField { + return this.currentScalar('height'); + } + + terrain(): ScalarField { + return this.terrainField; + } + + applyToHeight(operation: (heightData: Float32Array, grid: Grid) => void): void { + operation(this.height().data, this.grid); + } + + applyToFlux(operation: (flux: FluxField, grid: Grid) => void): void { + operation(this.currentFlux(), this.grid); + } + + stats(): StateStats { + const heightData = this.height().data; + let mass = 0; + let maxDepth = 0; + let index = 0; + while (index < heightData.length) { + const depth = heightData[index] ?? 0; + mass += depth; + if (depth > maxDepth) maxDepth = depth; + index += 1; + } + return { mass, maxDepth: worldUnits(maxDepth) }; + } + + reset(initialHeight: (xIndex: number, yIndex: number) => WorldUnits): void { + this.currentBufferIndices.height = 0; + this.currentBufferIndices.flux = 0; + this.currentBufferIndices.scale = 0; + const initialHeightData = this.heightBuffers[0].data; + initialHeightData.set( + initialHeightData.map((_, index) => + initialHeight(index % this.grid.nx, Math.floor(index / this.grid.nx)), + ), + ); + this.heightBuffers[1].data.fill(0); + for (const { fx, fy } of this.fluxBuffers) { + fx.fill(0); + fy.fill(0); + } + for (const { data } of this.scaleBuffers) data.fill(0); + } + + loadHeight(height: ArrayLike): void { + if (height.length !== this.grid.nx * this.grid.ny) { + throw new Error('Snapshot depth dimensions do not match the simulation grid'); + } + this.currentBufferIndices.height = 0; + this.currentBufferIndices.flux = 0; + this.currentBufferIndices.scale = 0; + this.heightBuffers[0].data.set(height); + this.heightBuffers[1].data.fill(0); + for (const { fx, fy } of this.fluxBuffers) { + fx.fill(0); + fy.fill(0); + } + for (const { data } of this.scaleBuffers) data.fill(0); + } + + private static selectBuffer(buffers: BufferPair, currentIndex: 0 | 1, side: BufferSide): T { + return buffers[side === 'current' ? currentIndex : currentIndex === 0 ? 1 : 0]; + } + + private selectScalarBuffer(role: ScalarRole, side: BufferSide): ScalarField { + return role === 'terrain' + ? this.terrainField + : Resources.selectBuffer(this.scalarBuffers[role], this.currentBufferIndices[role], side); + } +} + +export function createResources(grid: Grid, terrainData: Float32Array): Resources { + return new Resources(grid, terrainData); +} diff --git a/src/lib/Puddle/sim/rng.ts b/src/lib/Puddle/sim/rng.ts new file mode 100644 index 0000000..1c3da84 --- /dev/null +++ b/src/lib/Puddle/sim/rng.ts @@ -0,0 +1,31 @@ +export interface Rng { + /** Next float in [0, 1). */ + next: () => number; + /** Float in [min, max). */ + range: (min: number, max: number) => number; +} + +export class SeededRng implements Rng { + private state: number; + + constructor(seed: number) { + // Fold a possibly non-integer/negative seed into a nonzero uint32 state. + const initialState = Math.floor(seed) >>> 0; + this.state = initialState === 0 ? 0x9e3779b9 : initialState; + } + + next(): number { + this.state = (this.state + 0x6d2b79f5) | 0; + const mixed = Math.imul(this.state ^ (this.state >>> 15), 1 | this.state); + const remixed = mixed ^ (mixed + Math.imul(mixed ^ (mixed >>> 7), 61 | mixed)); + return ((remixed ^ (remixed >>> 14)) >>> 0) / 4294967296; + } + + range(min: number, max: number): number { + return min + (max - min) * this.next(); + } +} + +export function createRng(seed: number): Rng { + return new SeededRng(seed); +} diff --git a/src/lib/Puddle/sim/terrain.ts b/src/lib/Puddle/sim/terrain.ts new file mode 100644 index 0000000..d6e6f51 --- /dev/null +++ b/src/lib/Puddle/sim/terrain.ts @@ -0,0 +1,57 @@ +import { domainCoordinate } from './brands'; +import { fbm, sdRoundRect } from './noise'; +import type { Params } from './params'; +import type { Grid } from './types'; + +export interface WorldTerrainOptions { + readonly cellSize: number; + readonly bowlWidth: number; + readonly bowlHeight: number; + readonly bowlRim: number; +} + +function smooth(value: number): number { + return value * value * (3 - 2 * value); +} + +export function makeTerrain( + grid: Grid, + params: Params, + seed: number, + world?: WorldTerrainOptions, +): Float32Array { + const { nx: columnCount, ny: rowCount } = grid; + const terrain = new Float32Array(columnCount * rowCount); + for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { + for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) { + const terrainX = domainCoordinate( + world + ? ((columnIndex + 0.5 - columnCount / 2) * world.cellSize) / Math.max(world.bowlWidth, 1) + : (columnIndex + 0.5) / columnCount - 0.5, + ); + const terrainY = domainCoordinate( + world + ? ((rowIndex + 0.5 - rowCount / 2) * world.cellSize) / Math.max(world.bowlWidth, 1) + : (rowIndex + 0.5) / rowCount - 0.5, + ); + const bowlHalfX = world ? 0.5 : params.bowlHalfX; + const bowlHalfY = world + ? world.bowlHeight / Math.max(world.bowlWidth, 1) / 2 + : params.bowlHalfY; + // Existing normalized radius/rim semantics are anchored to the configured + // bowl width so they stay physical when the viewport grows. + const bowlRadius = params.bowlRadius; + const bowlRim = world ? world.bowlRim : params.bowlRim; + // Signed distance drives the bowl wall; noise roughens the pooled edge. + const signedDistance = sdRoundRect(terrainX, terrainY, bowlHalfX, bowlHalfY, bowlRadius); + const wallHeight = + params.bowlAmp * smooth(Math.max(0, Math.min(1, (signedDistance + 0.02) / bowlRim))); + const noiseHeight = + (fbm(terrainX * params.noiseFreq + 10, terrainY * params.noiseFreq + 10, seed) - 0.5) * + 2 * + params.noiseAmp; + terrain[rowIndex * columnCount + columnIndex] = wallHeight + noiseHeight; + } + } + return terrain; +} diff --git a/src/lib/Puddle/sim/tests/checkerboard.test.ts b/src/lib/Puddle/sim/tests/checkerboard.test.ts new file mode 100644 index 0000000..3b4f750 --- /dev/null +++ b/src/lib/Puddle/sim/tests/checkerboard.test.ts @@ -0,0 +1,188 @@ +import { describe, expect, it } from 'vitest'; + +import { createWaterSim, SIM_DEFAULTS } from '../index'; +import type { FluxField } from '../types'; + +interface InternalSim { + engine: { resources: { currentFlux: () => FluxField } }; +} + +interface CheckerSample { + parityFraction: number; + lagSignature: number; +} + +function percentile(values: number[], fraction: number): number { + const sorted = [...values].sort((a, b) => a - b); + return sorted[Math.min(sorted.length - 1, Math.floor(fraction * sorted.length))] ?? 0; +} + +function isNearShoreline( + depth: ArrayLike, + nx: number, + ny: number, + column: number, + row: number, +): boolean { + let hasWetCell = false; + let hasDryCell = false; + for (let y = Math.max(0, row - 1); y <= Math.min(ny - 1, row + 1); y++) { + for (let x = Math.max(0, column - 1); x <= Math.min(nx - 1, column + 1); x++) { + if ((depth[y * nx + x] ?? 0) > 0.15) hasWetCell = true; + else hasDryCell = true; + } + } + return hasWetCell && hasDryCell; +} + +function checkerSample( + field: Float32Array, + width: number, + height: number, + include: (column: number, row: number) => boolean, +): CheckerSample | undefined { + const residual = new Float64Array(field.length); + const active = new Uint8Array(field.length); + let residualEnergy = 0; + let parityProjection = 0; + let activeCount = 0; + for (let row = 1; row < height - 1; row++) { + for (let column = 1; column < width - 1; column++) { + if (!include(column, row)) continue; + const index = row * width + column; + const neighborMean = + ((field[index - 1] ?? 0) + + (field[index + 1] ?? 0) + + (field[index - width] ?? 0) + + (field[index + width] ?? 0)) / + 4; + const value = (field[index] ?? 0) - neighborMean; + residual[index] = value; + active[index] = 1; + residualEnergy += value * value; + parityProjection += value * ((column + row) % 2 === 0 ? 1 : -1); + activeCount++; + } + } + if (activeCount < 8 || residualEnergy === 0) return undefined; + + const correlation = (lag: number) => { + let product = 0; + let firstEnergy = 0; + let secondEnergy = 0; + for (let row = 1; row < height - 1; row++) { + for (let column = 1; column < width - 1; column++) { + const index = row * width + column; + if (active[index] !== 1) continue; + const value = residual[index]; + if (value === undefined) continue; + for (const next of [index + lag, index + lag * width]) { + if (active[next] !== 1) continue; + const nextValue = residual[next]; + if (nextValue === undefined) continue; + product += value * nextValue; + firstEnergy += value ** 2; + secondEnergy += nextValue ** 2; + } + } + } + return firstEnergy > 0 && secondEnergy > 0 + ? product / Math.sqrt(firstEnergy * secondEnergy) + : 0; + }; + return { + parityFraction: (parityProjection * parityProjection) / (activeCount * residualEnergy), + lagSignature: (correlation(2) - correlation(1)) / 2, + }; +} + +function runStrongTilt(seed: number) { + const nx = 29; + const ny = 16; + const sim = createWaterSim({ + nx, + ny, + seed, + integrator: 'pipes+momentum', + level: 0.58, + momentumSmoothing: 0, + momentumRetention: 0.82, + gravityDrift: false, + }); + sim.settle(60); + sim.setTiltOffset(-8, 0); + const samples: CheckerSample[] = []; + for (let step = 0; step < 120; step++) { + sim.step(0.05); + if (step < 10) continue; + const depth = sim.height; + const flux = (sim as unknown as InternalSim).engine.resources.currentFlux(); + const fx = checkerSample(flux.fx, nx + 1, ny, (column, row) => + [clamp(column - 1, 0, nx - 1), clamp(column, 0, nx - 1)].some((x) => + isNearShoreline(depth, nx, ny, x, row), + ), + ); + const fy = checkerSample(flux.fy, nx, ny + 1, (column, row) => + [clamp(row - 1, 0, ny - 1), clamp(row, 0, ny - 1)].some((y) => + isNearShoreline(depth, nx, ny, column, y), + ), + ); + if (fx) samples.push(fx); + if (fy) samples.push(fy); + } + return samples; +} + +function clamp(value: number, minimum: number, maximum: number): number { + return Math.max(minimum, Math.min(maximum, value)); +} + +describe('momentum checkerboard regression', () => { + it('does not develop a coherent odd/even shoreline mode under strong tilt', () => { + const samples = Array.from({ length: 8 }, (_, index) => runStrongTilt(index + 1)).flat(); + const parity = samples.map(({ parityFraction }) => parityFraction); + const lag = samples.map(({ lagSignature }) => lagSignature); + expect(samples.length).toBeGreaterThan(0); + // A coherent checkerboard approaches 1 for both metrics. + expect(percentile(parity, 0.95)).toBeLessThan(0.03); + expect(percentile(lag, 0.95)).toBeLessThan(0.17); + }); + + it('does not immediately reverse threshold state under default high-motion momentum', () => { + const nx = 63; + const ny = 35; + const threshold = 0.065; + const sim = createWaterSim({ + nx, + ny, + seed: 7, + integrator: SIM_DEFAULTS.integrator, + level: 0.46, + momentumSmoothing: SIM_DEFAULTS.momentumSmoothing, + momentumRetention: SIM_DEFAULTS.momentumRetention, + gravityDrift: false, + }); + sim.settle(90); + const massCap = sim.totalMass(); + sim.setTiltOffset(-2, 0); + + const mask = () => Uint8Array.from(sim.height, (height) => (height > threshold ? 1 : 0)); + let twoFramesAgo = mask(); + let previous = twoFramesAgo; + let oneFrameReversals = 0; + for (let frame = 0; frame < 300; frame++) { + const stats = sim.advance(1 / 60); + sim.clampMass(massCap, stats); + const current = mask(); + for (let cell = 0; cell < current.length; cell++) { + if (current[cell] === twoFramesAgo[cell] && current[cell] !== previous[cell]) { + oneFrameReversals++; + } + } + twoFramesAgo = previous; + previous = current; + } + + expect(oneFrameReversals).toBeLessThanOrEqual(100); + }); +}); diff --git a/src/lib/Puddle/sim/tests/emitters.test.ts b/src/lib/Puddle/sim/tests/emitters.test.ts new file mode 100644 index 0000000..6fbd50c --- /dev/null +++ b/src/lib/Puddle/sim/tests/emitters.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { seconds } from '../brands'; +import { rainEmitter } from '../emitters'; +import { createRng } from '../rng'; + +describe('rainEmitter — framerate-independent scheduling', () => { + it('emits the same drop count over the same sim-time regardless of dt chunking', () => { + const grid = { nx: 10, ny: 10 }; + const emitter = rainEmitter({ intervalSec: 0.5, amount: 1, radius: 1 }); + const countDrops = (dt: number, steps: number) => { + const rng = createRng(1); + let time = 0; + let dropCount = 0; + for (let stepIndex = 0; stepIndex < steps; stepIndex++) { + dropCount += emitter({ grid, time: seconds(time), dt: seconds(dt), rng }).length; + time += dt; + } + return dropCount; + }; + // 5 sim-seconds either way; 0.5 and 0.25 are exact in binary (no fp drift). + expect(countDrops(0.5, 10)).toBe(10); + expect(countDrops(0.25, 20)).toBe(10); + }); +}); diff --git a/src/lib/Puddle/sim/tests/facade.test.ts b/src/lib/Puddle/sim/tests/facade.test.ts new file mode 100644 index 0000000..c324411 --- /dev/null +++ b/src/lib/Puddle/sim/tests/facade.test.ts @@ -0,0 +1,253 @@ +import { describe, expect, expectTypeOf, it } from 'vitest'; + +import { createWaterSim } from '..'; + +function minimumOf(values: ArrayLike): number { + return Array.from(values).reduce((minimum, value) => Math.min(minimum, value), Infinity); +} + +function allFinite(values: ArrayLike): boolean { + return Array.from(values).every(Number.isFinite); +} + +describe('createWaterSim — invariants', () => { + it('conserves mass exactly with no sources (pipes)', () => { + const sim = createWaterSim({ nx: 40, ny: 24, seed: 3, integrator: 'pipes' }); + const massBeforeSettle = sim.totalMass(); + sim.settle(120); + const massAfterSettle = sim.totalMass(); + expect(Math.abs(massAfterSettle - massBeforeSettle) / massBeforeSettle).toBeLessThan(1e-4); + }); + + it('keeps depth non-negative after a large splat', () => { + const sim = createWaterSim({ nx: 40, ny: 24, seed: 5 }); + sim.splat(20, 12, 5, 3); + sim.settle(200); + expect(minimumOf(sim.height)).toBeGreaterThanOrEqual(0); + }); + + it('stays finite and bounded under rain with a mass cap', () => { + const sim = createWaterSim({ nx: 40, ny: 24, seed: 9, rain: true }); + sim.settle(); + const massCap = sim.totalMass(); + for (let frameIndex = 0; frameIndex < 200; frameIndex++) { + const frameStats = sim.advance(1 / 60); + sim.clampMass(massCap, frameStats); + } + expect(sim.totalMass()).toBeLessThanOrEqual(massCap + 1e-3); + expect(allFinite(sim.height)).toBe(true); + }); + + it('reuses advance stats without changing the mass clamp result', () => { + const options = { nx: 24, ny: 16, seed: 9, rain: true } as const; + const fallbackSim = createWaterSim(options); + const reusedStatsSim = createWaterSim(options); + fallbackSim.settle(); + reusedStatsSim.settle(); + const massCap = fallbackSim.totalMass(); + + for (let frameIndex = 0; frameIndex < 50; frameIndex++) { + fallbackSim.advance(1 / 60); + fallbackSim.clampMass(massCap); + const frameStats = reusedStatsSim.advance(1 / 60); + reusedStatsSim.clampMass(massCap, frameStats); + } + + expect(Float32Array.from(reusedStatsSim.height)).toEqual(Float32Array.from(fallbackSim.height)); + }); +}); + +describe('createWaterSim — momentum integrator (pipes+momentum)', () => { + it('conserves mass and stays non-negative & finite', () => { + const sim = createWaterSim({ nx: 40, ny: 24, seed: 7, integrator: 'pipes+momentum' }); + const massBeforeSplat = sim.totalMass(); + sim.splat(15, 10, 2, 3); + const afterSplat = sim.totalMass(); + sim.settle(150); + expect(Math.abs(sim.totalMass() - afterSplat) / afterSplat).toBeLessThan(1e-4); + expect(massBeforeSplat).toBeGreaterThan(0); + expect(minimumOf(sim.height)).toBeGreaterThanOrEqual(0); + expect(allFinite(sim.height)).toBe(true); + }); +}); + +describe('createWaterSim — momentum shoreline stability', () => { + it('does not flicker the thresholded shoreline under strong tilt', () => { + const nx = 29; + const ny = 16; + const threshold = 0.15; + const sim = createWaterSim({ + nx, + ny, + seed: 3, + integrator: 'pipes+momentum', + level: 0.58, + momentumSmoothing: 0.5, + momentumRetention: 0.82, + gravityDrift: false, + }); + sim.settle(60); + const massStart = sim.totalMass(); + const wetMask = () => Uint8Array.from(sim.height, (height) => (height > threshold ? 1 : 0)); + sim.setTiltOffset(-8, 0); + let toggleCounts = new Int32Array(nx * ny); + let previousMask = wetMask(); + for (let stepIndex = 0; stepIndex < 120; stepIndex++) { + sim.step(0.05); + const mask = wetMask(); + toggleCounts = Int32Array.from(toggleCounts, (toggleCount, cellIndex) => + mask[cellIndex] !== previousMask[cellIndex] ? toggleCount + 1 : toggleCount, + ); + previousMask = mask; + } + // A cell that fills or drains once toggles at most twice. + expect(toggleCounts.some((toggleCount) => toggleCount > 2)).toBe(false); + expect( + toggleCounts.reduce((maximum, count) => Math.max(maximum, count), 0), + ).toBeLessThanOrEqual(2); + expect(allFinite(sim.height)).toBe(true); + expect(Math.abs(sim.totalMass() - massStart) / massStart).toBeLessThan(1e-4); + }); +}); + +describe('createWaterSim — determinism', () => { + it('same seed + same calls => identical output (seeded RNG)', () => { + const runSimulation = () => { + const sim = createWaterSim({ nx: 32, ny: 20, seed: 5, rain: true }); + sim.settle(); + const massCap = sim.totalMass(); + for (let frameIndex = 0; frameIndex < 100; frameIndex++) { + sim.advance(1 / 60); + sim.clampMass(massCap); + } + return Float32Array.from(sim.height); + }; + const firstRun = runSimulation(); + const secondRun = runSimulation(); + expect(firstRun.length).toBe(secondRun.length); + expect(firstRun.every((value, index) => value === secondRun[index])).toBe(true); + }); +}); + +describe('createWaterSim — input validation', () => { + it('coerces invalid dimensions to a safe grid without throwing', () => { + const sim = createWaterSim({ nx: 0, ny: -5 }); + expect(sim.nx).toBeGreaterThanOrEqual(4); + expect(sim.ny).toBeGreaterThanOrEqual(4); + expect(sim.height.length).toBe(sim.nx * sim.ny); + }); + + it('clamps non-finite / out-of-range params so the sim stays finite', () => { + const sim = createWaterSim({ nx: 40, ny: 24, damping: NaN, gravity: -10, level: Infinity }); + sim.settle(120); + expect(allFinite(sim.height)).toBe(true); + }); +}); + +describe('createWaterSim — construction tuning options', () => { + it('clamps nx/ny to the maxDim option', () => { + const sim = createWaterSim({ nx: 4000, ny: 4000, maxDim: 16 }); + expect(sim.nx).toBe(16); + expect(sim.ny).toBe(16); + }); + + it('accepts drift tuning and non-finite drift without corrupting the field', () => { + const sim = createWaterSim({ + nx: 32, + ny: 20, + seed: 3, + gravityDrift: true, + driftAmp: NaN, + driftRateHz: 0.2, + }); + sim.settle(120); + expect(allFinite(sim.height)).toBe(true); + }); +}); + +describe('momentum tuning params — threaded to the momentum pass', () => { + it('changes the settled field when momentumRetention differs', () => { + const runSimulation = (momentumRetention: number) => { + const sim = createWaterSim({ + nx: 32, + ny: 20, + seed: 7, + integrator: 'pipes+momentum', + momentumRetention, + }); + sim.splat(16, 10, 2, 3); + sim.settle(12); // short: momentum hasn't fully dissipated yet + return Float32Array.from(sim.height); + }; + const lowRetentionHeight = runSimulation(0.2); + const highRetentionHeight = runSimulation(0.95); + expect(lowRetentionHeight.length).toBe(highRetentionHeight.length); + expect(lowRetentionHeight.some((value, index) => value !== highRetentionHeight[index])).toBe( + true, + ); + expect(minimumOf(highRetentionHeight)).toBeGreaterThanOrEqual(0); + expect(highRetentionHeight.every(Number.isFinite)).toBe(true); + }); +}); + +describe('setTiltOffset — interactive gravity tilt', () => { + const halfMasses = (sim: { nx: number; height: ArrayLike }) => { + const masses = { left: 0, right: 0 }; + for (let cellIndex = 0; cellIndex < sim.height.length; cellIndex++) { + const side = cellIndex % sim.nx < sim.nx / 2 ? 'left' : 'right'; + masses[side] += sim.height[cellIndex] ?? 0; + } + return masses; + }; + + it('shifts mass toward +x for a negative x offset (flux-pass sign convention)', () => { + const sim = createWaterSim({ nx: 40, ny: 24, seed: 3 }); + sim.settle(); + const halfMassesBeforeTilt = halfMasses(sim); + sim.setTiltOffset(-2, 0); + sim.settle(300); + const halfMassesAfterTilt = halfMasses(sim); + expect(halfMassesAfterTilt.right - halfMassesAfterTilt.left).toBeGreaterThan( + halfMassesBeforeTilt.right - halfMassesBeforeTilt.left, + ); + expect(halfMassesAfterTilt.right).toBeGreaterThan(halfMassesAfterTilt.left); + }); + + it('ignores non-finite offsets and conserves mass under tilt', () => { + const sim = createWaterSim({ nx: 32, ny: 20, seed: 3 }); + sim.settle(); + const mass = sim.totalMass(); + sim.setTiltOffset(NaN, 2); + sim.setTiltOffset(3, Infinity); + sim.setTiltOffset(-2, 1); + sim.settle(200); + expect(Math.abs(sim.totalMass() - mass) / mass).toBeLessThan(1e-4); + expect(minimumOf(sim.height)).toBeGreaterThanOrEqual(0); + }); +}); + +describe('facade — non-finite input guards', () => { + it('ignores NaN splat/step/clampMass and rain options without corrupting the field', () => { + const sim = createWaterSim({ nx: 24, ny: 16, seed: 4, rain: { amount: NaN, radius: NaN } }); + sim.settle(40); + sim.splat(NaN, 5, 1, 2); + sim.step(NaN); + sim.clampMass(NaN); + for (let frameIndex = 0; frameIndex < 30; frameIndex++) sim.advance(1 / 60); + expect(allFinite(sim.height)).toBe(true); + }); + + it('clamps a negative clampMass target to zero instead of flipping depths', () => { + const sim = createWaterSim({ nx: 24, ny: 16, seed: 4 }); + sim.settle(40); + sim.clampMass(-5); + expect(minimumOf(sim.height)).toBeGreaterThanOrEqual(0); + expect(allFinite(sim.height)).toBe(true); + }); + + it('exposes height as a read-only array-like view', () => { + const sim = createWaterSim({ nx: 24, ny: 16, seed: 4 }); + expectTypeOf(sim.height).toEqualTypeOf>(); + expect(sim.height.length).toBe(sim.nx * sim.ny); + }); +}); diff --git a/src/lib/Puddle/sim/tests/governor-engine-timing.test.ts b/src/lib/Puddle/sim/tests/governor-engine-timing.test.ts new file mode 100644 index 0000000..ca0214e --- /dev/null +++ b/src/lib/Puddle/sim/tests/governor-engine-timing.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; + +import { createWaterSim } from '..'; +import { seconds, worldUnits } from '../brands'; +import { DEFAULT_MAX_SUBSTEPS, planSubsteps } from '../governor'; +import { integrators } from '../integrators'; +import { resolveParams } from '../params'; +import type { StateStats } from '../types'; + +describe('planSubsteps', () => { + const stats: StateStats = { mass: 100, maxDepth: worldUnits(0.6) }; + const params = resolveParams({}); + + it('scales substeps with the frame budget and never exceeds CFL dt', () => { + const smallFramePlan = planSubsteps( + seconds(params.baseSubstep), + stats, + integrators.pipes, + params, + DEFAULT_MAX_SUBSTEPS, + ); + const largeFramePlan = planSubsteps( + seconds(params.baseSubstep * 5), + stats, + integrators.pipes, + params, + DEFAULT_MAX_SUBSTEPS, + ); + expect(smallFramePlan.substeps).toBe(1); + expect(largeFramePlan.substeps).toBeGreaterThan(smallFramePlan.substeps); + expect(largeFramePlan.dt).toBeLessThanOrEqual(params.baseSubstep + 1e-9); + }); + + it('caps substeps per frame (no spiral of death)', () => { + const cappedPlan = planSubsteps( + seconds(100), + stats, + integrators.pipes, + params, + DEFAULT_MAX_SUBSTEPS, + ); + expect(cappedPlan.substeps).toBeLessThanOrEqual(DEFAULT_MAX_SUBSTEPS); + }); +}); + +describe('advance — framerate independence (governor accumulator)', () => { + it('runs the same total substeps for the same real time regardless of frame size', () => { + const totalSubsteps = (frameDt: number, frames: number) => { + const sim = createWaterSim({ nx: 32, ny: 20, seed: 2 }); + sim.settle(30); + let totalSubstepCount = 0; + for (let frameIndex = 0; frameIndex < frames; frameIndex++) + totalSubstepCount += sim.advance(frameDt).substeps; + return totalSubstepCount; + }; + const substepsAt30Fps = totalSubsteps(1 / 30, 60); + const substepsAt60Fps = totalSubsteps(1 / 60, 120); + const substepsAt120Fps = totalSubsteps(1 / 120, 240); + // The floor accumulator carries the remainder, so total advanced sim-time + // is the same (within one substep of quantization) at every frame rate. + expect(Math.abs(substepsAt60Fps - substepsAt30Fps)).toBeLessThanOrEqual(2); + expect(Math.abs(substepsAt60Fps - substepsAt120Fps)).toBeLessThanOrEqual(2); + expect(substepsAt60Fps).toBeGreaterThanOrEqual(147); + expect(substepsAt60Fps).toBeLessThanOrEqual(151); + }); + + it('caps per-frame substeps at the maxSubsteps override', () => { + const sim = createWaterSim({ nx: 32, ny: 20, seed: 2, maxSubsteps: 1 }); + sim.settle(20); + expect(sim.advance(10).substeps).toBeLessThanOrEqual(1); + expect(Array.from(sim.height).every(Number.isFinite)).toBe(true); + }); +}); diff --git a/src/lib/Puddle/sim/tests/params.test.ts b/src/lib/Puddle/sim/tests/params.test.ts new file mode 100644 index 0000000..34fc5aa --- /dev/null +++ b/src/lib/Puddle/sim/tests/params.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveParams } from '../params'; + +describe('resolveParams', () => { + it('applies defaults, clamps ranges, and rejects non-finite', () => { + const resolved = resolveParams({ damping: 5, gravity: NaN, level: 0.7 }); + expect(resolved.damping).toBeLessThanOrEqual(0.9999); + expect(resolved.gravity).toBe(9.8); + expect(resolved.level).toBeCloseTo(0.7, 6); + }); + + it('exposes momentum tuning as clamped params', () => { + const resolved = resolveParams({ momentumSmoothing: 5, momentumRetention: -1 }); + expect(resolved.momentumSmoothing).toBe(1); + expect(resolved.momentumRetention).toBe(0); + expect(resolveParams({}).momentumRetention).toBeCloseTo(0.82, 6); + }); + + it('exposes timestep/CFL tuning as clamped params with correct defaults', () => { + const defaults = resolveParams({}); + expect(defaults.cflSafety).toBeCloseTo(0.9, 6); + expect(defaults.minWaveDepth).toBeCloseTo(0.001, 6); + expect(defaults.baseSubstep).toBeCloseTo(0.08, 6); + expect(defaults.timeScale).toBe(6); + const clamped = resolveParams({ cflSafety: 9, baseSubstep: 0, timeScale: NaN }); + expect(clamped.cflSafety).toBe(1); + expect(clamped.baseSubstep).toBeGreaterThan(0); + expect(clamped.timeScale).toBe(6); + }); +}); diff --git a/src/lib/Puddle/sim/types.ts b/src/lib/Puddle/sim/types.ts new file mode 100644 index 0000000..a7c47b6 --- /dev/null +++ b/src/lib/Puddle/sim/types.ts @@ -0,0 +1,109 @@ +import type { + Role, + Seconds, + SubstepSeconds, + TexelCoordinate, + TexelDistance, + WorldUnits, +} from './brands'; +import type { Params } from './params'; +import type { Rng } from './rng'; + +export interface Grid { + readonly nx: number; + readonly ny: number; +} + +/** Cell-centered field (height / scale / terrain), length nx*ny row-major. */ +export interface ScalarField { + readonly nx: number; + readonly ny: number; + readonly data: Float32Array; +} + +/** MAC-staggered flux: fx on vertical faces ((nx+1)*ny), fy on horizontal (nx*(ny+1)). */ +export interface FluxField { + readonly nx: number; + readonly ny: number; + readonly fx: Float32Array; + readonly fy: Float32Array; +} + +export type ScalarRole = 'height' | 'scale' | 'terrain'; + +/** Gravity split into normal acceleration (`perp`) and in-plane acceleration (`tiltX`, `tiltY`). */ +export interface GravitySample { + readonly perp: number; + readonly tiltX: number; + readonly tiltY: number; +} + +/** + * A pass kernel's view of the world: current-copy reads by role (plus the + * pre-swap copy via `readPrevScalar`), and the ping-pong target for whatever + * this pass writes. The kernel never sees raw buffers or generations — the + * executor resolves everything. + */ +export interface PassCtx { + readonly grid: Grid; + readonly dt: SubstepSeconds; + readonly params: Params; + readonly gravity: GravitySample; + readonly readScalar: (role: ScalarRole) => ScalarField; + /** The role's value before its last swap (e.g. pre-height-update depth). */ + readonly readPrevScalar: (role: ScalarRole) => ScalarField; + readonly readFlux: () => FluxField; + readonly writeScalar: () => ScalarField; + readonly writeFlux: () => FluxField; +} + +export type PassKind = 'flux_update' | 'outflow_scale' | 'height_update' | 'momentum'; + +export interface PassDesc { + readonly kind: PassKind; + readonly reads: readonly Role[]; + readonly writes: Role; +} + +export interface StateStats { + readonly mass: number; + readonly maxDepth: WorldUnits; +} + +export interface Integrator { + readonly id: 'pipes' | 'pipes+momentum' | (string & {}); + readonly passes: readonly PassDesc[]; + readonly maxStableDt: (stats: StateStats, params: Params) => SubstepSeconds; +} + +export type SourceCommand = + | { + readonly kind: 'splat'; + readonly x: TexelCoordinate; + readonly y: TexelCoordinate; + readonly amount: WorldUnits; + readonly radius: TexelDistance; + } + | { readonly kind: 'rain'; readonly amount: WorldUnits } + | { readonly kind: 'fill'; readonly level: WorldUnits } + | { readonly kind: 'wipe' }; + +export interface EmitCtx { + readonly grid: Grid; + readonly time: Seconds; + readonly dt: Seconds; + readonly rng: Rng; +} +export type SourceEmitter = (context: EmitCtx) => readonly SourceCommand[]; + +/** Serializable so a noise-phase driver can participate in snapshots. */ +export interface GravityDriver { + readonly sample: (time: Seconds) => GravitySample; + readonly serialize: () => unknown; +} + +export interface FrameStats { + readonly mass: number; + readonly maxDepth: WorldUnits; + readonly substeps: number; +} diff --git a/src/lib/Puddle/snapshot/binary.test.ts b/src/lib/Puddle/snapshot/binary.test.ts new file mode 100644 index 0000000..f59e892 --- /dev/null +++ b/src/lib/Puddle/snapshot/binary.test.ts @@ -0,0 +1,100 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { describe, expect, test } from 'vitest'; + +import { + assertDryGuardBand, + decodePuddleSnapshot, + encodePuddleSnapshot, + remapCenteredDepth, + type PuddleSnapshot, +} from './binary'; + +function snapshot(overrides: Partial = {}): PuddleSnapshot { + return { + nx: 4, + ny: 4, + cellSize: 10, + guardBand: 1, + seed: 7, + compatibilityHash: 42, + totalMass: 3, + depth: new Float32Array([0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0]), + ...overrides, + }; +} + +describe('puddle snapshot binary', () => { + test('round-trips versioned metadata and depth', () => { + const source = snapshot(); + const decoded = decodePuddleSnapshot(encodePuddleSnapshot(source)); + + expect(decoded).toMatchObject({ + nx: source.nx, + ny: source.ny, + cellSize: source.cellSize, + guardBand: source.guardBand, + seed: source.seed, + compatibilityHash: source.compatibilityHash, + totalMass: source.totalMass, + }); + expect([...decoded.depth]).toEqual([...source.depth]); + }); + + test('rejects corrupt and wet-guard snapshots', () => { + const corrupt = encodePuddleSnapshot(snapshot()); + new DataView(corrupt).setUint32(0, 0, true); + expect(() => decodePuddleSnapshot(corrupt)).toThrow(/magic/); + + const wet = snapshot(); + wet.depth[0] = 0.1; + expect(() => { + assertDryGuardBand(wet); + }).toThrow(/wet/); + }); + + test('generated default snapshot has a dry guard band', async () => { + const contents = await readFile(path.resolve('static/puddle/default.bin')); + const buffer = contents.buffer.slice( + contents.byteOffset, + contents.byteOffset + contents.byteLength, + ); + expect(() => { + assertDryGuardBand(decodePuddleSnapshot(buffer)); + }).not.toThrow(); + }); +}); + +describe('centered snapshot remapping', () => { + test('pads outside the source world domain with dry cells', () => { + const source = snapshot(); + const target = remapCenteredDepth(source, 6, 6, 10); + + for (let x = 0; x < 6; x++) { + expect(target[x]).toBe(0); + expect(target[5 * 6 + x]).toBe(0); + } + for (let y = 0; y < 6; y++) { + expect(target[y * 6]).toBe(0); + expect(target[y * 6 + 5]).toBe(0); + } + expect(target.reduce((mass, depth) => mass + depth, 0)).toBe(3); + }); + + test('maps odd/even grids by world coordinates instead of integer offsets', () => { + const source = snapshot({ + nx: 3, + ny: 3, + depth: new Float32Array([0, 0, 0, 0, 2, 0, 0, 0, 0]), + }); + const target = remapCenteredDepth(source, 4, 4, 10); + + expect(target[1 * 4 + 1]).toBe(0.5); + expect(target[1 * 4 + 2]).toBe(0.5); + expect(target[2 * 4 + 1]).toBe(0.5); + expect(target[2 * 4 + 2]).toBe(0.5); + expect(target.reduce((mass, depth) => mass + depth, 0)).toBe(2); + expect(target[0]).toBe(0); + }); +}); diff --git a/src/lib/Puddle/snapshot/binary.ts b/src/lib/Puddle/snapshot/binary.ts new file mode 100644 index 0000000..1b11f28 --- /dev/null +++ b/src/lib/Puddle/snapshot/binary.ts @@ -0,0 +1,172 @@ +const MAGIC = 0x4c445550; +const VERSION = 1; +const HEADER_BYTES = 48; + +export interface PuddleSnapshot { + readonly nx: number; + readonly ny: number; + readonly cellSize: number; + readonly guardBand: number; + readonly seed: number; + readonly compatibilityHash: number; + readonly totalMass: number; + readonly depth: Float32Array; +} + +export function encodePuddleSnapshot(snapshot: PuddleSnapshot): ArrayBuffer { + const cellCount = snapshot.nx * snapshot.ny; + if (snapshot.depth.length !== cellCount) throw new Error('Snapshot depth length is invalid'); + const buffer = new ArrayBuffer(HEADER_BYTES + cellCount * Float32Array.BYTES_PER_ELEMENT); + const view = new DataView(buffer); + view.setUint32(0, MAGIC, true); + view.setUint16(4, VERSION, true); + view.setUint16(6, HEADER_BYTES, true); + view.setUint32(8, snapshot.nx, true); + view.setUint32(12, snapshot.ny, true); + view.setFloat32(16, snapshot.cellSize, true); + view.setUint32(20, snapshot.guardBand, true); + view.setInt32(24, snapshot.seed, true); + view.setUint32(28, snapshot.compatibilityHash, true); + view.setFloat64(32, snapshot.totalMass, true); + view.setUint32(40, cellCount, true); + new Float32Array(buffer, HEADER_BYTES, cellCount).set(snapshot.depth); + return buffer; +} + +export function decodePuddleSnapshot(buffer: ArrayBuffer): PuddleSnapshot { + if (buffer.byteLength < HEADER_BYTES) throw new Error('Snapshot is truncated'); + const view = new DataView(buffer); + if (view.getUint32(0, true) !== MAGIC) throw new Error('Snapshot magic is invalid'); + if (view.getUint16(4, true) !== VERSION) throw new Error('Snapshot version is unsupported'); + const headerBytes = view.getUint16(6, true); + const nx = view.getUint32(8, true); + const ny = view.getUint32(12, true); + const cellCount = view.getUint32(40, true); + if ( + headerBytes !== HEADER_BYTES || + nx === 0 || + ny === 0 || + cellCount !== nx * ny || + buffer.byteLength !== headerBytes + cellCount * Float32Array.BYTES_PER_ELEMENT + ) { + throw new Error('Snapshot dimensions are invalid'); + } + const depth = new Float32Array(cellCount); + depth.set(new Float32Array(buffer, headerBytes, cellCount)); + return { + nx, + ny, + cellSize: view.getFloat32(16, true), + guardBand: view.getUint32(20, true), + seed: view.getInt32(24, true), + compatibilityHash: view.getUint32(28, true), + totalMass: view.getFloat64(32, true), + depth, + }; +} + +export function assertDryGuardBand( + snapshot: Pick, + epsilon = 1e-6, +): void { + const { nx, ny, guardBand, depth } = snapshot; + if (guardBand < 1 || guardBand * 2 >= nx || guardBand * 2 >= ny) { + throw new Error('Snapshot guard band is invalid'); + } + for (let y = 0; y < ny; y++) { + for (let x = 0; x < nx; x++) { + if (x >= guardBand && x < nx - guardBand && y >= guardBand && y < ny - guardBand) { + continue; + } + if ((depth[y * nx + x] ?? 0) > epsilon) { + throw new Error(`Snapshot guard band is wet at ${x.toString()},${y.toString()}`); + } + } + } +} + +export function assertDryGuardFlux( + nx: number, + ny: number, + guardBand: number, + flux: { readonly fx: ArrayLike; readonly fy: ArrayLike }, + epsilon = 1e-6, +): void { + for (let y = 0; y < ny; y++) { + for (let x = 0; x <= nx; x++) { + if (x > guardBand && x < nx - guardBand && y >= guardBand && y < ny - guardBand) { + continue; + } + if (Math.abs(flux.fx[y * (nx + 1) + x] ?? 0) > epsilon) { + throw new Error(`Snapshot guard-band x-flux is nonzero at ${x.toString()},${y.toString()}`); + } + } + } + for (let y = 0; y <= ny; y++) { + for (let x = 0; x < nx; x++) { + if (y > guardBand && y < ny - guardBand && x >= guardBand && x < nx - guardBand) { + continue; + } + if (Math.abs(flux.fy[y * nx + x] ?? 0) > epsilon) { + throw new Error(`Snapshot guard-band y-flux is nonzero at ${x.toString()},${y.toString()}`); + } + } + } +} + +export function remapCenteredDepth( + source: Pick, + targetNx: number, + targetNy: number, + targetCellSize: number, +): Float32Array { + if (source.cellSize !== targetCellSize) { + throw new Error('Snapshot cell size is incompatible with the target grid'); + } + const cellSize = source.cellSize; + const cellArea = cellSize * cellSize; + const target = new Float32Array(targetNx * targetNy); + const sourceLeft = (-source.nx * cellSize) / 2; + const sourceTop = (-source.ny * cellSize) / 2; + const targetLeft = (-targetNx * cellSize) / 2; + const targetTop = (-targetNy * cellSize) / 2; + for (let y = 0; y < targetNy; y++) { + const targetCellTop = targetTop + y * cellSize; + const targetCellBottom = targetCellTop + cellSize; + const sourceYStart = Math.max(0, Math.floor((targetCellTop - sourceTop) / cellSize)); + const sourceYEnd = Math.min( + source.ny - 1, + Math.ceil((targetCellBottom - sourceTop) / cellSize) - 1, + ); + for (let x = 0; x < targetNx; x++) { + const targetCellLeft = targetLeft + x * cellSize; + const targetCellRight = targetCellLeft + cellSize; + const sourceXStart = Math.max(0, Math.floor((targetCellLeft - sourceLeft) / cellSize)); + const sourceXEnd = Math.min( + source.nx - 1, + Math.ceil((targetCellRight - sourceLeft) / cellSize) - 1, + ); + let depth = 0; + for (let sourceY = sourceYStart; sourceY <= sourceYEnd; sourceY++) { + const sourceCellTop = sourceTop + sourceY * cellSize; + const overlapY = Math.max( + 0, + Math.min(targetCellBottom, sourceCellTop + cellSize) - + Math.max(targetCellTop, sourceCellTop), + ); + for (let sourceX = sourceXStart; sourceX <= sourceXEnd; sourceX++) { + const sourceCellLeft = sourceLeft + sourceX * cellSize; + const overlapX = Math.max( + 0, + Math.min(targetCellRight, sourceCellLeft + cellSize) - + Math.max(targetCellLeft, sourceCellLeft), + ); + depth += + (source.depth[sourceY * source.nx + sourceX] ?? 0) * ((overlapX * overlapY) / cellArea); + } + } + target[y * targetNx + x] = depth; + } + } + return target; +} diff --git a/src/lib/Puddle/snapshot/compatibility.test.ts b/src/lib/Puddle/snapshot/compatibility.test.ts new file mode 100644 index 0000000..3715127 --- /dev/null +++ b/src/lib/Puddle/snapshot/compatibility.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from 'vitest'; + +import { PUDDLE_DEFAULTS } from '../config'; +import type { PuddleSimulationOptions } from '../runtime/puddleSimulation'; +import { snapshotCompatibilityHash, type SnapshotCompatibilityOptions } from './compatibility'; + +const simulation: PuddleSimulationOptions = { + seed: PUDDLE_DEFAULTS.seed, + level: PUDDLE_DEFAULTS.level, + noiseAmp: PUDDLE_DEFAULTS.noiseAmp, + bowlWidth: PUDDLE_DEFAULTS.bowlWidth, + bowlHeight: PUDDLE_DEFAULTS.bowlHeight, + bowlAmp: PUDDLE_DEFAULTS.bowlAmp, + bowlRim: PUDDLE_DEFAULTS.bowlRim, + integrator: PUDDLE_DEFAULTS.integrator, + momentumSmoothing: PUDDLE_DEFAULTS.momentumSmoothing, + momentumRetention: PUDDLE_DEFAULTS.momentumRetention, + timeScale: PUDDLE_DEFAULTS.timeScale, + baseSubstep: PUDDLE_DEFAULTS.baseSubstep, + cflSafety: PUDDLE_DEFAULTS.cflSafety, + minWaveDepth: PUDDLE_DEFAULTS.minWaveDepth, + maxSubsteps: PUDDLE_DEFAULTS.maxSubsteps, + gravityDrift: PUDDLE_DEFAULTS.gravityDrift, + driftAmp: PUDDLE_DEFAULTS.driftAmp, + driftRateHz: PUDDLE_DEFAULTS.driftRateHz, + rainInterval: PUDDLE_DEFAULTS.rainInterval, + rainAmount: PUDDLE_DEFAULTS.rainAmount, + rainRadius: PUDDLE_DEFAULTS.rainRadius, +}; + +const options: SnapshotCompatibilityOptions = { + cellSize: PUDDLE_DEFAULTS.cellSize, + settleSubsteps: PUDDLE_DEFAULTS.settleSubsteps, + simulation, +}; + +describe('snapshot compatibility hash', () => { + test.each([ + ['cflSafety', 0.7], + ['minWaveDepth', 0.01], + ['gravityDrift', false], + ['driftAmp', 1.5], + ['driftRateHz', 0.25], + ] as const)('changes when %s changes', (key, value) => { + const changed = { + ...options, + simulation: { ...simulation, [key]: value }, + }; + + expect(snapshotCompatibilityHash(changed)).not.toBe(snapshotCompatibilityHash(options)); + }); + + test('changes across terrain, integrator, timestep, and source categories', () => { + const variants: SnapshotCompatibilityOptions[] = [ + { ...options, cellSize: options.cellSize + 1 }, + { ...options, settleSubsteps: options.settleSubsteps + 1 }, + { ...options, simulation: { ...simulation, seed: simulation.seed + 1 } }, + { ...options, simulation: { ...simulation, integrator: 'pipes' } }, + { ...options, simulation: { ...simulation, baseSubstep: simulation.baseSubstep / 2 } }, + { ...options, simulation: { ...simulation, rainAmount: simulation.rainAmount / 2 } }, + ]; + const baseline = snapshotCompatibilityHash(options); + + expect(variants.every((variant) => snapshotCompatibilityHash(variant) !== baseline)).toBe(true); + }); +}); diff --git a/src/lib/Puddle/snapshot/compatibility.ts b/src/lib/Puddle/snapshot/compatibility.ts new file mode 100644 index 0000000..c5c7b49 --- /dev/null +++ b/src/lib/Puddle/snapshot/compatibility.ts @@ -0,0 +1,50 @@ +import type { PuddleSimulationOptions } from '../runtime/puddleSimulation'; + +export const PUDDLE_TERRAIN_VERSION = 1; + +export interface SnapshotCompatibilityOptions { + readonly cellSize: number; + readonly settleSubsteps: number; + readonly simulation: PuddleSimulationOptions; +} + +function fnv1a(value: string): number { + let hash = 0x811c9dc5; + for (let index = 0; index < value.length; index++) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 0x01000193); + } + return hash >>> 0; +} + +export function snapshotCompatibilityHash(options: SnapshotCompatibilityOptions): number { + const simulation = options.simulation; + return fnv1a( + [ + PUDDLE_TERRAIN_VERSION, + options.cellSize, + options.settleSubsteps, + simulation.seed, + simulation.level, + simulation.noiseAmp, + simulation.bowlWidth, + simulation.bowlHeight, + simulation.bowlAmp, + simulation.bowlRim, + simulation.integrator, + simulation.momentumSmoothing, + simulation.momentumRetention, + simulation.timeScale, + simulation.baseSubstep, + simulation.cflSafety, + simulation.minWaveDepth, + simulation.maxSubsteps, + simulation.gravityDrift, + simulation.driftAmp, + simulation.driftRateHz, + simulation.rainInterval, + simulation.rainAmount, + simulation.rainRadius, + ].join('|'), + ); +} diff --git a/src/lib/Puddle/snapshot/types.ts b/src/lib/Puddle/snapshot/types.ts new file mode 100644 index 0000000..ddeecbe --- /dev/null +++ b/src/lib/Puddle/snapshot/types.ts @@ -0,0 +1,12 @@ +export type PuddleSnapshotName = 'default' | 'compact' | 'medium' | 'expanded' | 'large'; + +export interface PuddleSnapshotAsset { + readonly name: PuddleSnapshotName; + readonly url: string; + readonly nx: number; + readonly ny: number; + readonly cellSize: number; + readonly compatibilityHash: number; + readonly path: string; + readonly clip: string; +} diff --git a/src/lib/Puddle/stories/Puddle.stories.svelte b/src/lib/Puddle/stories/Puddle.stories.svelte new file mode 100644 index 0000000..22723c5 --- /dev/null +++ b/src/lib/Puddle/stories/Puddle.stories.svelte @@ -0,0 +1,107 @@ + + + + +{#snippet template({ backgroundColor, puddleTextColor, textColor, ...args }: Args)} +
+ +
+{/snippet} + + { + const path = canvasElement.querySelector('path[data-puddle-shape]'); + await expect(path).not.toBeNull(); + if (!path) return; + const fraction = await waitForPaintedFraction(path, 60); + // A puddle covers part of the box but not all of it. + await expect(fraction).toBeGreaterThan(0.1); + await expect(fraction).toBeLessThan(0.95); + }} +/> + + + + + + + + + + +{#snippet device({ backgroundColor, puddleTextColor, textColor, ...args }: Args)} +
+ + Permission: {devicePermission} + +
+{/snippet} + + + diff --git a/src/lib/Puddle/stories/PuddleDebug.stories.svelte b/src/lib/Puddle/stories/PuddleDebug.stories.svelte new file mode 100644 index 0000000..173514b --- /dev/null +++ b/src/lib/Puddle/stories/PuddleDebug.stories.svelte @@ -0,0 +1,193 @@ + + +{#snippet template(args: Args)} + {@const terrain = renderTerrainDepth(args)} +
+ + {#each terrain.cells as cell (cell.index)} + + {/each} + +
+ Terrain depth +
+
+ high {terrain.maximum.toFixed(2)} + deep {terrain.minimum.toFixed(2)} +
+ {terrain.columns} × {terrain.rows} cells +
+
+{/snippet} + + { + const heatmap = canvasElement.querySelector('[data-terrain-depth]'); + await expect(heatmap).not.toBeNull(); + await expect(heatmap?.querySelectorAll('rect').length).toBeGreaterThan(0); + }} +/> diff --git a/src/lib/Puddle/stories/storyConfig.ts b/src/lib/Puddle/stories/storyConfig.ts new file mode 100644 index 0000000..746d830 --- /dev/null +++ b/src/lib/Puddle/stories/storyConfig.ts @@ -0,0 +1,76 @@ +import type { ComponentProps } from 'svelte'; + +import type { Meta } from '@storybook/sveltekit'; + +import { brandColorValues } from '$lib/tokens'; + +import { PUDDLE_DEFAULTS } from '../config'; +import type Puddle from '../Puddle.svelte'; +import { paramSchema } from '../sim'; + +type Args = ComponentProps; +type PuddleMeta = Meta; + +const argTypes: NonNullable = { + cellSize: { control: { type: 'range', min: 12, max: 60, step: 1 } }, + threshold: { control: { type: 'range', min: 0, max: 0.3, step: 0.005 } }, + level: { control: { type: 'range', min: 0.3, max: 4, step: 0.02 } }, + seed: { control: { type: 'range', min: 1, max: 40, step: 1 } }, + noiseAmp: { control: { type: 'range', min: 0, max: 0.8, step: 0.02 } }, + bowlWidth: { control: { type: 'range', min: 200, max: 1800, step: 10 } }, + bowlHeight: { control: { type: 'range', min: 120, max: 1200, step: 10 } }, + bowlAmp: { + control: { + type: 'range', + min: paramSchema.bowlAmp.min, + max: paramSchema.bowlAmp.max, + step: 0.1, + }, + }, + bowlRim: { + control: { + type: 'range', + min: paramSchema.bowlRim.min, + max: paramSchema.bowlRim.max, + step: 0.01, + }, + }, + animated: { control: 'boolean' }, + followCursor: { control: 'boolean' }, + integrator: { control: 'radio', options: ['pipes', 'pipes+momentum'] }, + momentumSmoothing: { control: { type: 'range', min: 0, max: 1, step: 0.02 } }, + momentumRetention: { control: { type: 'range', min: 0, max: 1, step: 0.02 } }, + timeScale: { control: { type: 'range', min: 0.5, max: 20, step: 0.5 } }, + baseSubstep: { control: { type: 'range', min: 0.02, max: 0.2, step: 0.005 } }, + maxSubsteps: { control: { type: 'range', min: 1, max: 16, step: 1 } }, + cflSafety: { control: { type: 'range', min: 0.3, max: 1, step: 0.02 } }, + minWaveDepth: { control: { type: 'range', min: 0.0001, max: 0.05, step: 0.0005 } }, + settleSubsteps: { control: { type: 'range', min: 0, max: 200, step: 10 } }, + gravityDrift: { control: 'boolean' }, + driftAmp: { control: { type: 'range', min: 0, max: 8, step: 0.05 } }, + driftRateHz: { control: { type: 'range', min: 0, max: 0.5, step: 0.01 } }, + rainInterval: { control: { type: 'range', min: 0.1, max: 3, step: 0.1 } }, + rainAmount: { control: { type: 'range', min: 0, max: 0.3, step: 0.01 } }, + rainRadius: { control: { type: 'range', min: 0.5, max: 5, step: 0.5 } }, + maxCells: { control: { type: 'range', min: 20, max: 200, step: 10 } }, + cursorTilt: { control: { type: 'range', min: 0, max: 8, step: 0.1 } }, + cursorEase: { control: { type: 'range', min: 0.05, max: 1.5, step: 0.05 } }, + deviceGravity: { control: 'boolean' }, + deviceTilt: { control: { type: 'range', min: 0, max: 12, step: 0.5 } }, + deviceEase: { control: { type: 'range', min: 0.05, max: 1.5, step: 0.05 } }, + color: { control: 'color' }, +}; + +export const puddleStoryConfig = { + args: { + ...PUDDLE_DEFAULTS, + } satisfies Args, + argTypes, + parameters: { + backgrounds: { + default: 'lime', + values: [{ name: 'lime', value: brandColorValues.primary['100'] }], + }, + controls: { include: Object.keys(argTypes) }, + }, +} satisfies Pick; diff --git a/src/lib/Puddle/tests/Puddle.clip.svelte.test.ts b/src/lib/Puddle/tests/Puddle.clip.svelte.test.ts new file mode 100644 index 0000000..4c3e17b --- /dev/null +++ b/src/lib/Puddle/tests/Puddle.clip.svelte.test.ts @@ -0,0 +1,79 @@ +import { expect, test } from 'vitest'; +import { page } from 'vitest/browser'; +import { render } from 'vitest-browser-svelte'; + +import PuddleClipFixture from './PuddleClipFixture.svelte'; + +async function screenshotPixels(base64: string) { + const image = new Image(); + image.src = base64.startsWith('data:') ? base64 : `data:image/png;base64,${base64}`; + await image.decode(); + const canvas = document.createElement('canvas'); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + const context = canvas.getContext('2d'); + expect(context).not.toBeNull(); + context?.drawImage(image, 0, 0); + return { + at(x: number, y: number) { + return context?.getImageData( + Math.round(x * (canvas.width - 1)), + Math.round(y * (canvas.height - 1)), + 1, + 1, + ).data; + }, + }; +} + +test('clips inverse content with the same local SVG path and transform as the fill', async () => { + await render(PuddleClipFixture); + const host = document.querySelector('[data-puddle-host]'); + const fillSvg = document.querySelector('svg[data-puddle-renderer]'); + const path = fillSvg?.querySelector('path[data-puddle-shape]'); + const visibleUse = fillSvg?.querySelector('[data-puddle-visible-shape]'); + const clipPath = fillSvg?.querySelector('[data-puddle-clip-shape]'); + const clippedContent = host?.querySelector(':scope > [data-puddle-clipped-content]'); + const baseProbe = host?.querySelector('[data-puddle-base-probe]'); + const probe = clippedContent?.querySelector('[data-puddle-clipped-probe]'); + expect(host).not.toBeNull(); + expect(fillSvg).not.toBeNull(); + expect(path).not.toBeNull(); + expect(visibleUse).not.toBeNull(); + expect(clipPath).not.toBeNull(); + expect(clippedContent).not.toBeNull(); + expect(host?.querySelector('foreignObject')).toBeNull(); + expect(baseProbe).not.toBeNull(); + expect(probe).not.toBeNull(); + if ( + !host || + !fillSvg || + !path || + !visibleUse || + !clipPath || + !clippedContent || + !baseProbe || + !probe + ) + return; + + await expect.poll(() => host.hasAttribute('data-puddle-live'), { timeout: 10_000 }).toBe(true); + expect(visibleUse.style.transform).toContain('translate(50%, 50%)'); + expect(clipPath.getAttribute('d')).toBe(path.getAttribute('d')); + expect(clipPath.parentElement?.getAttribute('clipPathUnits')).toBe('userSpaceOnUse'); + expect(clipPath.getAttribute('transform')).toMatch(/^translate\(/); + expect(clippedContent.style.clipPath).toBe('var(--puddle-clip)'); + expect(host.style.getPropertyValue('--puddle-clip')).toContain('puddle-clip'); + const cellSize = Number(fillSvg.dataset['puddleCellSize']); + expect(path.getAttribute('d')).toMatch(new RegExp(`v${cellSize.toString()}h-`)); + + baseProbe.style.cssText = + 'position:absolute;inset:0;width:100%;height:100%;background:rgb(0,0,255);'; + clippedContent.style.cssText = + 'position:absolute;inset:0;width:100%;height:100%;clip-path:var(--puddle-clip);'; + probe.style.cssText = 'width:100%;height:100%;background:rgb(255,0,0);'; + const screenshot = await page.getByTestId('puddle-clip-fixture').screenshot({ save: false }); + const pixels = await screenshotPixels(screenshot); + expect(Array.from(pixels.at(0.5, 0.5) ?? [])).toEqual([255, 0, 0, 255]); + expect(Array.from(pixels.at(0.01, 0.01) ?? [])).toEqual([0, 0, 255, 255]); +}); diff --git a/src/lib/Puddle/tests/Puddle.interaction.svelte.test.ts b/src/lib/Puddle/tests/Puddle.interaction.svelte.test.ts new file mode 100644 index 0000000..55a8845 --- /dev/null +++ b/src/lib/Puddle/tests/Puddle.interaction.svelte.test.ts @@ -0,0 +1,104 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-svelte'; + +import Puddle from '../Puddle.svelte'; +import { nextFrame, paintedHalves } from './svg'; + +async function waitForPaintedHalves( + path: SVGPathElement, +): Promise<{ left: number; right: number }> { + const host = path.closest('[data-puddle-host]'); + for (let i = 0; i < 120 && !host?.hasAttribute('data-puddle-live'); i++) { + await nextFrame(); + } + let halves = { left: 0, right: 0 }; + for (let i = 0; i < 120; i++) { + halves = paintedHalves(path); + if (halves.left + halves.right > 0) break; + await nextFrame(); + } + return halves; +} + +test('followCursor gently leans the puddle toward the pointer', async () => { + await render(Puddle, { + props: { + animated: true, + bowlWidth: 517, + bowlHeight: 252, + followCursor: true, + style: 'width:760px;height:420px;', + }, + }); + const path = document.querySelector('path[data-puddle-shape]'); + expect(path).not.toBeNull(); + if (!path) return; + + const before = await waitForPaintedHalves(path); + const total = before.left + before.right; + expect(total).toBeGreaterThan(0); + + // Hold the cursor well to the right of the puddle and let the loop run. + const rect = path.ownerSVGElement?.getBoundingClientRect(); + expect(rect).toBeDefined(); + if (!rect) return; + window.dispatchEvent( + new PointerEvent('pointermove', { + clientX: Math.min(window.innerWidth - 2, rect.right + 300), + clientY: rect.top + rect.height / 2, + }), + ); + // Poll for the migration instead of a fixed wait: under full-suite load RAF + // starves and the lean accumulates slower. 2% of the painted area is ~10 grid + // cells of systematic shift — far beyond raindrop jitter (~1 cell of wobble). + const target = before.right - before.left + total * 0.02; + let after = before; + for (let i = 0; i < 30 && after.right - after.left <= target; i++) { + await new Promise((resolve) => void setTimeout(resolve, 500)); + after = paintedHalves(path); + } + expect(after.right - after.left).toBeGreaterThan(target); +}, 30_000); + +test('deviceGravity sloshes the puddle using device rotation', async () => { + await render(Puddle, { + props: { + animated: true, + bowlWidth: 517, + bowlHeight: 252, + deviceGravity: true, + deviceTilt: 8, + deviceEase: 0.05, + style: 'width:760px;height:420px;', + }, + }); + const path = document.querySelector('path[data-puddle-shape]'); + expect(path).not.toBeNull(); + if (!path) return; + + const before = await waitForPaintedHalves(path); + const total = before.left + before.right; + expect(total).toBeGreaterThan(0); + + window.dispatchEvent( + new DeviceMotionEvent('devicemotion', { + rotationRate: { alpha: 0, beta: 0, gamma: 0 }, + interval: 16, + }), + ); + await new Promise((resolve) => void setTimeout(resolve, 50)); + + const target = before.right - before.left + total * 0.02; + let after = before; + for (let i = 0; i < 30 && after.right - after.left <= target; i++) { + window.dispatchEvent( + new DeviceMotionEvent('devicemotion', { + rotationRate: { alpha: 0, beta: 360, gamma: 0 }, + interval: 16, + }), + ); + await new Promise((resolve) => void setTimeout(resolve, 100)); + after = paintedHalves(path); + } + expect(after.right - after.left).toBeGreaterThan(target); +}); diff --git a/src/lib/Puddle/tests/Puddle.rendering.svelte.test.ts b/src/lib/Puddle/tests/Puddle.rendering.svelte.test.ts new file mode 100644 index 0000000..5f9e4ca --- /dev/null +++ b/src/lib/Puddle/tests/Puddle.rendering.svelte.test.ts @@ -0,0 +1,137 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-svelte'; + +import Puddle from '../Puddle.svelte'; +import { waitForPaintedFraction, wetAt } from './svg'; + +test('renders a thresholded puddle silhouette', async () => { + await render(Puddle, { + props: { + animated: false, + bowlWidth: 517, + bowlHeight: 252, + color: '#141414', + style: 'width:760px;height:420px;', + }, + }); + const path = document.querySelector('path[data-puddle-shape]'); + expect(path).not.toBeNull(); + if (!path) return; + + const fraction = await waitForPaintedFraction(path, 120); + + // A puddle covers a chunk of the box but leaves green around it. + expect(fraction).toBeGreaterThan(0.1); + expect(fraction).toBeLessThan(0.9); + // Wet in the middle, dry in the corners => a rounded blob, not a filled rectangle. + expect(wetAt(path, 0.5, 0.5)).toBe(true); + expect(wetAt(path, 0.02, 0.03)).toBe(false); + expect(wetAt(path, 0.98, 0.97)).toBe(false); +}); + +test('accepts a culori Color object for `color`', async () => { + await render(Puddle, { + props: { + animated: false, + color: { mode: 'rgb', r: 1, g: 0, b: 0 }, + style: 'width:760px;height:420px;', + }, + }); + const host = document.querySelector('[data-puddle-host]'); + const path = document.querySelector('path[data-puddle-shape]'); + expect(host).not.toBeNull(); + expect(path).not.toBeNull(); + if (!host || !path) return; + + await waitForPaintedFraction(path, 120); + expect(host.style.getPropertyValue('--puddle-color')).toMatch(/^color\(srgb /); + expect(getComputedStyle(path).fill).not.toBe('rgb(20, 20, 20)'); +}); + +test('uses one canonical CSS color variable for the SVG and fallback', async () => { + await render(Puddle, { + props: { + animated: false, + color: 'color(display-p3 1 0 0)', + style: 'width:760px;height:420px;', + }, + }); + + const host = document.querySelector('[data-puddle-host]'); + expect(host?.style.getPropertyValue('--puddle-color')).toBe('color(display-p3 1 0 0)'); +}); + +test('keeps the apparent cell size stable when the measured grid changes', async () => { + await render(Puddle, { + props: { animated: false, cellSize: 19, style: 'width:380px;height:228px;' }, + }); + const host = document.querySelector('[data-puddle-host]'); + const svg = document.querySelector('svg[data-puddle-renderer]'); + const path = svg?.querySelector('path[data-puddle-shape]'); + expect(host).not.toBeNull(); + expect(svg).not.toBeNull(); + expect(path).not.toBeNull(); + if (!host || !svg || !path) return; + + await expect.poll(() => host.hasAttribute('data-puddle-live'), { timeout: 10_000 }).toBe(true); + const beforeRows = Number(svg.dataset['puddleRows']); + const beforeTransform = svg.querySelector('[data-puddle-visible-shape]')?.style + .transform; + host.style.setProperty('height', '456px'); + + await expect + .poll(() => Number(svg.dataset['puddleRows']), { timeout: 10_000 }) + .toBeGreaterThan(beforeRows); + expect(Number(svg.dataset['puddleCellSize'])).toBe(19); + expect(path.getAttribute('d')).toMatch(/v19h-/); + expect(svg.querySelector('[data-puddle-visible-shape]')?.style.transform).not.toBe( + beforeTransform, + ); +}, 20_000); + +test('clips every responsive snapshot in centered CSS-pixel space', async () => { + await render(Puddle, { + props: { + animated: false, + responsiveSnapshots: true, + style: 'width:400px;height:800px;', + }, + }); + const host = document.querySelector('[data-puddle-host]'); + expect(host).not.toBeNull(); + if (!host) return; + host.style.position = 'fixed'; + host.style.inset = '0 auto auto 0'; + const overlay = document.createElement('div'); + overlay.style.cssText = + 'position:absolute;inset:0;z-index:1000;pointer-events:auto;background:red;'; + host.append(overlay); + const rect = host.getBoundingClientRect(); + + for (const name of ['compact', 'medium', 'expanded', 'large']) { + const svg = host.querySelector(`[data-puddle-static-profile="${name}"]`); + const visibleUse = svg?.querySelector('[data-puddle-static-visible]'); + expect(svg).not.toBeNull(); + expect(visibleUse).not.toBeNull(); + if (!svg || !visibleUse) continue; + + expect(visibleUse.style.transform).toContain('translate(50%, 50%)'); + const clipValue = host.style.getPropertyValue(`--puddle-clip-${name}`); + expect(clipValue).toMatch(/^shape\(/); + expect(CSS.supports('clip-path', clipValue)).toBe(true); + overlay.style.clipPath = clipValue; + expect(document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2)).toBe( + overlay, + ); + expect(document.elementFromPoint(rect.left + 2, rect.top + 2)).not.toBe(overlay); + } +}); + +test('uses unique shape IDs for each component instance', async () => { + await render(Puddle, { props: { animated: false, style: 'width:80px;height:60px;' } }); + await render(Puddle, { props: { animated: false, style: 'width:80px;height:60px;' } }); + + const paths = [...document.querySelectorAll('path[data-puddle-shape]')]; + expect(paths).toHaveLength(2); + expect(new Set(paths.map((path) => path.id)).size).toBe(2); +}); diff --git a/src/lib/Puddle/tests/PuddleClipFixture.svelte b/src/lib/Puddle/tests/PuddleClipFixture.svelte new file mode 100644 index 0000000..405ccb5 --- /dev/null +++ b/src/lib/Puddle/tests/PuddleClipFixture.svelte @@ -0,0 +1,19 @@ + + +{#snippet clippedContent()} +
+{/snippet} + + +
+
diff --git a/src/lib/Puddle/tests/svg.ts b/src/lib/Puddle/tests/svg.ts new file mode 100644 index 0000000..8f73c2f --- /dev/null +++ b/src/lib/Puddle/tests/svg.ts @@ -0,0 +1,79 @@ +export const nextFrame = () => + new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + resolve(); + }); + }); + }); + +interface PuddleRun { + x: number; + y: number; + width: number; +} + +function runsOf(path: SVGPathElement): PuddleRun[] { + const runs: PuddleRun[] = []; + const commands = path.getAttribute('d') ?? ''; + const pattern = /M([\d.]+) ([\d.]+)h([\d.]+)v([\d.]+)h-([\d.]+)z/g; + const cellSize = Number(path.ownerSVGElement?.dataset['puddleCellSize'] ?? 1); + for (const match of commands.matchAll(pattern)) { + const x = Number(match[1]) / cellSize; + const y = Number(match[2]) / cellSize; + const width = Number(match[3]) / cellSize; + if (Number(match[4]) !== cellSize || Number(match[5]) / cellSize !== width) { + throw new Error('invalid puddle run'); + } + runs.push({ x, y, width }); + } + return runs; +} + +function gridSize(path: SVGPathElement): { nx: number; ny: number } { + const renderer = path.ownerSVGElement; + if (!renderer) throw new Error('puddle path is not inside an SVG'); + return { + nx: Number(renderer.dataset['puddleCols']), + ny: Number(renderer.dataset['puddleRows']), + }; +} + +export function wetAt(path: SVGPathElement, fx: number, fy: number): boolean { + const { nx, ny } = gridSize(path); + const x = Math.floor(nx * fx); + const y = Math.floor(ny * fy); + return runsOf(path).some((run) => run.y === y && x >= run.x && x < run.x + run.width); +} + +export function paintedFraction(path: SVGPathElement): number { + const { nx, ny } = gridSize(path); + const cells = runsOf(path).reduce((total, run) => total + run.width, 0); + return cells / (nx * ny); +} + +export function paintedHalves(path: SVGPathElement): { left: number; right: number } { + const { nx } = gridSize(path); + let left = 0; + let right = 0; + for (const run of runsOf(path)) { + for (let x = run.x; x < run.x + run.width; x++) { + if (x < nx / 2) left++; + else right++; + } + } + return { left, right }; +} + +export async function waitForPaintedFraction( + path: SVGPathElement, + maxFrames: number, +): Promise { + let fraction = 0; + for (let i = 0; i < maxFrames; i++) { + fraction = paintedFraction(path); + if (fraction > 0) break; + await nextFrame(); + } + return fraction; +} diff --git a/src/lib/Puddle/worker/core.test.ts b/src/lib/Puddle/worker/core.test.ts new file mode 100644 index 0000000..cd96da1 --- /dev/null +++ b/src/lib/Puddle/worker/core.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from 'vitest'; + +import { PUDDLE_DEFAULTS } from '../config'; +import { encodePuddleSnapshot } from '../snapshot/binary'; +import { snapshotCompatibilityHash } from '../snapshot/compatibility'; +import { initializePuddleWorkerState } from './core'; +import type { PuddleWorkerInitMessage } from './protocol'; + +const simulation = { + seed: PUDDLE_DEFAULTS.seed, + level: PUDDLE_DEFAULTS.level, + noiseAmp: PUDDLE_DEFAULTS.noiseAmp, + bowlWidth: PUDDLE_DEFAULTS.bowlWidth, + bowlHeight: PUDDLE_DEFAULTS.bowlHeight, + bowlAmp: PUDDLE_DEFAULTS.bowlAmp, + bowlRim: PUDDLE_DEFAULTS.bowlRim, + integrator: PUDDLE_DEFAULTS.integrator, + momentumSmoothing: PUDDLE_DEFAULTS.momentumSmoothing, + momentumRetention: PUDDLE_DEFAULTS.momentumRetention, + timeScale: PUDDLE_DEFAULTS.timeScale, + baseSubstep: PUDDLE_DEFAULTS.baseSubstep, + cflSafety: PUDDLE_DEFAULTS.cflSafety, + minWaveDepth: PUDDLE_DEFAULTS.minWaveDepth, + maxSubsteps: PUDDLE_DEFAULTS.maxSubsteps, + gravityDrift: PUDDLE_DEFAULTS.gravityDrift, + driftAmp: PUDDLE_DEFAULTS.driftAmp, + driftRateHz: PUDDLE_DEFAULTS.driftRateHz, + rainInterval: PUDDLE_DEFAULTS.rainInterval, + rainAmount: PUDDLE_DEFAULTS.rainAmount, + rainRadius: PUDDLE_DEFAULTS.rainRadius, +}; + +test('worker core accepts a compatible snapshot and remaps it to the measured grid', async () => { + const compatibilityHash = snapshotCompatibilityHash({ + cellSize: PUDDLE_DEFAULTS.cellSize, + settleSubsteps: PUDDLE_DEFAULTS.settleSubsteps, + simulation, + }); + const buffer = encodePuddleSnapshot({ + nx: 8, + ny: 8, + cellSize: PUDDLE_DEFAULTS.cellSize, + guardBand: 1, + seed: PUDDLE_DEFAULTS.seed, + compatibilityHash, + totalMass: 0, + depth: new Float32Array(64), + }); + const message: PuddleWorkerInitMessage = { + type: 'init', + generation: 1, + width: 280, + height: 196, + geometry: { cellSize: PUDDLE_DEFAULTS.cellSize, maxCells: 200 }, + simulation, + settleSubsteps: PUDDLE_DEFAULTS.settleSubsteps, + threshold: PUDDLE_DEFAULTS.threshold, + animated: false, + snapshotUrl: '/puddle/default.bin', + compatibilityHash, + cursorTilt: PUDDLE_DEFAULTS.cursorTilt, + cursorEase: PUDDLE_DEFAULTS.cursorEase, + deviceEase: PUDDLE_DEFAULTS.deviceEase, + }; + + const state = await initializePuddleWorkerState(message, () => Promise.resolve(buffer)); + + expect(state.snapshotLoaded).toBe(true); + expect(state.nx).toBe(12); + expect(state.ny).toBe(9); + expect(state.cellSize).toBe(PUDDLE_DEFAULTS.cellSize); + expect(state.massCap).toBe(0); + expect(state.path()).toBe(''); + expect(state.path()).toBeNull(); + + state.sim.splat(0, 0, 1, 0.01); + expect(state.path()).toBeNull(); +}); diff --git a/src/lib/Puddle/worker/core.ts b/src/lib/Puddle/worker/core.ts new file mode 100644 index 0000000..b428b37 --- /dev/null +++ b/src/lib/Puddle/worker/core.ts @@ -0,0 +1,71 @@ +import { resolvePuddleGeometry } from '../geometry'; +import { puddlePath } from '../render/puddleRenderer'; +import { createPuddleSimulation } from '../runtime/puddleSimulation'; +import { assertDryGuardBand, decodePuddleSnapshot, remapCenteredDepth } from '../snapshot/binary'; +import type { PuddleWorkerInitMessage } from './protocol'; + +const HIDDEN_RELAXATION_SUBSTEPS = 6; + +function createDepthPath(nx: number, ny: number, cellSize: number, threshold: number) { + const mask = new Uint8Array(nx * ny); + let initialized = false; + let path = ''; + return (height: ArrayLike): string | null => { + let changed = !initialized; + for (let index = 0; index < mask.length; index++) { + const column = index % nx; + const row = Math.floor(index / nx); + const border = column === 0 || column === nx - 1 || row === 0 || row === ny - 1; + const wet = !border && (height[index] ?? 0) > threshold ? 1 : 0; + if (mask[index] !== wet) { + mask[index] = wet; + changed = true; + } + } + initialized = true; + if (!changed) return null; + path = puddlePath(mask, nx, ny, cellSize); + return path; + }; +} + +export async function initializePuddleWorkerState( + message: PuddleWorkerInitMessage, + loadSnapshot: (url: string) => Promise, +) { + const geometry = resolvePuddleGeometry(message.width, message.height, message.geometry); + if (!geometry.ready) throw new Error('Puddle host has no measurable area'); + const sim = createPuddleSimulation(geometry, message.simulation); + let snapshotLoaded = false; + try { + const snapshot = decodePuddleSnapshot(await loadSnapshot(message.snapshotUrl)); + assertDryGuardBand(snapshot); + if ( + snapshot.compatibilityHash !== message.compatibilityHash || + snapshot.seed !== message.simulation.seed + ) { + throw new Error('Puddle snapshot is incompatible with the current options'); + } + sim.loadHeight(remapCenteredDepth(snapshot, geometry.cols, geometry.rows, geometry.cellSize)); + sim.settle(HIDDEN_RELAXATION_SUBSTEPS); + snapshotLoaded = true; + } catch { + sim.settle(message.settleSubsteps); + } + const massCap = sim.totalMass(); + const renderPath = createDepthPath( + geometry.cols, + geometry.rows, + geometry.cellSize, + message.threshold, + ); + return { + sim, + nx: geometry.cols, + ny: geometry.rows, + cellSize: geometry.cellSize, + massCap, + snapshotLoaded, + path: () => renderPath(sim.height), + }; +} diff --git a/src/lib/Puddle/worker/expansionDiagnostic.test.ts b/src/lib/Puddle/worker/expansionDiagnostic.test.ts new file mode 100644 index 0000000..1e0bb2a --- /dev/null +++ b/src/lib/Puddle/worker/expansionDiagnostic.test.ts @@ -0,0 +1,107 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { describe, expect, test } from 'vitest'; + +import { PUDDLE_DEFAULTS } from '../config'; +import { createPuddleSimulation, type PuddleSimulationOptions } from '../runtime/puddleSimulation'; +import { decodePuddleSnapshot, remapCenteredDepth } from '../snapshot/binary'; + +const COLS = 65; +const ROWS = 38; +const FRAME_SECONDS = 1 / 60; + +const simulation: PuddleSimulationOptions = { + seed: PUDDLE_DEFAULTS.seed, + level: PUDDLE_DEFAULTS.level, + noiseAmp: PUDDLE_DEFAULTS.noiseAmp, + bowlWidth: PUDDLE_DEFAULTS.bowlWidth, + bowlHeight: PUDDLE_DEFAULTS.bowlHeight, + bowlAmp: PUDDLE_DEFAULTS.bowlAmp, + bowlRim: PUDDLE_DEFAULTS.bowlRim, + integrator: PUDDLE_DEFAULTS.integrator, + momentumSmoothing: PUDDLE_DEFAULTS.momentumSmoothing, + momentumRetention: PUDDLE_DEFAULTS.momentumRetention, + timeScale: PUDDLE_DEFAULTS.timeScale, + baseSubstep: PUDDLE_DEFAULTS.baseSubstep, + cflSafety: PUDDLE_DEFAULTS.cflSafety, + minWaveDepth: PUDDLE_DEFAULTS.minWaveDepth, + maxSubsteps: PUDDLE_DEFAULTS.maxSubsteps, + gravityDrift: PUDDLE_DEFAULTS.gravityDrift, + driftAmp: PUDDLE_DEFAULTS.driftAmp, + driftRateHz: PUDDLE_DEFAULTS.driftRateHz, + rainInterval: PUDDLE_DEFAULTS.rainInterval, + rainAmount: PUDDLE_DEFAULTS.rainAmount, + rainRadius: PUDDLE_DEFAULTS.rainRadius, +}; + +function wetRowSpan(height: ArrayLike): number { + let first = ROWS; + let last = -1; + for (let y = 0; y < ROWS; y++) { + for (let x = 0; x < COLS; x++) { + if ((height[y * COLS + x] ?? 0) <= PUDDLE_DEFAULTS.threshold) continue; + first = Math.min(first, y); + last = Math.max(last, y); + break; + } + } + return last >= first ? last - first + 1 : 0; +} + +async function loadInitialDepth(): Promise { + const contents = await readFile(path.resolve('static/puddle/large.bin')); + const buffer = contents.buffer.slice( + contents.byteOffset, + contents.byteOffset + contents.byteLength, + ); + return remapCenteredDepth(decodePuddleSnapshot(buffer), COLS, ROWS, PUDDLE_DEFAULTS.cellSize); +} + +function spans( + initialDepth: Float32Array, + overrides: Partial, + tiltY = 0, +): readonly [number, number, number] { + const sim = createPuddleSimulation( + { + width: COLS * PUDDLE_DEFAULTS.cellSize, + height: ROWS * PUDDLE_DEFAULTS.cellSize, + cols: COLS, + rows: ROWS, + cellSize: PUDDLE_DEFAULTS.cellSize, + }, + { ...simulation, ...overrides }, + ); + sim.loadHeight(initialDepth); + sim.settle(6); + sim.setTiltOffset(0, tiltY); + const massCap = sim.totalMass(); + const result: number[] = [wetRowSpan(sim.height)]; + for (let frame = 1; frame <= 60; frame++) { + const stats = sim.advance(FRAME_SECONDS); + sim.clampMass(massCap, stats); + if (frame === 24 || frame === 60) result.push(wetRowSpan(sim.height)); + } + return result as unknown as readonly [number, number, number]; +} + +describe('rapid vertical expansion force diagnostic', () => { + test('isolates rain as the expansion source over the same simulated interval', async () => { + const initialDepth = await loadInitialDepth(); + const disabled = spans(initialDepth, { gravityDrift: false, rainAmount: 0 }); + const driftOnly = spans(initialDepth, { rainAmount: 0 }); + const rainOnly = spans(initialDepth, { gravityDrift: false }); + const defaults = spans(initialDepth, {}); + const tiltOnly = spans( + initialDepth, + { gravityDrift: false, rainAmount: 0 }, + -PUDDLE_DEFAULTS.cursorTilt, + ); + + expect(driftOnly[2] - disabled[2]).toBeLessThanOrEqual(1); + expect(rainOnly[2] - disabled[2]).toBeGreaterThanOrEqual(5); + expect(defaults[2] - disabled[2]).toBeGreaterThanOrEqual(5); + expect(tiltOnly[2] - disabled[2]).toBeLessThanOrEqual(2); + }); +}); diff --git a/src/lib/Puddle/worker/protocol.ts b/src/lib/Puddle/worker/protocol.ts new file mode 100644 index 0000000..72b221a --- /dev/null +++ b/src/lib/Puddle/worker/protocol.ts @@ -0,0 +1,43 @@ +import type { PuddleGeometryOptions } from '../geometry'; +import type { PuddleSimulationOptions } from '../runtime/puddleSimulation'; + +export interface PuddleWorkerInitMessage { + readonly type: 'init'; + readonly generation: number; + readonly width: number; + readonly height: number; + readonly geometry: PuddleGeometryOptions; + readonly simulation: PuddleSimulationOptions; + readonly settleSubsteps: number; + readonly threshold: number; + readonly animated: boolean; + readonly snapshotUrl: string; + readonly compatibilityHash: number; + readonly cursorTilt: number; + readonly cursorEase: number; + readonly deviceEase: number; +} + +export type PuddleWorkerInput = + | PuddleWorkerInitMessage + | { readonly type: 'active'; readonly active: boolean } + | { readonly type: 'pointer'; readonly x: number; readonly y: number } + | { readonly type: 'clear-pointer' } + | { readonly type: 'device-tilt'; readonly x: number; readonly y: number } + | { readonly type: 'frame-consumed' }; + +export type PuddleWorkerOutput = + | { + readonly type: 'ready'; + readonly generation: number; + readonly nx: number; + readonly ny: number; + readonly cellSize: number; + readonly path: string; + } + | { + readonly type: 'path'; + readonly generation: number; + readonly path: string; + } + | { readonly type: 'error'; readonly generation: number; readonly message: string }; diff --git a/src/lib/Puddle/worker/puddle.worker.ts b/src/lib/Puddle/worker/puddle.worker.ts new file mode 100644 index 0000000..0dea485 --- /dev/null +++ b/src/lib/Puddle/worker/puddle.worker.ts @@ -0,0 +1,130 @@ +import { initializePuddleWorkerState } from './core'; +import type { PuddleWorkerInitMessage, PuddleWorkerInput, PuddleWorkerOutput } from './protocol'; + +type WorkerState = Awaited>; + +let generation = 0; +let state: WorkerState | null = null; +let initialized: PuddleWorkerInitMessage | null = null; +let active = false; +let awaitingFrameConsumption = false; +let pointer: { x: number; y: number } | null = null; +let deviceTilt = { x: 0, y: 0 }; +let smoothedPointer = { x: 0, y: 0 }; +let smoothedDevice = { x: 0, y: 0 }; +let lastTime = 0; +let frameHandle = 0; + +function post(message: PuddleWorkerOutput): void { + self.postMessage(message); +} + +function cancelLoop(): void { + if (frameHandle !== 0) cancelAnimationFrame(frameHandle); + frameHandle = 0; + lastTime = 0; +} + +function scheduleLoop(): void { + if (!active || !initialized?.animated || !state || frameHandle !== 0) return; + frameHandle = requestAnimationFrame(loop); +} + +function loop(time: number): void { + frameHandle = 0; + if (!active || !initialized?.animated || !state) return; + const dt = lastTime > 0 ? Math.min(0.1, (time - lastTime) / 1000) : 0; + lastTime = time; + const cursorEase = + Number.isFinite(initialized.cursorEase) && initialized.cursorEase > 0 + ? initialized.cursorEase + : 0.05; + const blend = 1 - Math.exp(-dt / cursorEase); + smoothedPointer.x += ((pointer?.x ?? 0) - smoothedPointer.x) * blend; + smoothedPointer.y += ((pointer?.y ?? 0) - smoothedPointer.y) * blend; + const deviceEase = + Number.isFinite(initialized.deviceEase) && initialized.deviceEase > 0 + ? initialized.deviceEase + : 0.05; + const deviceBlend = 1 - Math.exp(-dt / deviceEase); + smoothedDevice.x += (deviceTilt.x - smoothedDevice.x) * deviceBlend; + smoothedDevice.y += (deviceTilt.y - smoothedDevice.y) * deviceBlend; + state.sim.setTiltOffset( + -initialized.cursorTilt * smoothedPointer.x + smoothedDevice.x, + -initialized.cursorTilt * smoothedPointer.y + smoothedDevice.y, + ); + const stats = state.sim.advance(dt); + state.sim.clampMass(state.massCap, stats); + if (!awaitingFrameConsumption) { + const path = state.path(); + if (path !== null) { + post({ type: 'path', generation, path }); + awaitingFrameConsumption = true; + } + } + scheduleLoop(); +} + +async function initialize(message: PuddleWorkerInitMessage): Promise { + cancelLoop(); + generation = message.generation; + initialized = message; + state = null; + awaitingFrameConsumption = false; + pointer = null; + smoothedPointer = { x: 0, y: 0 }; + smoothedDevice = { x: 0, y: 0 }; + try { + const next = await initializePuddleWorkerState(message, async (url) => { + const response = await fetch(url); + if (!response.ok) throw new Error(`Snapshot request failed (${response.status.toString()})`); + return response.arrayBuffer(); + }); + if (generation !== message.generation) return; + state = next; + const path = next.path(); + if (path === null) throw new Error('Puddle worker produced no initial path'); + post({ + type: 'ready', + generation, + nx: next.nx, + ny: next.ny, + cellSize: next.cellSize, + path, + }); + awaitingFrameConsumption = true; + scheduleLoop(); + } catch (error) { + post({ + type: 'error', + generation, + message: error instanceof Error ? error.message : 'Puddle worker failed', + }); + } +} + +self.onmessage = (event: MessageEvent): void => { + const message = event.data; + switch (message.type) { + case 'init': + void initialize(message); + break; + case 'active': + active = message.active; + if (active) scheduleLoop(); + else cancelLoop(); + break; + case 'pointer': + pointer = { x: message.x, y: message.y }; + break; + case 'clear-pointer': + pointer = null; + break; + case 'device-tilt': + deviceTilt = { x: message.x, y: message.y }; + break; + case 'frame-consumed': + awaitingFrameConsumption = false; + break; + } +}; diff --git a/src/lib/SprayBorder/SprayBorder.stories.svelte b/src/lib/SprayBorder/SprayBorder.stories.svelte new file mode 100644 index 0000000..485acec --- /dev/null +++ b/src/lib/SprayBorder/SprayBorder.stories.svelte @@ -0,0 +1,134 @@ + + + +{#snippet template(args: Args)} +
+ Spray border +
+{/snippet} + +{#snippet comparison(args: Args)} +
+ {#each comparisonSizes as size (size.label)} +
+

{size.label}

+
+ {#each comparisonSeeds as seed (seed)} +
+ FAQ + Seed {seed} +
+ {/each} +
+
+ {/each} +
+{/snippet} + + { + const canvas = canvasElement.querySelector('canvas'); + await expect(canvas).not.toBeNull(); + if (!canvas) return; + for (let i = 0; i < 30; i++) { + if (paintedPixels(canvas) > 0) break; + await nextFrame(); + } + // Where WebGL2 is available the spray must actually blit; otherwise just no-op. + if (hasWebGL2()) await expect(paintedPixels(canvas)).toBeGreaterThan(0); + else await expect(canvas).toBeTruthy(); + }} +/> + + { + const canvases = Array.from(canvasElement.querySelectorAll('canvas')); + const webGL2Available = hasWebGL2(); + await expect(canvases).toHaveLength(comparisonSeeds.length * comparisonSizes.length); + for (const canvas of canvases) { + for (let i = 0; i < 30; i++) { + if (paintedPixels(canvas) > 0) break; + await nextFrame(); + } + if (webGL2Available) await expect(paintedPixels(canvas)).toBeGreaterThan(0); + } + }} +/> diff --git a/src/lib/SprayBorder/SprayBorder.svelte b/src/lib/SprayBorder/SprayBorder.svelte new file mode 100644 index 0000000..4798bdc --- /dev/null +++ b/src/lib/SprayBorder/SprayBorder.svelte @@ -0,0 +1,93 @@ + + + + + {@render children?.()} + diff --git a/src/lib/SprayBorder/spray.ts b/src/lib/SprayBorder/spray.ts new file mode 100644 index 0000000..13da6e8 --- /dev/null +++ b/src/lib/SprayBorder/spray.ts @@ -0,0 +1,21 @@ +import type { Attachment } from 'svelte/attachments'; + +import { parse } from 'culori'; + +import { renderSpray, type RenderParams } from './sprayRenderer'; + +/** + * Drives the shared WebGL spray renderer for the `` it's attached to. + * Re-runs whenever `params` changes (size, dpr, color, or any knob). + */ +export function spray(params: RenderParams): Attachment { + return (canvas) => { + // culori's parse only takes strings; a Color object is already parsed. parse returns undefined for unparseable input. + const color = typeof params.color === 'string' ? parse(params.color) : params.color; + if (!color) { + console.warn(' invalid color', params.color); + return; + } + renderSpray(canvas, { ...params, color }); + }; +} diff --git a/src/lib/SprayBorder/sprayRenderer.ts b/src/lib/SprayBorder/sprayRenderer.ts new file mode 100644 index 0000000..ef4d3a1 --- /dev/null +++ b/src/lib/SprayBorder/sprayRenderer.ts @@ -0,0 +1,241 @@ +// Shared-context WebGL2 renderer for the "spray border" effect (technique 3 of +// the spray-border demo). One offscreen GL context renders every button's +// texture and blits it into each button's own 2D canvas, so a page with many +// buttons stays under the ~16-WebGL-context-per-tab ceiling. + +import { converter, type Color } from 'culori'; +import * as twgl from 'twgl.js'; + +const lrgb = converter('lrgb'); + +// Fullscreen triangle; the fragment shader does all the work in gl_FragCoord space. +const VS = /*glsl*/ `#version 300 es +in vec2 p; +void main(){ gl_Position = vec4(p, 0.0, 1.0); }`; + +// SDF rounded-rect solid core + round-dot stipple spray in the falloff band. +const FS = /*glsl*/ `#version 300 es +precision highp float; +out vec4 fragColor; +uniform vec2 uResolution; // device-px texture size +uniform vec2 uCoreHalfSize; // half-size of the solid core, device px +uniform float uRadius; // corner radius, device px +uniform float uSpread; // band width, device px +uniform float uDensity; // fleck density +uniform float uSeed; +uniform float uDpr; // device pixels per CSS px (keeps dot size DPR-stable) +uniform vec3 uColor; + +// --- Spray tuning (values suffixed _CSS_PX are scaled by uDpr at their use sites) --- +const int DOTS_PER_CELL = 12; // candidate dots evaluated per grid cell +const float CELL_SIZE_CSS_PX = 4.0; // stipple grid cell size +const float DOT_RADIUS_MIN_CSS_PX = 0.5; // smallest dot radius +const float DOT_RADIUS_JITTER_CSS_PX = 0.85; // extra random radius added on top of the minimum +// Dot radius scales across the band: big enough at the rim that full-survival +// packing saturates to solid (n*pi*r^2 >~ 4), tapering to fine far specks. +const float DOT_SCALE_NEAR = 1.55; +const float DOT_SCALE_FAR = 0.5; +const float MIN_EDGE_AA_PX = 0.75; // floor on a dot's anti-aliased rim (~1 device px) +const float CORE_EDGE_INSET = 2.5; // core/band boundary pulled this far inside the SDF (css px in) edge +const float FALLOFF_EXPONENT = 6.0; // how steeply dot-survival odds drop across the band +const float CLUMP_SCALE_CSS_PX = 10.0; // feature size of spray clumps along the edge +const float CORE_ROUGHNESS_AMP_CSS_PX = 0.5; // subtle variation in the solid silhouette +const float SPRAY_CLUMP_AMP_CSS_PX = 1.2; // stronger variation in fleck survival +const float SOLID_RIM_CSS_PX = 1.4; // mean overshoot of the rough solid edge past the box edge +const float EDGE_RECESS_CAP_CSS_PX = 0.3; // clumps bulge freely outward but barely recess inward +const float SOLID_FADE_CSS_PX = 0.75; // thin density floor sealing pinholes right at the rough edge +const float BASE_DENSITY = 7.0; // survival multiplier at uDensity=1 (the designed look) + +// --- Hash salts: arbitrary offsets that give each draw an independent random stream --- +const float DOT_ID_STRIDE = 23.0; // spacing between successive dots' hash identities +const vec2 SEED_SHIFT_SCALE = vec2(37.0, 71.0); // per-axis grid translation per unit uSeed +const float SURVIVAL_DRAW_SALT = 4.7; // decorrelates the dot-survival random draw +const float RADIUS_DRAW_SALT = 9.1; // decorrelates the dot-radius random draw + +float hash(vec2 point){ + point = fract(point * vec2(123.34, 456.21)); + point += dot(point, point + 45.32); + return fract(point.x * point.y); +} +vec2 hash2(vec2 point){ + return vec2(hash(point), hash(point + 71.13)); +} +// Smooth value noise; drives large-scale clumping so the spray reads as blobby +// spatter rather than uniform fuzz. +float vnoise(vec2 point){ + vec2 cell = floor(point), frac = fract(point); + frac = frac * frac * (3.0 - 2.0 * frac); + float a = hash(cell); + float b = hash(cell + vec2(1.0, 0.0)); + float c = hash(cell + vec2(0.0, 1.0)); + float d = hash(cell + vec2(1.0, 1.0)); + return mix(mix(a, b, frac.x), mix(c, d, frac.x), frac.y); +} +// Signed distance to a rounded box centered at the origin (<0 inside). +float sdRoundedBox(vec2 point, vec2 halfSize, float radius){ + vec2 cornerDist = abs(point) - halfSize + radius; + return min(max(cornerDist.x, cornerDist.y), 0.0) + length(max(cornerDist, 0.0)) - radius; +} +void main(){ + vec2 pixel = gl_FragCoord.xy; + vec2 boxCenter = uResolution * 0.5; + vec2 seedShift = uSeed * SEED_SHIFT_SCALE; + float distToCore = sdRoundedBox(pixel - boxCenter, uCoreHalfSize, uRadius); // <0 inside core + + float coreEdge = -CORE_EDGE_INSET * (uResolution.y / (uCoreHalfSize.y * 2.0 + uSpread * 2.0)); + // Keep the solid silhouette restrained while the surrounding flecks retain + // stronger clumping from the same noise field. + float coreEdgeNoise = max( + (vnoise((pixel + seedShift) / (CLUMP_SCALE_CSS_PX * uDpr)) - 0.5) * 2.0 * CORE_ROUGHNESS_AMP_CSS_PX * uDpr + + SOLID_RIM_CSS_PX * uDpr, + -EDGE_RECESS_CAP_CSS_PX * uDpr); + float coreRoughDist = distToCore - coreEdgeNoise; // 0 at the rough solid edge + if(coreRoughDist <= coreEdge){ fragColor = vec4(uColor, 1.0); return; } // rough solid core + if(distToCore > uSpread) discard; // beyond the band + + // Stipple: lay a jittered grid over the band; each cell holds several + // candidate round dots of a fixed small size. Each dot survives with a + // probability that falls off across the band, so the *count* of dots ramps up + // toward the core — packing tight enough to merge into solid — and thins to + // scattered specks at the edge. Scanning the 3x3 neighborhood lets dots spill + // across cell borders, so the underlying grid never reads as a grid. + float cellSize = CELL_SIZE_CSS_PX * uDpr; + vec2 grid = (pixel + seedShift) / cellSize; + vec2 cell = floor(grid); + vec2 frac = fract(grid); + + float cover = 0.0; + for(int j = -1; j <= 1; j++){ + for(int i = -1; i <= 1; i++){ + for(int k = 0; k < DOTS_PER_CELL; k++){ + vec2 neighbor = cell + vec2(float(i), float(j)); + vec2 id = neighbor + float(k) * DOT_ID_STRIDE; // per-dot identity + vec2 jitter = hash2(id); // dot center anywhere in cell + vec2 dotPixel = (neighbor + jitter) * cellSize - seedShift; // dot center, device px + float dotCore = sdRoundedBox(dotPixel - boxCenter, uCoreHalfSize, uRadius); + if(dotCore > uSpread) continue; + + float dotNoiseSample = (vnoise((dotPixel + seedShift) / (CLUMP_SCALE_CSS_PX * uDpr)) - 0.5) * 2.0; + float dotCoreEdgeNoise = max( + dotNoiseSample * CORE_ROUGHNESS_AMP_CSS_PX * uDpr + SOLID_RIM_CSS_PX * uDpr, + -EDGE_RECESS_CAP_CSS_PX * uDpr); + float dotCoreRough = dotCore - dotCoreEdgeNoise; + float maxDotRadius = (DOT_RADIUS_MIN_CSS_PX + DOT_RADIUS_JITTER_CSS_PX) * DOT_SCALE_NEAR * uDpr; + if(dotCoreRough <= coreEdge - maxDotRadius) continue; // fully hidden under the solid + + // Survival follows stronger clumps than the solid edge, keeping the spray + // organic without transferring that waviness to the core silhouette. + float dotSprayEdgeNoise = max( + dotNoiseSample * SPRAY_CLUMP_AMP_CSS_PX * uDpr + SOLID_RIM_CSS_PX * uDpr, + -EDGE_RECESS_CAP_CSS_PX * uDpr); + float dotSprayRough = dotCore - dotSprayEdgeNoise; + float bandPos = clamp((dotSprayRough - coreEdge) / (uSpread - coreEdge), 0.0, 1.0); + float keepProb = pow(1.0 - bandPos, FALLOFF_EXPONENT) * uDensity * BASE_DENSITY; + if(hash(id + SURVIVAL_DRAW_SALT) > keepProb) continue; // this dot didn't survive + + float dotRadius = (DOT_RADIUS_MIN_CSS_PX + DOT_RADIUS_JITTER_CSS_PX * hash(id + RADIUS_DRAW_SALT)) + * mix(DOT_SCALE_NEAR, DOT_SCALE_FAR, bandPos) * uDpr; + + vec2 toDot = (vec2(float(i), float(j)) + jitter - frac) * cellSize; + float dist = length(toDot); + float aa = max(fwidth(dist), MIN_EDGE_AA_PX); // ~1px anti-aliased rim + cover = max(cover, 1.0 - smoothstep(dotRadius - aa, dotRadius + aa, dist)); + } + } + } + + // Density floor: the stipple alone tops out well below full coverage, which + // would let the crisp silhouette of the rough core read through the fringe. + // Ramp from full alpha at the rough edge so the gradient truly starts solid. + cover = max(cover, 1.0 - smoothstep(coreEdge, coreEdge + SOLID_FADE_CSS_PX * uDpr, coreRoughDist)); + + if(cover <= 0.0) discard; + fragColor = vec4(uColor, cover); +}`; + +export interface RenderParams { + /** CSS pixel size of the button's box (not the oversized canvas). */ + cssW: number; + cssH: number; + dpr: number; + /** How far, in CSS px, the spray bleeds past the box. */ + spread: number; + radius: number; + density: number; + seed: number; + color: string | Color; +} + +let gl: WebGL2RenderingContext | null = null; +let glCanvas: OffscreenCanvas | null = null; +let progInfo: twgl.ProgramInfo | null = null; +let fullscreenTri: twgl.BufferInfo | null = null; + +// Lazy so the module is import-safe during SSR (no top-level DOM access). +function ensureGL(): boolean { + if (gl) return true; + glCanvas = new OffscreenCanvas(1, 1); + // preserveDrawingBuffer: drawImage() reads a cleared buffer without it. + gl = glCanvas.getContext('webgl2', { + premultipliedAlpha: false, + preserveDrawingBuffer: true, + antialias: false, + powerPreference: 'low-power', + }); + if (!gl) return false; + gl.drawingBufferColorSpace = 'display-p3'; + progInfo = twgl.createProgramInfo(gl, [VS, FS]); + fullscreenTri = twgl.createBufferInfoFromArrays(gl, { + p: { numComponents: 2, data: [-1, -1, 3, -1, -1, 3] }, + }); + return true; +} + +/** + * Render the spray into `target` (a visible per-button 2D canvas). + * Returns false (leaving `target` untouched) when WebGL2 is missing. + */ +export function renderSpray(target: HTMLCanvasElement, params: RenderParams): boolean { + if (!ensureGL() || !gl || !glCanvas || !progInfo || !fullscreenTri) return false; + + const resolutionWidth = Math.max(1, Math.round((params.cssW + 2 * params.spread) * params.dpr)); + const resolutionHeight = Math.max(1, Math.round((params.cssH + 2 * params.spread) * params.dpr)); + + const rgb = lrgb(params.color); + if (!rgb) throw new Error(`Invalid color: ${JSON.stringify(params.color)}`); + + if (glCanvas.width !== resolutionWidth || glCanvas.height !== resolutionHeight) { + glCanvas.width = resolutionWidth; + glCanvas.height = resolutionHeight; + } + gl.viewport(0, 0, resolutionWidth, resolutionHeight); // must re-set after every resize + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.useProgram(progInfo.program); + twgl.setBuffersAndAttributes(gl, progInfo, fullscreenTri); + twgl.setUniforms(progInfo, { + uResolution: [resolutionWidth, resolutionHeight], + uCoreHalfSize: [(params.cssW / 2) * params.dpr, (params.cssH / 2) * params.dpr], + uRadius: params.radius * params.dpr, + uSpread: params.spread * params.dpr, + uDensity: params.density, + uSeed: params.seed, + uDpr: params.dpr, + uColor: [rgb.r, rgb.g, rgb.b], + }); + twgl.drawBufferInfo(gl, fullscreenTri, gl.TRIANGLES); + + // Blit the shared GL canvas into the button's own 2D canvas (crisp at device px). + if (target.width !== resolutionWidth || target.height !== resolutionHeight) { + target.width = resolutionWidth; + target.height = resolutionHeight; + } + const targetCtx = target.getContext('2d', { + colorSpace: 'display-p3', + }); + if (!targetCtx) return false; + targetCtx.clearRect(0, 0, resolutionWidth, resolutionHeight); + targetCtx.drawImage(glCanvas, 0, 0); + return true; +} diff --git a/src/lib/assets/event-mark.png b/src/lib/assets/event-mark.png new file mode 100644 index 0000000..0210717 Binary files /dev/null and b/src/lib/assets/event-mark.png differ diff --git a/src/lib/assets/favicon-48x48.png b/src/lib/assets/favicon-48x48.png new file mode 100644 index 0000000..1513392 Binary files /dev/null and b/src/lib/assets/favicon-48x48.png differ diff --git a/src/lib/assets/favicon.ico b/src/lib/assets/favicon.ico new file mode 100644 index 0000000..e740884 Binary files /dev/null and b/src/lib/assets/favicon.ico differ diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..3003dd2 --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/lib/assets/fonts/body/Extrabold-800.woff2 b/src/lib/assets/fonts/body/Extrabold-800.woff2 new file mode 100644 index 0000000..2c59940 Binary files /dev/null and b/src/lib/assets/fonts/body/Extrabold-800.woff2 differ diff --git a/src/lib/assets/fonts/body/ExtraboldItalic-800.woff2 b/src/lib/assets/fonts/body/ExtraboldItalic-800.woff2 new file mode 100644 index 0000000..55f8491 Binary files /dev/null and b/src/lib/assets/fonts/body/ExtraboldItalic-800.woff2 differ diff --git a/src/lib/assets/fonts/body/Hairline-100.woff2 b/src/lib/assets/fonts/body/Hairline-100.woff2 new file mode 100644 index 0000000..2e3ad2c Binary files /dev/null and b/src/lib/assets/fonts/body/Hairline-100.woff2 differ diff --git a/src/lib/assets/fonts/body/HairlineItalic-100.woff2 b/src/lib/assets/fonts/body/HairlineItalic-100.woff2 new file mode 100644 index 0000000..12f643c Binary files /dev/null and b/src/lib/assets/fonts/body/HairlineItalic-100.woff2 differ diff --git a/src/lib/assets/fonts/body/Italic-400.woff2 b/src/lib/assets/fonts/body/Italic-400.woff2 new file mode 100644 index 0000000..1588e74 Binary files /dev/null and b/src/lib/assets/fonts/body/Italic-400.woff2 differ diff --git a/src/lib/assets/fonts/body/Light-300.woff2 b/src/lib/assets/fonts/body/Light-300.woff2 new file mode 100644 index 0000000..ca8a3bb Binary files /dev/null and b/src/lib/assets/fonts/body/Light-300.woff2 differ diff --git a/src/lib/assets/fonts/body/LightItalic-300.woff2 b/src/lib/assets/fonts/body/LightItalic-300.woff2 new file mode 100644 index 0000000..6d385f3 Binary files /dev/null and b/src/lib/assets/fonts/body/LightItalic-300.woff2 differ diff --git a/src/lib/assets/fonts/body/Regular-400.woff2 b/src/lib/assets/fonts/body/Regular-400.woff2 new file mode 100644 index 0000000..c53392c Binary files /dev/null and b/src/lib/assets/fonts/body/Regular-400.woff2 differ diff --git a/src/lib/assets/fonts/body/Semibold-600.woff2 b/src/lib/assets/fonts/body/Semibold-600.woff2 new file mode 100644 index 0000000..3ffe945 Binary files /dev/null and b/src/lib/assets/fonts/body/Semibold-600.woff2 differ diff --git a/src/lib/assets/fonts/body/SemiboldItalic-600.woff2 b/src/lib/assets/fonts/body/SemiboldItalic-600.woff2 new file mode 100644 index 0000000..7474a2a Binary files /dev/null and b/src/lib/assets/fonts/body/SemiboldItalic-600.woff2 differ diff --git a/src/lib/assets/og-image.png b/src/lib/assets/og-image.png new file mode 100644 index 0000000..ff71383 Binary files /dev/null and b/src/lib/assets/og-image.png differ diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..fe3cbec --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,33 @@ +// place files you want to import through the `$lib` alias in this folder. +export { default as Button } from './Button/Button.svelte'; +export type { ButtonVariant } from './Button/Button.svelte'; +export { default as ClipAwareButton } from './Button/ClipAwareButton.svelte'; +export type { + ClipAwareButtonAppearance, + ClipAwareButtonControl, + ClipAwareButtonLayer, + ClipAwareButtonLayers, + ClipAwareButtonProps, + ClipAwareButtonSize, + ClipAwareButtonVariant, +} from './Button/ClipAwareButton.svelte'; +export { default as ColorPalette } from './ColorPalette/ColorPalette.svelte'; +export type { ColorPaletteProps } from './ColorPalette/ColorPalette.svelte'; +export { default as ErodedCheckerboard } from './ErodedCheckerboard/ErodedCheckerboard.svelte'; +export type { ErodedCheckerboardProps } from './ErodedCheckerboard/ErodedCheckerboard.svelte'; +export { default as LandingHero } from './LandingHero/LandingHero.svelte'; +export type { LandingHeroProps } from './LandingHero/LandingHero.svelte'; +export { + createColorPalette, + interpolatorNames, + standardColorStops, + type ColorStop, + type InterpolatorName, + type OffsetInterpolation, + type PaletteConfig, + type PaletteSwatch, +} from './ColorPalette/palette'; +export { default as Puddle } from './Puddle/Puddle.svelte'; +export { requestDeviceGravityPermission, type DeviceGravityPermission } from './Puddle/device'; +export { default as SprayBorder } from './SprayBorder/SprayBorder.svelte'; +export { default as Head } from './Head.svelte'; diff --git a/src/lib/theme/presetBrandColors.test.ts b/src/lib/theme/presetBrandColors.test.ts new file mode 100644 index 0000000..3345cae --- /dev/null +++ b/src/lib/theme/presetBrandColors.test.ts @@ -0,0 +1,38 @@ +import { createGenerator, presetWind4 } from 'unocss'; +import { describe, expect, test } from 'vitest'; + +import { brandColors, brandColorValues } from '$lib/tokens'; + +import { createBrandColorCss, presetBrandColors } from './presetBrandColors'; + +describe('presetBrandColors', () => { + test('emits sRGB defaults and guarded canonical overrides', () => { + const css = createBrandColorCss({ primary: { 500: 'oklch(51.2% 0.2529 286.36)' } }); + + expect(css).toMatch(/--brand-primary-500: rgb\(/); + expect(css).toContain('@supports (color: oklch(0% 0 0))'); + expect(css).toContain('--brand-primary-500: oklch(51.2% 0.2529 286.36);'); + }); + + test('registers the references as UnoCSS brand colors', async () => { + const uno = await createGenerator({ + presets: [ + presetWind4(), + presetBrandColors({ values: brandColorValues, references: brandColors }), + ], + }); + const { css } = await uno.generate( + 'bg-brand-primary-500 bg-brand-secondary-500 bg-brand-shade-950', + ); + + expect(css).toContain('--brand-primary-500: rgb('); + expect(css).toContain('--brand-secondary-500: rgb('); + expect(css).toContain('--brand-shade-950: rgb('); + expect(css).toContain('.bg-brand-primary-500'); + expect(css).toContain('var(--brand-primary-500)'); + expect(css).toContain('.bg-brand-secondary-500'); + expect(css).toContain('var(--brand-secondary-500)'); + expect(css).toContain('.bg-brand-shade-950'); + expect(css).toContain('var(--brand-shade-950)'); + }); +}); diff --git a/src/lib/theme/presetBrandColors.ts b/src/lib/theme/presetBrandColors.ts new file mode 100644 index 0000000..22efe13 --- /dev/null +++ b/src/lib/theme/presetBrandColors.ts @@ -0,0 +1,51 @@ +import { formatRgb, toGamut } from 'culori'; +import { definePreset } from 'unocss'; + +type BrandColorPalette = Record>; + +export interface BrandColorPresetOptions { + values: BrandColorPalette; + references: BrandColorPalette; +} + +const toSrgb = toGamut('rgb', 'oklch'); + +function declarations(palette: BrandColorPalette, serialize: (color: string) => string): string { + return Object.entries(palette) + .flatMap(([family, colors]) => + Object.entries(colors).map( + ([stop, color]) => ` --brand-${family}-${stop}: ${serialize(color)};`, + ), + ) + .join('\n'); +} + +export function createBrandColorCss(values: BrandColorPalette): string { + const fallback = declarations(values, (color) => formatRgb(toSrgb(color))); + const modern = declarations(values, (color) => color); + + return `:root, +:host { +${fallback} +} +@supports (color: oklch(0% 0 0)) { + :root, + :host { +${modern} + } +}`; +} + +export const presetBrandColors = definePreset((options) => { + if (!options) throw new Error('presetBrandColors requires brand color values and references'); + + return { + name: 'summer-summit-brand-colors', + theme: { + colors: { + brand: options.references, + }, + }, + preflights: [{ getCSS: () => createBrandColorCss(options.values) }], + }; +}); diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts new file mode 100644 index 0000000..ebc2243 --- /dev/null +++ b/src/lib/tokens.ts @@ -0,0 +1,182 @@ +// Design tokens + +import { parse } from 'culori'; +import { theme } from '@unocss/preset-wind4/theme'; + +import { createColorPalette, type ColorStop } from './ColorPalette/palette'; + +const primaryColor = parse('oklch(0.8925 0.2645 125.76)'); +if (!primaryColor) throw new Error(); + +const secondaryColor = parse('#3600F7'); +if (!secondaryColor) throw new Error(); + +const shadeColor = parse('#141414'); +if (!shadeColor) throw new Error(); + +export const primaryPaletteConfig = { + color: primaryColor, + hue: { + stops: [0, 0.0675, 0.09], + interpolator: 'interpolatorSplineMonotone2', + }, + chroma: { + stops: [0, -0.06, -0.36, -0.6], + interpolator: 'interpolatorSplineMonotone2', + }, +} satisfies Parameters[0]; + +export const secondaryPaletteConfig = { + color: secondaryColor, + hue: { + stops: [0.08, 0.04, 0, -0.08], + interpolator: 'interpolatorSplineMonotone2', + }, + chroma: { + stops: [-0.7, -0.37, -0.18, 0, -0.6, -0.63], + interpolator: 'interpolatorSplineMonotone2', + }, +} satisfies Parameters[0]; + +export const shadePaletteConfig = { + color: shadeColor, + hue: { + stops: [0, 0], + interpolator: 'interpolatorLinear', + }, + chroma: { + stops: [0, 0], + interpolator: 'interpolatorLinear', + }, +} satisfies Parameters[0]; + +const primaryPalette = createColorPalette(primaryPaletteConfig); +const secondaryPalette = createColorPalette(secondaryPaletteConfig); +const shadePalette = createColorPalette(shadePaletteConfig); + +function paletteRecord( + value: typeof primaryPalette, + select: (swatch: (typeof primaryPalette)[number]) => string, +): Record { + return Object.fromEntries(value.map((swatch) => [swatch.stop, select(swatch)])) as Record< + ColorStop, + string + >; +} + +export const brandColorValues = { + primary: paletteRecord(primaryPalette, ({ css }) => css), + secondary: paletteRecord(secondaryPalette, ({ css }) => css), + shade: paletteRecord(shadePalette, ({ css }) => css), +} as const; + +export const brandColors = { + primary: paletteRecord(primaryPalette, ({ stop }) => `var(--brand-primary-${stop})`), + secondary: paletteRecord(secondaryPalette, ({ stop }) => `var(--brand-secondary-${stop})`), + shade: paletteRecord(shadePalette, ({ stop }) => `var(--brand-shade-${stop})`), +} as const; + +export const heroColorValues = { + background: brandColorValues.primary[100], + content: brandColorValues.shade[900], + mainContent: brandColorValues.secondary[600], + outline: brandColorValues.shade[900], + inverseContent: brandColorValues.shade[50], +} as const; + +export const heroColors = { + background: brandColors.primary[100], + content: brandColors.shade[900], + mainContent: brandColors.secondary[600], + outline: brandColors.shade[900], + inverseContent: brandColors.shade[50], +} as const; + +export const buttonColorValues = { + primary: { + surface: brandColorValues.shade[900], + content: brandColorValues.primary[100], + hoverContent: brandColorValues.primary[50], + disabledSurface: brandColorValues.shade[300], + disabledContent: brandColorValues.shade[600], + }, + secondary: { + surface: brandColorValues.secondary[600], + content: brandColorValues.shade[50], + hoverContent: brandColorValues.primary[50], + disabledSurface: brandColorValues.shade[700], + disabledContent: brandColorValues.shade[400], + }, + dark: { + surface: brandColorValues.shade[900], + content: brandColorValues.shade[50], + hoverContent: brandColorValues.primary[50], + disabledSurface: brandColorValues.shade[700], + disabledContent: brandColorValues.shade[400], + }, + light: { + surface: brandColorValues.shade[50], + content: brandColorValues.shade[900], + hoverContent: brandColorValues.secondary[500], + disabledSurface: brandColorValues.shade[300], + disabledContent: brandColorValues.shade[600], + }, + focusRing: brandColorValues.primary[800], +} as const; + +export const buttonColors = { + primary: { + surface: brandColors.shade[900], + content: brandColors.primary[100], + hoverContent: brandColors.primary[50], + disabledSurface: brandColors.shade[300], + disabledContent: brandColors.shade[600], + }, + secondary: { + surface: brandColors.secondary[600], + content: brandColors.shade[50], + hoverContent: brandColors.primary[50], + disabledSurface: brandColors.shade[700], + disabledContent: brandColors.shade[400], + }, + dark: { + surface: brandColors.shade[900], + content: brandColors.shade[50], + hoverContent: brandColors.primary[50], + disabledSurface: brandColors.shade[700], + disabledContent: brandColors.shade[400], + }, + light: { + surface: brandColors.shade[50], + content: brandColors.shade[900], + hoverContent: brandColors.secondary[500], + disabledSurface: brandColors.shade[300], + disabledContent: brandColors.shade[600], + }, + focusRing: brandColors.primary[800], +} as const; + +export const buttonSizes = { + default: { + height: '2.5rem', + inlinePadding: '1.0rem', + sprayRadius: 6, + spraySpread: 6, + }, + large: { + height: '3rem', + inlinePadding: '1.5rem', + sprayRadius: 8, + spraySpread: 8, + }, +} as const; + +export const fonts = { + body: 'PP Neue Montreal', + header: 'Redaction 35', + art: 'Redaction 50', +} as const; + +export type SizeName = string; +export type TextScale = Record; +export const textScale: TextScale = theme.text; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..0f3493a --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,27 @@ + + + + {#each links as link (link.attrs?.['href'])} + + {/each} + + + + + + +{@render children()} diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..df46931 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1 @@ +export const prerender = 'auto'; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..4c0b4dd --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/static/puddle/compact.bin b/static/puddle/compact.bin new file mode 100644 index 0000000..241610d Binary files /dev/null and b/static/puddle/compact.bin differ diff --git a/static/puddle/default.bin b/static/puddle/default.bin new file mode 100644 index 0000000..e656bf5 Binary files /dev/null and b/static/puddle/default.bin differ diff --git a/static/puddle/expanded.bin b/static/puddle/expanded.bin new file mode 100644 index 0000000..7456403 Binary files /dev/null and b/static/puddle/expanded.bin differ diff --git a/static/puddle/large.bin b/static/puddle/large.bin new file mode 100644 index 0000000..e656bf5 Binary files /dev/null and b/static/puddle/large.bin differ diff --git a/static/puddle/medium.bin b/static/puddle/medium.bin new file mode 100644 index 0000000..33cab67 Binary files /dev/null and b/static/puddle/medium.bin differ diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/svelte.config.ts b/svelte.config.ts new file mode 100644 index 0000000..9f383ad --- /dev/null +++ b/svelte.config.ts @@ -0,0 +1,65 @@ +import type { ConfigEnv } from 'vite'; + +import adapter from '@sveltejs/adapter-cloudflare'; +import type { Config, KitConfig } from '@sveltejs/kit'; +import type { TsConfigJson, OmitIndexSignature } from 'type-fest'; + +export type ConfigFn = (env: Partial) => OmitIndexSignature; + +const config: ConfigFn = ({ mode }) => { + const isDev = mode === 'development'; + + const cspDirectives = { + 'default-src': ['self'], + 'connect-src': ['self'], + 'font-src': ['self', 'data:'], + 'img-src': ['self', 'data:'], + 'object-src': ['none'], + 'base-uri': ['none'], + 'frame-ancestors': ['none'], + 'form-action': ['self'], + 'script-src': ['self', 'strict-dynamic'], + 'style-src': ['self', 'unsafe-inline'], + 'worker-src': isDev ? ['self', 'blob:'] : ['self'], + 'upgrade-insecure-requests': true, + } satisfies NonNullable['directives']; + + return { + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => + filename.split(/[/\\]/).includes('node_modules') ? undefined : true, + experimental: { + async: true, + }, + }, + kit: { + adapter: adapter(), + alias: { + $storybook: '.storybook/*', + }, + inlineStyleThreshold: 10000, + typescript: { + config(tsconfig: TsConfigJson) { + // exclude tooling from app `tsconfig.json` + const dropFromApp = [/vite\.config\.(c|m)?[jt]s$/]; + tsconfig.include = (tsconfig.include ?? []).filter( + (p) => !dropFromApp.some((re) => re.test(p)), + ); + + tsconfig.include.push('../.storybook/preview.ts'); + tsconfig.include.push('../.storybook/**/*.svelte'); + + delete tsconfig.compilerOptions?.noEmit; + + return tsconfig; + }, + }, + csp: { + directives: cspDirectives, + }, + }, + }; +}; + +export default config; diff --git a/tools/clean-build.ts b/tools/clean-build.ts new file mode 100644 index 0000000..8a28271 --- /dev/null +++ b/tools/clean-build.ts @@ -0,0 +1,6 @@ +// Used to workaround bug where Cloudflare's workerd holds a lock on the .svelte-kit/cloudflare directory, preventing `vite build` from replacing it. + +import { rm } from 'node:fs/promises'; +import { normalize } from 'node:path'; + +await rm(normalize('./.svelte-kit/cloudflare'), { recursive: true, force: true }); diff --git a/tools/generate-puddle-snapshots.ts b/tools/generate-puddle-snapshots.ts new file mode 100644 index 0000000..d50e87b --- /dev/null +++ b/tools/generate-puddle-snapshots.ts @@ -0,0 +1,171 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { format, resolveConfig } from 'prettier'; +import { createServer } from 'vite'; + +import type { + LANDING_PUDDLE_PROFILES as LandingPuddleProfilesValue, + LandingPuddleProfile, +} from '../src/lib/LandingHero/puddleProfiles'; +import type { PUDDLE_DEFAULTS as PuddleDefaultsValue } from '../src/lib/Puddle/config'; +import type { + puddleClipShape as PuddleClipShapeValue, + puddlePath as PuddlePathValue, +} from '../src/lib/Puddle/render/puddleRenderer'; +import type { createPuddleSimulation as CreatePuddleSimulationValue } from '../src/lib/Puddle/runtime/puddleSimulation'; +import type { + assertDryGuardBand as AssertDryGuardBandValue, + assertDryGuardFlux as AssertDryGuardFluxValue, + encodePuddleSnapshot as EncodePuddleSnapshotValue, +} from '../src/lib/Puddle/snapshot/binary'; +import type { snapshotCompatibilityHash as SnapshotCompatibilityHashValue } from '../src/lib/Puddle/snapshot/compatibility'; +import type { PuddleSnapshotAsset, PuddleSnapshotName } from '../src/lib/Puddle/snapshot/types'; + +interface ConfigModule { + PUDDLE_DEFAULTS: typeof PuddleDefaultsValue; +} +interface ProfileModule { + LANDING_PUDDLE_PROFILES: typeof LandingPuddleProfilesValue; +} +interface SimulationModule { + createPuddleSimulation: typeof CreatePuddleSimulationValue; +} +interface RendererModule { + puddleClipShape: typeof PuddleClipShapeValue; + puddlePath: typeof PuddlePathValue; +} +interface CompatibilityModule { + snapshotCompatibilityHash: typeof SnapshotCompatibilityHashValue; +} +interface BinaryModule { + assertDryGuardBand: typeof AssertDryGuardBandValue; + assertDryGuardFlux: typeof AssertDryGuardFluxValue; + encodePuddleSnapshot: typeof EncodePuddleSnapshotValue; +} + +const root = process.cwd(); +const server = await createServer({ + appType: 'custom', + configFile: false, + root, + resolve: { alias: { $lib: path.join(root, 'src/lib') } }, + server: { middlewareMode: true }, +}); + +try { + const { PUDDLE_DEFAULTS } = (await server.ssrLoadModule( + '/src/lib/Puddle/config.ts', + )) as ConfigModule; + const { LANDING_PUDDLE_PROFILES } = (await server.ssrLoadModule( + '/src/lib/LandingHero/puddleProfiles.ts', + )) as ProfileModule; + const { createPuddleSimulation } = (await server.ssrLoadModule( + '/src/lib/Puddle/runtime/puddleSimulation.ts', + )) as SimulationModule; + const { puddleClipShape, puddlePath } = (await server.ssrLoadModule( + '/src/lib/Puddle/render/puddleRenderer.ts', + )) as RendererModule; + const { snapshotCompatibilityHash } = (await server.ssrLoadModule( + '/src/lib/Puddle/snapshot/compatibility.ts', + )) as CompatibilityModule; + const { assertDryGuardBand, assertDryGuardFlux, encodePuddleSnapshot } = + (await server.ssrLoadModule('/src/lib/Puddle/snapshot/binary.ts')) as BinaryModule; + + const profiles: Record = { + default: { + bowlWidth: PUDDLE_DEFAULTS.bowlWidth, + bowlHeight: PUDDLE_DEFAULTS.bowlHeight, + level: PUDDLE_DEFAULTS.level, + threshold: PUDDLE_DEFAULTS.threshold, + cellSize: PUDDLE_DEFAULTS.cellSize, + }, + ...LANDING_PUDDLE_PROFILES, + }; + const guardBand = 6; + const records = {} as Record; + await mkdir(path.join(root, 'static/puddle'), { recursive: true }); + await mkdir(path.join(root, 'src/lib/Puddle/generated'), { recursive: true }); + + for (const [name, profile] of Object.entries(profiles) as [ + PuddleSnapshotName, + LandingPuddleProfile, + ][]) { + const simulation = { + seed: PUDDLE_DEFAULTS.seed, + level: profile.level, + noiseAmp: PUDDLE_DEFAULTS.noiseAmp, + bowlWidth: profile.bowlWidth, + bowlHeight: profile.bowlHeight, + bowlAmp: PUDDLE_DEFAULTS.bowlAmp, + bowlRim: PUDDLE_DEFAULTS.bowlRim, + integrator: PUDDLE_DEFAULTS.integrator, + momentumSmoothing: PUDDLE_DEFAULTS.momentumSmoothing, + momentumRetention: PUDDLE_DEFAULTS.momentumRetention, + timeScale: PUDDLE_DEFAULTS.timeScale, + baseSubstep: PUDDLE_DEFAULTS.baseSubstep, + cflSafety: PUDDLE_DEFAULTS.cflSafety, + minWaveDepth: PUDDLE_DEFAULTS.minWaveDepth, + maxSubsteps: PUDDLE_DEFAULTS.maxSubsteps, + gravityDrift: PUDDLE_DEFAULTS.gravityDrift, + driftAmp: PUDDLE_DEFAULTS.driftAmp, + driftRateHz: PUDDLE_DEFAULTS.driftRateHz, + rainInterval: PUDDLE_DEFAULTS.rainInterval, + rainAmount: PUDDLE_DEFAULTS.rainAmount, + rainRadius: PUDDLE_DEFAULTS.rainRadius, + }; + const rimCells = Math.ceil((PUDDLE_DEFAULTS.bowlRim * profile.bowlWidth) / profile.cellSize); + const cols = Math.ceil(profile.bowlWidth / profile.cellSize) + 2 * (guardBand + rimCells + 2); + const rows = Math.ceil(profile.bowlHeight / profile.cellSize) + 2 * (guardBand + rimCells + 2); + const geometry = { + width: cols * profile.cellSize, + height: rows * profile.cellSize, + cols, + rows, + cellSize: profile.cellSize, + }; + const sim = createPuddleSimulation(geometry, simulation); + sim.settle(PUDDLE_DEFAULTS.settleSubsteps); + const depth = Float32Array.from(sim.height); + const compatibilityHash = snapshotCompatibilityHash({ + cellSize: profile.cellSize, + settleSubsteps: PUDDLE_DEFAULTS.settleSubsteps, + simulation, + }); + const snapshot = { + nx: cols, + ny: rows, + cellSize: profile.cellSize, + guardBand, + seed: simulation.seed, + compatibilityHash, + totalMass: sim.totalMass(), + depth, + }; + assertDryGuardBand(snapshot); + assertDryGuardFlux(cols, rows, guardBand, sim.flux); + const mask = Uint8Array.from(depth, (value) => (value > profile.threshold ? 1 : 0)); + const fileName = `${name}.bin`; + await writeFile( + path.join(root, 'static/puddle', fileName), + new Uint8Array(encodePuddleSnapshot(snapshot)), + ); + records[name] = { + name, + url: `/puddle/${fileName}`, + nx: cols, + ny: rows, + cellSize: profile.cellSize, + compatibilityHash, + path: puddlePath(mask, cols, rows, profile.cellSize), + clip: puddleClipShape(mask, cols, rows, profile.cellSize), + }; + } + + const source = `// Generated by tools/generate-puddle-snapshots.ts. Do not edit.\nimport type { PuddleSnapshotAsset, PuddleSnapshotName } from '../snapshot/types';\n\nexport const PUDDLE_SNAPSHOTS = ${JSON.stringify(records, null, '\t')} as const satisfies Record;\n`; + const outputPath = path.join(root, 'src/lib/Puddle/generated/puddleSnapshots.ts'); + const prettierConfig = (await resolveConfig(outputPath)) ?? {}; + await writeFile(outputPath, await format(source, { ...prettierConfig, filepath: outputPath })); +} finally { + await server.close(); +} diff --git a/tools/lint-commit-msg.ts b/tools/lint-commit-msg.ts new file mode 100644 index 0000000..ddf7240 --- /dev/null +++ b/tools/lint-commit-msg.ts @@ -0,0 +1,116 @@ +// Lint all commit messages starting from the first commit that diverges from its base branch. + +import { execFileSync, spawnSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import cliPkg from '@commitlint/cli/package.json' with { type: 'json' }; + +const git = (args: string[]) => execFileSync('git', args, { encoding: 'utf8' }).trim(); + +const refExists = (ref: string) => { + try { + git(['rev-parse', '--verify', '--quiet', ref]); + return true; + } catch { + return false; + } +}; + +const remoteExists = (remote: string) => git(['remote']).split('\n').includes(remote); + +function resolveRemoteDefault(remote: string): string { + const output = git(['ls-remote', '--symref', remote, 'HEAD']); + const branchMatch = /^ref: refs\/heads\/(.+)\tHEAD$/m.exec(output); + const oidMatch = /^([0-9a-f]+)\tHEAD$/m.exec(output); + if (branchMatch?.[1] === undefined || oidMatch?.[1] === undefined) + throw new Error(`could not determine the default branch for remote '${remote}'`); + + const ref = `refs/remotes/${remote}/${branchMatch[1]}`; + if (!refExists(ref)) + throw new Error( + `remote default branch '${remote}/${branchMatch[1]}' is not fetched; run 'git fetch ${remote}'`, + ); + if (git(['rev-parse', ref]) !== oidMatch[1]) + throw new Error( + `remote default branch '${remote}/${branchMatch[1]}' is stale; run 'git fetch ${remote}'`, + ); + return ref; +} + +function mergeBase(base: string): string { + try { + return git(['merge-base', base, 'HEAD']); + } catch (error) { + throw new Error(`could not find a common ancestor between '${base}' and HEAD`, { + cause: error, + }); + } +} + +interface CommitRange { + from: string; + includeFrom: boolean; +} + +function resolveRange(): CommitRange { + const fromEnv = process.env['COMMITLINT_FROM']; + if (fromEnv !== undefined && fromEnv !== '') return { from: fromEnv, includeFrom: false }; + + const baseEnv = process.env['COMMITLINT_BASE']; + if (baseEnv !== undefined && baseEnv !== '') { + if (!refExists(baseEnv)) throw new Error(`base ref '${baseEnv}' does not exist`); + return { from: mergeBase(baseEnv), includeFrom: false }; + } + + if (remoteExists('origin')) + return { from: mergeBase(resolveRemoteDefault('origin')), includeFrom: false }; + + const localBase = ['main', 'master'].find(refExists); + if (localBase !== undefined) return { from: mergeBase(localBase), includeFrom: false }; + + const [root] = git(['rev-list', '--max-parents=0', 'HEAD']).split('\n'); + if (root === undefined || root === '') throw new Error('no root commit found'); + return { from: root, includeFrom: true }; +} + +const cliPkgPath = fileURLToPath(import.meta.resolve('@commitlint/cli/package.json')); +const bin = cliPkg.bin as string | Record; +const binRel = typeof bin === 'string' ? bin : bin['commitlint']; +if (binRel === undefined || binRel === '') + throw new Error('commitlint bin not found in its package.json'); +const cliPath = join(dirname(cliPkgPath), binRel); + +const slot = Number(process.env['GIT_CONFIG_COUNT'] ?? 0); +const slotKey = String(slot); +const env = { + ...process.env, + GIT_CONFIG_COUNT: String(slot + 1), + [`GIT_CONFIG_KEY_${slotKey}`]: 'log.showSignature', + [`GIT_CONFIG_VALUE_${slotKey}`]: 'false', +}; + +function lint(args: string[], input?: string): number { + const { error, signal, status } = spawnSync(process.execPath, [cliPath, ...args], { + stdio: input === undefined ? 'inherit' : ['pipe', 'inherit', 'inherit'], + env, + input, + }); + if (error !== undefined) throw error; + if (signal !== null) throw new Error(`commitlint terminated by signal ${signal}`); + if (status === null) throw new Error('commitlint exited without a status'); + return status; +} + +const range = resolveRange(); +const extraArgs = process.argv.slice(2); +let status = 0; + +if (range.includeFrom) { + const message = git(['show', '--no-show-signature', '-s', '--format=%B', range.from]); + status = lint(extraArgs, `${message}\n`); +} + +const rangeStatus = lint(['--from', range.from, ...extraArgs]); +if (status === 0) status = rangeStatus; +process.exit(status); diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..94b12fc --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,8 @@ +{ + "extends": ["./tsconfig.base.json", "./.svelte-kit/tsconfig.json"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "typeRoots": ["./@types", "./node_modules/@types", "./node_modules"], + "types": ["node", "unplugin-fonts/client", "vitest/importMeta", "@sveltejs/enhanced-img"] + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..1edb38c --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext"], + "moduleResolution": "Bundler", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "moduleDetection": "force", + "erasableSyntaxOnly": true, + + "skipLibCheck": true, + "noErrorTruncation": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noUncheckedSideEffectImports": true, + + // Handled by @typescript-eslint + "noUnusedLocals": false, + "noUnusedParameters": false, + + "rewriteRelativeImportExtensions": true, + "sourceMap": true, + "declarationMap": true, + "composite": true, + "noEmit": false, + + "outDir": "./node_modules/.cache/tsc/", + + "types": [] + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..736284c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.tools.json" }] +} diff --git a/tsconfig.tools.json b/tsconfig.tools.json new file mode 100644 index 0000000..609c8a7 --- /dev/null +++ b/tsconfig.tools.json @@ -0,0 +1,11 @@ +{ + "extends": ["./.svelte-kit/tsconfig.json", "./tsconfig.base.json"], + "compilerOptions": { + "checkJs": true, + "types": ["node"] + }, + + "include": ["./*", "./.storybook/**/*", "./tools/**/*", "@types/**/*"], + "exclude": ["./.storybook/preview.ts", "./.storybook/**/*.svelte"], + "references": [{ "path": "./tsconfig.app.json" }] +} diff --git a/uno.config.ts b/uno.config.ts new file mode 100644 index 0000000..50b84b8 --- /dev/null +++ b/uno.config.ts @@ -0,0 +1,37 @@ +import { + defineConfig, + presetIcons, + presetTypography, + presetWebFonts, + transformerVariantGroup, + transformerDirectives, + transformerCompileClass, + presetWind4, +} from 'unocss'; +import extractorSvelte from '@unocss/extractor-svelte'; + +import { presetBrandColors } from './src/lib/theme/presetBrandColors'; +import { brandColors, brandColorValues, fonts } from './src/lib/tokens'; + +export default defineConfig({ + presets: [ + presetWind4({ + preflights: { + reset: true, + }, + }), + presetBrandColors({ values: brandColorValues, references: brandColors }), + presetIcons({ + warn: true, + }), + presetTypography(), + presetWebFonts({ + provider: 'none', // handled by unplugin-font with vite + fonts: Object.fromEntries( + Object.entries(fonts).map(([className, family]) => [className, `'${family}'`]), + ), + }), + ], + transformers: [transformerVariantGroup(), transformerDirectives(), transformerCompileClass()], + extractors: [extractorSvelte()], +}); diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..c63c747 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,171 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { enhancedImages } from '@sveltejs/enhanced-img'; +import { sveltekit } from '@sveltejs/kit/vite'; +import UnoCSS from 'unocss/vite'; +import Unfonts from 'unplugin-fonts/vite'; +import devtoolsJson from 'vite-plugin-devtools-json'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +import svkitCfgFn from './svelte.config.ts'; + +const dirname = + typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + +// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon +export default defineConfig((env) => { + const svkitCfg = svkitCfgFn(env); + + return { + plugins: [ + devtoolsJson(), + UnoCSS( + env.command === 'serve' + ? { + // UnoCSS doesn't pick up new classes automatically on HMR for some reason + content: { + filesystem: ['src/**/*.{svelte,mdx}'], + }, + } + : undefined, + ), + enhancedImages(), + sveltekit({ + ...Object.fromEntries(Object.entries(svkitCfg).filter(([key]) => key !== 'kit')), + ...svkitCfg.kit, + }), + Unfonts({ + inlineFontFace: true, + custom: { + preload: false, + display: 'swap', + families: [ + { + name: 'PP Neue Montreal', + local: 'PP Neue Montreal', + src: './src/lib/assets/fonts/body/*.woff2', + fallback: { + category: 'sans-serif', + }, + transform(font) { + const match = /^([A-Za-z]+)-(\d+)$/.exec(font.basename); + if (!match) return null; + + const [, subfamily, weight] = match; + if (subfamily === undefined || weight === undefined) return null; + + return { + ...font, + weight: Number(weight), + style: subfamily.endsWith('Italic') ? 'italic' : 'normal', + }; + }, + }, + ], + }, + fontsource: { + families: [ + { + name: 'Redaction 35', + weights: [400, 700], + styles: ['normal'], + }, + { + name: 'Redaction 50', + weights: [400, 700], + styles: ['normal'], + }, + ], + }, + }), + ], + define: { + // Allow vite to strip out tests in production + 'import.meta.vitest': 'undefined', + }, + test: { + includeSource: ['src/**/*.{js,ts}'], + expect: { + requireAssertions: true, + }, + projects: [ + { + extends: './vite.config.ts', + test: { + name: 'client', + browser: { + enabled: true, + provider: playwright(), + instances: [ + { + browser: 'chromium', + headless: true, + }, + ], + }, + include: ['src/**/*.svelte.{test,spec}.{js,ts}'], + exclude: ['src/lib/server/**'], + }, + }, + // Playwright's Windows WebKit port lacks OffscreenCanvas, so scope WebKit coverage to DOM/SVG clipping. + { + extends: './vite.config.ts', + test: { + name: 'webkit-puddle-clip', + sequence: { groupOrder: 1 }, + browser: { + enabled: true, + provider: playwright(), + instances: [ + { + browser: 'webkit', + headless: true, + }, + ], + }, + include: ['src/lib/Puddle/tests/Puddle.clip.svelte.test.ts'], + }, + }, + { + extends: './vite.config.ts', + test: { + name: 'server', + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'], + exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'], + }, + }, + { + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + storybookTest({ + configDir: path.join(dirname, '.storybook'), + }), + ], + test: { + name: 'storybook', + browser: { + enabled: true, + headless: true, + provider: playwright(), + // Vitest's default 63315 sits in Windows' ephemeral range, which + // Hyper-V/WinNat reserves in shifting blocks -> intermittent EACCES. + // Pin a fixed port below 49152 on IPv4. (vitest #9035) + api: { host: '127.0.0.1', port: 7357 }, + instances: [ + { + browser: 'chromium', + }, + ], + }, + }, + }, + ], + }, + }; +}); diff --git a/vitest.shims.d.ts b/vitest.shims.d.ts new file mode 100644 index 0000000..03b1801 --- /dev/null +++ b/vitest.shims.d.ts @@ -0,0 +1 @@ +/// diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..c958998 --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "summer-summit", + "compatibility_date": "2026-06-16", + "compatibility_flags": ["nodejs_compat", "nodejs_als"], + "main": ".svelte-kit/cloudflare/_worker.js", + "assets": { + "binding": "ASSETS", + "directory": ".svelte-kit/cloudflare", + }, + "workers_dev": true, + "preview_urls": true, + "observability": { + "enabled": true, + }, + "routes": [ + { + "pattern": "summersummit.dev", + "custom_domain": true, + }, + ], + "build": { + "command": "pnpm run build", + }, +}