diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28922d3..45b6ace 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,90 @@ name: ci -# `push` limite a main, sinon chaque branche de PR declenche DEUX fois le meme job — une -# fois pour le push, une fois pour la PR — sur exactement le meme commit. + +# `push` reste limite a main pour ne pas executer deux fois la verification +# d'une branche qui a deja une pull request ouverte. on: push: branches: [main] pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: verifie: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: { node-version: 24, cache: pnpm } - - run: pnpm install --frozen-lockfile - - run: pnpm build - - run: pnpm test + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + with: + # Bun 1.4.0 est encore publie sur le canal canary. + bun-version: canary + - run: bun install --frozen-lockfile + - run: bun run test + - run: bun run build + - run: bun run pack:check + - uses: actions/upload-artifact@v4 + with: + name: app-dist + path: dist/app + if-no-files-found: error + retention-days: 1 + + apercu: + if: >- + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + needs: verifie + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + pull-requests: write + steps: + - uses: actions/download-artifact@v4 + with: + name: app-dist + path: dist/app + - name: Deploie l'apercu Cloudflare Pages + id: deploy + uses: cloudflare/wrangler-action@v4 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy dist/app --project-name=bloub-app --branch=pr-${{ github.event.pull_request.number }} + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + - name: Publie le lien dans la pull request + env: + GH_TOKEN: ${{ github.token }} + PREVIEW_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }} + run: >- + gh pr comment "${{ github.event.pull_request.number }}" + --repo "${{ github.repository }}" + --edit-last + --create-if-none + --body "Apercu Cloudflare Pages : [${PREVIEW_URL}](${PREVIEW_URL})" + + production: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: verifie + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + steps: + - uses: actions/download-artifact@v4 + with: + name: app-dist + path: dist/app + - name: Deploie Cloudflare Pages en production + uses: cloudflare/wrangler-action@v4 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy dist/app --project-name=bloub-app --branch=main + gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f342c..c3cda95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,5 +90,5 @@ fixed, and the measurement is in the commit that fixes it. - `StateDef.hint`: 15 hardcoded French strings nothing read, inside a type meant to become public. -[unreleased]: https://github.com/jeremy-prt/bloub/compare/v0.1.1...HEAD -[0.1.1]: https://github.com/jeremy-prt/bloub/releases/tag/v0.1.1 +[unreleased]: https://github.com/NorbertBodziony/bloub/compare/v0.1.1...HEAD +[0.1.1]: https://github.com/NorbertBodziony/bloub/releases/tag/v0.1.1 diff --git a/CLAUDE.md b/CLAUDE.md index 31736e4..a3980df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,16 +3,16 @@ ## Commands ```bash -pnpm dev # 5190 (set in vite.config.ts, mirrored in .claude/launch.json) -pnpm test # vitest -pnpm build # vue-tsc --noEmit && vite build +bun run dev # 5190 (set in vite.config.ts, mirrored in .claude/launch.json) +bun run test # Vitest +bun run build # type check, demo build, and package build ``` -Vue 3.5 + Vite 8 + TS strict + Tailwind 4 (`@tailwindcss/vite` plugin, no -`tailwind.config.js`), pnpm. +SolidJS + Vite 8 + TS strict + Tailwind 4 (`@tailwindcss/vite` plugin, no +`tailwind.config.js`), Bun. Style: 2 spaces, single quotes, **no semicolons**, comments in French. No ESLint -and no Prettier: `vue-tsc` is the only gate, so run `pnpm build` before +and no Prettier: strict TypeScript is the source gate, so run `bun run build` before concluding. ## The most important rule @@ -106,7 +106,7 @@ Details and the reasoning behind each are in [docs/](docs/): arrow-key navigation and focus moved into the menu on open, and they stop exposing the children as ordinary buttons. Three popups declared them and implemented none of it, so the Tab order didn't match what was announced — they are plain button lists now, with - `aria-haspopup="true"` and `aria-expanded`. `Settings.vue` shows the other route: a real + `aria-haspopup="true"` and `aria-expanded`. `Settings.tsx` shows the other route: a real `radiogroup` with a moving `tabindex`. Pick one, never the label alone. - **64rem is the only breakpoint, and it separates two different layouts, not two sizes.** Above it the scene is the three-column grid and the page never scrolls (`#app { overflow: @@ -146,11 +146,11 @@ run it, the component's API. Don't duplicate it here. ## Tests -`pnpm test` runs in `node` by default. **One file asks for a DOM** and says so on its first -line (`// @vitest-environment happy-dom`): `ui/capture.test.ts`, which mounts `BloubBot.vue` +`bun run test` runs in `node` by default. DOM tests ask for `happy-dom` on their first +line (`// @vitest-environment happy-dom`). `ui/capture.test.ts` mounts `BloubBot.tsx` to check the off-screen player — the exported render must be the component's own, not a -second drawing built beside it. That is also why `vitest.config.ts` carries the Vue plugin. -Keep the DOM per-file: a global DOM environment would slow the whole suite for one test. +second drawing built beside it. That is also why `vitest.config.ts` carries the Solid plugin. +Keep the DOM per-file: a global DOM environment would slow the rest of the suite. `capture.test.ts` is the one that catches what nothing else can — the export defects are invisible short of stepping through an MP4 frame by frame. @@ -166,7 +166,7 @@ are what `engine.sample(1)` returns for `idle`, byte for byte. `favicon.ico` and `apple-touch-icon.png` are rasterised from it. `docs/demo.gif` and `docs/states.png` are the same idea: rendered by walking -`engine.sample(t)` and writing the SVG layers in `BloubBot.vue`'s order, then +`engine.sample(t)` and writing the SVG layers in `BloubBot.tsx`'s order, then `rsvg-convert` + `ffmpeg`. They are **not** browser captures: the browser pane suspends `requestAnimationFrame` when hidden, so an animation can't be captured there at all. To redo them, drive the engine, don't reach for a screenshot. diff --git a/README.md b/README.md index 523902a..9421947 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # bloub +[Live demo](https://bloub-app.pages.dev) · +[](https://github.com/NorbertBodziony/bloub/actions/workflows/ci.yml) + An SVG recreation of the x.ai bot avatar: **one filled black shape** that morphs between 14 states, **two white shapes** for the eyes that morph independently, on a plain background. No animation library. @@ -9,19 +12,20 @@ a plain background. No animation library. ## Running it ```bash -pnpm install -pnpm dev +bun install +bun run dev ``` Then open http://localhost:5190. ```bash -pnpm test # vitest -pnpm build # vue-tsc --noEmit && vite build +bun run test # Vitest +bun run build # strict type check, demo build, and package build ``` -Vue 3, Vite, TypeScript, Tailwind 4. No ESLint and no Prettier: `vue-tsc` is the -only gate, so run `pnpm build` before you call something done. +SolidJS, Vite, TypeScript, Tailwind 4, and Bun. No ESLint and no Prettier: +strict TypeScript is the source gate, so run `bun run build` before you call +something done. ## What's in it @@ -80,21 +84,36 @@ the DOM-less test suite possible. | [docs/export.md](docs/export.md) | Exporting to SVG, PNG, GIF and MP4 | | [docs/i18n.md](docs/i18n.md) | The hand-rolled translation layer | -## Using the component +## Using the package + +```bash +bun add bloub solid-js +``` + +```tsx +import { createSignal } from 'solid-js' +import { BloubBot, defaultCycle, type BloubBotRef } from 'bloub' + +const [playing, setPlaying] = createSignal(false) +let bot: BloubBotRef | undefined -```vue - - + (bot = value)} + cycle={defaultCycle().blocks} + playing={playing()} + onPlayingChange={setPlaying} +/> ``` -`block` is the playback cursor: a montage can play the same state twice, so the -index is what identifies where you are; `state` follows it as an output. Pass -`frozenAt` and the component renders one exact frame with no animation loop, which -is how the thumbnails and the state board are drawn. +`block` is the playback cursor. A montage can play the same state twice, so the +index identifies the current item. `state` follows it as an output. Pass +`frozenAt` to render one exact frame with no animation loop. The component also +exports a ref with `svg`, `seek(index, offset?)`, and `renderAt(seconds)`. Props: `size`, `shape`, `color`, `expression`, `paper`, `frozenAt`, `cycle`, -`follow`, `gaze`. Models: `block`, `state`, `playing`, `elapsed`. See -[BloubBot.vue](src/components/BloubBot.vue) for the details. +`follow`, `gaze`, `ariaLabel`, `block`, `state`, `playing`, and `elapsed`. Each +playback value has a matching `on...Change` callback. See +[BloubBot.tsx](src/components/BloubBot.tsx) for the details. ## Changes diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..c86719a --- /dev/null +++ b/bun.lock @@ -0,0 +1,365 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "bloub", + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.0.0", + "happy-dom": "^20.11.2", + "mediabunny": "^1.54.0", + "solid-js": "^1.9.15", + "tailwindcss": "^4.3.3", + "typescript": "^5.9.3", + "vite": "^8.2.1", + "vite-plugin-solid": "^2.11.14", + "vitest": "^4.1.10", + }, + "peerDependencies": { + "solid-js": "^1.9.15", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "7.29.7", "@babel/helper-validator-identifier": "7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5", "@jridgewell/trace-mapping": "0.3.31" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "0.3.13", "@jridgewell/trace-mapping": "0.3.31" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.4", "", { "os": "none", "cpu": "arm64" }, "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "2.3.5", "enhanced-resolve": "5.24.5", "jiti": "2.7.0", "lightningcss": "1.32.0", "magic-string": "0.30.21", "source-map-js": "1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "8.2.1" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "4.0.2", "assertion-error": "2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.12", "", { "dependencies": { "@types/dom-webcodecs": "0.1.13" } }, "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw=="], + + "@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], + + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "26.2.0" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@vitest/expect": ["@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.1" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "3.0.3", "magic-string": "0.30.21" }, "optionalDependencies": { "vite": "8.2.1" } }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "3.1.1" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], + + "@vitest/snapshot": ["@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" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + + "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "2.0.0", "tinyrainbow": "3.1.1" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.10", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.15", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.10" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.15" }, "optionalPeers": ["solid-js"] }, "sha512-GBmg1OiPb+OwcH51XbDAKPtvrPfQW7rCJTJxcp8+yhtWwN+kqnbEJk2SgVybd+uhTxTKAvjaFyiQSr/eUZBwzg=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="], + + "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "26.2.0" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.411", "", {}, "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg=="], + + "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "4.2.11", "tapable": "2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "1.0.9" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + + "fdir": ["fdir@6.5.0", "", { "optionalDependencies": { "picomatch": "4.0.5" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "happy-dom": ["happy-dom@20.11.2", "", { "dependencies": { "@types/node": "26.2.0", "@types/whatwg-mimetype": "3.0.2", "@types/ws": "8.18.1", "buffer-image-size": "0.6.4", "entities": "7.0.1", "whatwg-mimetype": "3.0.0", "ws": "8.21.3" } }, "sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw=="], + + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mediabunny": ["mediabunny@1.54.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "0.1.12", "@types/dom-webcodecs": "0.1.13" } }, "sha512-L7yaV6iS6SYtHAs0FPQsAy1fprwvoX39WqbAD7GaCQVXKj/7zwxlAQkCkL2K52XakuDES8lbRRNpUlGWUOnv8Q=="], + + "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "3.3.18", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "rolldown": ["rolldown@1.2.4", "", { "dependencies": { "@oxc-project/types": "0.144.0", "@rolldown/pluginutils": "1.0.1" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.4", "@rolldown/binding-darwin-arm64": "1.2.4", "@rolldown/binding-darwin-x64": "1.2.4", "@rolldown/binding-freebsd-x64": "1.2.4", "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", "@rolldown/binding-linux-arm64-gnu": "1.2.4", "@rolldown/binding-linux-arm64-musl": "1.2.4", "@rolldown/binding-linux-ppc64-gnu": "1.2.4", "@rolldown/binding-linux-s390x-gnu": "1.2.4", "@rolldown/binding-linux-x64-gnu": "1.2.4", "@rolldown/binding-linux-x64-musl": "1.2.4", "@rolldown/binding-openharmony-arm64": "1.2.4", "@rolldown/binding-win32-arm64-msvc": "1.2.4", "@rolldown/binding-win32-x64-msvc": "1.2.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "seroval": ["seroval@1.5.6", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="], + + "seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "solid-js": ["solid-js@1.9.15", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg=="], + + "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.5" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], + + "vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "1.33.0", "picomatch": "4.0.5", "postcss": "8.5.26", "rolldown": "1.2.4", "tinyglobby": "0.2.17" }, "optionalDependencies": { "@types/node": "26.2.0", "fsevents": "2.3.3", "jiti": "2.7.0" }, "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], + + "vite-plugin-solid": ["vite-plugin-solid@2.11.14", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.0.0 || ^7.0.0", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-7ZVBt8rpoyqmlwin2kRIUveaHoF6/kulY7gsnD+qFh4nS29V4OPAnw+ojoAspXIjObiL9o1xh9a/nTuYHm02Rw=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@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.4", "pathe": "2.0.3", "picomatch": "4.0.5", "std-env": "4.2.0", "tinybench": "2.9.0", "tinyexec": "1.3.0", "tinyglobby": "0.2.17", "tinyrainbow": "3.1.1", "why-is-node-running": "2.3.0" }, "optionalDependencies": { "@types/node": "26.2.0", "happy-dom": "20.11.2" }, "peerDependencies": { "vite": "8.2.1" }, "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "ws": ["ws@8.21.3", "", {}, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@tailwindcss/node/lightningcss": ["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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + } +} diff --git a/index.html b/index.html index e68a797..7388309 100644 --- a/index.html +++ b/index.html @@ -53,9 +53,9 @@ Les trois URL ci-dessous sont donc a reprendre si le domaine change, elles ne se deduisent de rien. --> - - - + + + diff --git a/package.json b/package.json index 504e5ad..f418fd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bloub", - "private": true, + "private": false, "version": "0.1.1", "type": "module", "description": "SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.", @@ -8,36 +8,55 @@ "author": "Jérémy Perret", "repository": { "type": "git", - "url": "git+https://github.com/jeremy-prt/bloub.git" + "url": "git+https://github.com/NorbertBodziony/bloub.git" }, + "homepage": "https://bloub-app.pages.dev", "keywords": [ "svg", "animation", "avatar", - "vue", + "solid-js", "morphing" ], - "packageManager": "pnpm@11.17.0", + "packageManager": "bun@1.4.0", + "files": [ + "dist/package" + ], + "types": "./dist/package/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/package/types/index.d.ts", + "browser": "./dist/package/index.js", + "import": "./dist/package/server.js", + "default": "./dist/package/server.js" + } + }, + "sideEffects": false, "scripts": { "dev": "vite", - "build": "vue-tsc --noEmit && vite build", + "typecheck": "tsc --noEmit", + "build": "bun run typecheck && bun run build:app && bun run build:package", + "build:app": "vite build", + "build:package": "vite build --config vite.lib.config.ts && vite build --config vite.lib.ssr.config.ts && tsc -p tsconfig.lib.json", "preview": "vite preview", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:package": "bun run build:package && tsc -p tsconfig.package-test.json && bun -e \"import { BotEngine } from 'bloub'; console.log(new BotEngine().sample(0).bodyPath.length)\"", + "pack:check": "bun run test:package && bun pm pack --dry-run" }, - "dependencies": { - "mediabunny": "^1.54.0", - "vue": "^3.5.41" + "peerDependencies": { + "solid-js": "^1.9.15" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", "@types/node": "^26.0.0", - "@vitejs/plugin-vue": "^6.0.8", "happy-dom": "^20.11.2", + "mediabunny": "^1.54.0", + "solid-js": "^1.9.15", "tailwindcss": "^4.3.3", "typescript": "^5.9.3", "vite": "^8.2.1", - "vitest": "^4.1.10", - "vue-tsc": "^3.3.9" + "vite-plugin-solid": "^2.11.14", + "vitest": "^4.1.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index d8467ea..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1391 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - mediabunny: - specifier: ^1.54.0 - version: 1.54.0 - vue: - specifier: ^3.5.41 - version: 3.5.41(typescript@5.9.3) - devDependencies: - '@tailwindcss/vite': - specifier: ^4.3.3 - version: 4.3.3(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0)) - '@types/node': - specifier: ^26.0.0 - version: 26.2.0 - '@vitejs/plugin-vue': - specifier: ^6.0.8 - version: 6.0.8(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))(vue@3.5.41(typescript@5.9.3)) - happy-dom: - specifier: ^20.11.2 - version: 20.11.2 - tailwindcss: - specifier: ^4.3.3 - version: 4.3.3 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vite: - specifier: ^8.2.1 - version: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) - vitest: - specifier: ^4.1.10 - version: 4.1.10(@types/node@26.2.0)(happy-dom@20.11.2)(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0)) - vue-tsc: - specifier: ^3.3.9 - version: 3.3.9(typescript@5.9.3) - -packages: - - '@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.8': - resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/types@7.29.8': - resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} - engines: {node: '>=6.9.0'} - - '@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==} - - '@oxc-project/types@0.144.0': - resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} - - '@rolldown/binding-android-arm64@1.2.4': - resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.2.4': - resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.2.4': - resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.2.4': - resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.2.4': - resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.2.4': - resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.2.4': - resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.2.4': - resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.2.4': - resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.2.4': - resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.2.4': - resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.2.4': - resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-win32-arm64-msvc@1.2.4': - resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.2.4': - resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@tailwindcss/node@4.3.3': - resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - - '@tailwindcss/oxide-android-arm64@4.3.3': - resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.3.3': - resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.3.3': - resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.3.3': - resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.3.3': - resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.3.3': - resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.3.3': - resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} - engines: {node: '>= 20'} - - '@tailwindcss/vite@4.3.3': - resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 || ^8 - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/dom-mediacapture-transform@0.1.12': - resolution: {integrity: sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==} - - '@types/dom-webcodecs@0.1.13': - resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/node@26.2.0': - resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} - - '@types/whatwg-mimetype@3.0.2': - resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} - - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - - '@vitejs/plugin-vue@6.0.8': - resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vue: ^3.2.25 - - '@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@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@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - - '@volar/language-core@2.4.28': - resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} - - '@volar/source-map@2.4.28': - resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} - - '@volar/typescript@2.4.28': - resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - - '@vue/compiler-core@3.5.41': - resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} - - '@vue/compiler-dom@3.5.41': - resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} - - '@vue/compiler-sfc@3.5.41': - resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} - - '@vue/compiler-ssr@3.5.41': - resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} - - '@vue/language-core@3.3.9': - resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==} - - '@vue/reactivity@3.5.41': - resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==} - - '@vue/runtime-core@3.5.41': - resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==} - - '@vue/runtime-dom@3.5.41': - resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==} - - '@vue/server-renderer@3.5.41': - resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==} - - '@vue/shared@3.5.41': - resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} - - alien-signals@3.2.1: - resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - buffer-image-size@0.6.4: - resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} - engines: {node: '>=4.0'} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - enhanced-resolve@5.24.5: - resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} - engines: {node: '>=10.13.0'} - - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} - - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - expect-type@1.4.0: - resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} - engines: {node: '>=12.0.0'} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - happy-dom@20.11.2: - resolution: {integrity: sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==} - engines: {node: '>=20.0.0'} - - jiti@2.7.0: - resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - 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-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - 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-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - 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-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - 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-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - 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-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - 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-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - 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-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - 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-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - 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-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - 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-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - mediabunny@1.54.0: - resolution: {integrity: sha512-L7yaV6iS6SYtHAs0FPQsAy1fprwvoX39WqbAD7GaCQVXKj/7zwxlAQkCkL2K52XakuDES8lbRRNpUlGWUOnv8Q==} - - muggle-string@0.4.1: - resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} - - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} - engines: {node: ^10 || ^12 || >=14} - - rolldown@1.2.4: - resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - - tailwindcss@4.3.3: - resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} - - tapable@2.3.3: - resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} - engines: {node: '>=6'} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@1.3.0: - resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} - engines: {node: '>=18'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.1.1: - resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} - engines: {node: '>=14.0.0'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - - vite@8.2.1: - resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.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 - - 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 - - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - - vue-tsc@3.3.9: - resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==} - hasBin: true - peerDependencies: - typescript: '>=5.0.0' - - vue@3.5.41: - resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - whatwg-mimetype@3.0.0: - resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} - engines: {node: '>=12'} - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - ws@8.21.3: - resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} - 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 - -snapshots: - - '@babel/helper-string-parser@7.29.7': {} - - '@babel/helper-validator-identifier@7.29.7': {} - - '@babel/parser@7.29.8': - dependencies: - '@babel/types': 7.29.8 - - '@babel/types@7.29.8': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - - '@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 - - '@oxc-project/types@0.144.0': {} - - '@rolldown/binding-android-arm64@1.2.4': - optional: true - - '@rolldown/binding-darwin-arm64@1.2.4': - optional: true - - '@rolldown/binding-darwin-x64@1.2.4': - optional: true - - '@rolldown/binding-freebsd-x64@1.2.4': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.2.4': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.2.4': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.2.4': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.2.4': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.2.4': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.2.4': - optional: true - - '@rolldown/binding-linux-x64-musl@1.2.4': - optional: true - - '@rolldown/binding-openharmony-arm64@1.2.4': - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.2.4': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.2.4': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@standard-schema/spec@1.1.0': {} - - '@tailwindcss/node@4.3.3': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.5 - jiti: 2.7.0 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.3.3 - - '@tailwindcss/oxide-android-arm64@4.3.3': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.3.3': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.3.3': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.3.3': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.3.3': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - optional: true - - '@tailwindcss/oxide@4.3.3': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.3 - '@tailwindcss/oxide-darwin-arm64': 4.3.3 - '@tailwindcss/oxide-darwin-x64': 4.3.3 - '@tailwindcss/oxide-freebsd-x64': 4.3.3 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 - '@tailwindcss/oxide-linux-x64-musl': 4.3.3 - '@tailwindcss/oxide-wasm32-wasi': 4.3.3 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - - '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))': - dependencies: - '@tailwindcss/node': 4.3.3 - '@tailwindcss/oxide': 4.3.3 - tailwindcss: 4.3.3 - vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/dom-mediacapture-transform@0.1.12': - dependencies: - '@types/dom-webcodecs': 0.1.13 - - '@types/dom-webcodecs@0.1.13': {} - - '@types/estree@1.0.9': {} - - '@types/node@26.2.0': - dependencies: - undici-types: 8.3.0 - - '@types/whatwg-mimetype@3.0.2': {} - - '@types/ws@8.18.1': - dependencies: - '@types/node': 26.2.0 - - '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))(vue@3.5.41(typescript@5.9.3))': - dependencies: - '@rolldown/pluginutils': 1.0.1 - vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) - vue: 3.5.41(typescript@5.9.3) - - '@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.1 - - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) - - '@vitest/pretty-format@4.1.10': - dependencies: - tinyrainbow: 3.1.1 - - '@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@4.1.10': {} - - '@vitest/utils@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 - - '@volar/language-core@2.4.28': - dependencies: - '@volar/source-map': 2.4.28 - - '@volar/source-map@2.4.28': {} - - '@volar/typescript@2.4.28': - dependencies: - '@volar/language-core': 2.4.28 - path-browserify: 1.0.1 - vscode-uri: 3.1.0 - - '@vue/compiler-core@3.5.41': - dependencies: - '@babel/parser': 7.29.8 - '@vue/shared': 3.5.41 - entities: 7.0.1 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - - '@vue/compiler-dom@3.5.41': - dependencies: - '@vue/compiler-core': 3.5.41 - '@vue/shared': 3.5.41 - - '@vue/compiler-sfc@3.5.41': - dependencies: - '@babel/parser': 7.29.8 - '@vue/compiler-core': 3.5.41 - '@vue/compiler-dom': 3.5.41 - '@vue/compiler-ssr': 3.5.41 - '@vue/shared': 3.5.41 - estree-walker: 2.0.2 - magic-string: 0.30.21 - postcss: 8.5.26 - source-map-js: 1.2.1 - - '@vue/compiler-ssr@3.5.41': - dependencies: - '@vue/compiler-dom': 3.5.41 - '@vue/shared': 3.5.41 - - '@vue/language-core@3.3.9': - dependencies: - '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.41 - '@vue/shared': 3.5.41 - alien-signals: 3.2.1 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - picomatch: 4.0.5 - - '@vue/reactivity@3.5.41': - dependencies: - '@vue/shared': 3.5.41 - - '@vue/runtime-core@3.5.41': - dependencies: - '@vue/reactivity': 3.5.41 - '@vue/shared': 3.5.41 - - '@vue/runtime-dom@3.5.41': - dependencies: - '@vue/reactivity': 3.5.41 - '@vue/runtime-core': 3.5.41 - '@vue/shared': 3.5.41 - csstype: 3.2.3 - - '@vue/server-renderer@3.5.41': - dependencies: - '@vue/compiler-ssr': 3.5.41 - '@vue/runtime-dom': 3.5.41 - '@vue/shared': 3.5.41 - - '@vue/shared@3.5.41': {} - - alien-signals@3.2.1: {} - - assertion-error@2.0.1: {} - - buffer-image-size@0.6.4: - dependencies: - '@types/node': 26.2.0 - - chai@6.2.2: {} - - convert-source-map@2.0.0: {} - - csstype@3.2.3: {} - - detect-libc@2.1.2: {} - - enhanced-resolve@5.24.5: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - entities@7.0.1: {} - - es-module-lexer@2.3.1: {} - - estree-walker@2.0.2: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - expect-type@1.4.0: {} - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - fsevents@2.3.3: - optional: true - - graceful-fs@4.2.11: {} - - happy-dom@20.11.2: - dependencies: - '@types/node': 26.2.0 - '@types/whatwg-mimetype': 3.0.2 - '@types/ws': 8.18.1 - buffer-image-size: 0.6.4 - entities: 7.0.1 - whatwg-mimetype: 3.0.0 - ws: 8.21.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - jiti@2.7.0: {} - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.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 - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - mediabunny@1.54.0: - dependencies: - '@types/dom-mediacapture-transform': 0.1.12 - '@types/dom-webcodecs': 0.1.13 - - muggle-string@0.4.1: {} - - nanoid@3.3.18: {} - - obug@2.1.4: {} - - path-browserify@1.0.1: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@4.0.5: {} - - postcss@8.5.26: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - rolldown@1.2.4: - dependencies: - '@oxc-project/types': 0.144.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.4 - '@rolldown/binding-darwin-arm64': 1.2.4 - '@rolldown/binding-darwin-x64': 1.2.4 - '@rolldown/binding-freebsd-x64': 1.2.4 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 - '@rolldown/binding-linux-arm64-gnu': 1.2.4 - '@rolldown/binding-linux-arm64-musl': 1.2.4 - '@rolldown/binding-linux-ppc64-gnu': 1.2.4 - '@rolldown/binding-linux-s390x-gnu': 1.2.4 - '@rolldown/binding-linux-x64-gnu': 1.2.4 - '@rolldown/binding-linux-x64-musl': 1.2.4 - '@rolldown/binding-openharmony-arm64': 1.2.4 - '@rolldown/binding-win32-arm64-msvc': 1.2.4 - '@rolldown/binding-win32-x64-msvc': 1.2.4 - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - std-env@4.2.0: {} - - tailwindcss@4.3.3: {} - - tapable@2.3.3: {} - - tinybench@2.9.0: {} - - tinyexec@1.3.0: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tinyrainbow@3.1.1: {} - - typescript@5.9.3: {} - - undici-types@8.3.0: {} - - vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.26 - rolldown: 1.2.4 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 26.2.0 - fsevents: 2.3.3 - jiti: 2.7.0 - - vitest@4.1.10(@types/node@26.2.0)(happy-dom@20.11.2)(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.2.0)(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.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.3.0 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 26.2.0 - happy-dom: 20.11.2 - transitivePeerDependencies: - - msw - - vscode-uri@3.1.0: {} - - vue-tsc@3.3.9(typescript@5.9.3): - dependencies: - '@volar/typescript': 2.4.28 - '@vue/language-core': 3.3.9 - typescript: 5.9.3 - - vue@3.5.41(typescript@5.9.3): - dependencies: - '@vue/compiler-dom': 3.5.41 - '@vue/compiler-sfc': 3.5.41 - '@vue/runtime-dom': 3.5.41 - '@vue/server-renderer': 3.5.41 - '@vue/shared': 3.5.41 - optionalDependencies: - typescript: 5.9.3 - - whatwg-mimetype@3.0.0: {} - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - ws@8.21.3: {} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..4ac4be7 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,333 @@ +import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' +import BotTile from '@/components/BotTile' +import Customizer from '@/components/Customizer' +import BloubBot, { type BloubBotRef } from '@/components/BloubBot' +import ExportBar from '@/components/ExportBar' +import CycleDialog from '@/components/CycleDialog' +import GifDialog from '@/components/GifDialog' +import Settings from '@/components/Settings' +import SideRail, { type ViewId } from '@/components/SideRail' +import Timeline from '@/components/Timeline' +import { nomDeCycle, t } from '@/i18n' +import { copie, copieTexte, cycleVersGif, cycleVersMp4, svgAutonome, telecharge, versGifAnime, versPng, versSvgAnime } from '@/ui/capture' +import { ACTION_BY_ID, ANIM_IMAGES, ANIM_PAS, CYCLE_TAILLE, FOND_GIF_DEFAUT, FORMAT_CYCLE_DEFAUT, GIF_IMAGES, GIF_PAS, BLANC, Abandon, couleurDeFond, cycleImages, cyclePas, nomFichier, type ActionId, type EtatExport, type FondGif, type FormatCycle } from '@/ui/export' +import { HUMEURS } from '@/ui/gaze' +import { INTRO, INTRO_GAZE, POSE_AT, introDue } from '@/ui/intro' +import { ecris, lis, type NomStocke } from '@/ui/stockage' +import { blockAt, blocksWith, defaultCycle, makeBlock, parseCycles, totalDuration, type Cycle } from '@/bot/cycles' +import { DEFAULT_EXPRESSION, EXPRESSION_BY_ID } from '@/bot/expressions' +import { COLOR_BY_ID, DEFAULT_COLOR, DEFAULT_SHAPE, SHAPE_BY_ID } from '@/bot/skins' +import { POSES, SEQUENCE, STATES, type StateId } from '@/bot/states' + +function readHash() { + const params = new URLSearchParams(location.hash.slice(1)) + const asked = params.get('etat') as StateId | null + const known = STATES.some((state) => state.id === asked) + return { + state: known ? asked! : 'idle' as StateId, + named: known, + playing: !params.has('stop'), + gallery: params.has('planche'), + arrivee: params.has('arrivee') + } +} + +function stored(name: NomStocke, fallback: string, exists: (value: string) => boolean) { + const value = lis(name) + return value && exists(value) ? value : fallback +} + +const REST = [makeBlock('idle')] +const ENTREE = [makeBlock('swirl'), makeBlock('idle')] +const ENTREE_CALME = [makeBlock('idle')] +const NOM = 'BLOUB' +const HUMEUR_MS = 4200 +const RETARD_ARRIVEE = 400 +const CONFIRMATION_MS = 1800 + +export default function App() { + const initial = readHash() + const [gallery, setGallery] = createSignal(initial.gallery) + const quietQuery = window.matchMedia('(prefers-reduced-motion: reduce)') + const [quiet, setQuiet] = createSignal(quietQuery.matches) + const [navigationEntry] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[] + const navigation = navigationEntry?.type ?? 'navigate' + const [intro, setIntro] = createSignal(initial.arrivee || introDue({ named: initial.named, gallery: initial.gallery, rechargement: navigation !== 'navigate', calme: quiet() })) + + const restored = parseCycles(lis('cycles')) + const [cycles, setCycles] = createSignal(restored.length ? restored : [defaultCycle()]) + const [activeId, setActiveId] = createSignal(stored('cycle', cycles()[0]!.id, (value) => cycles().some((cycle) => cycle.id === value))) + const [block, setBlock] = createSignal(0) + const [elapsed, setElapsed] = createSignal(0) + const cycle = createMemo(() => cycles().find((item) => item.id === activeId()) ?? cycles()[0]!) + + function locate(id: StateId) { + const order = [cycle(), ...cycles().filter((item) => item.id !== activeId())] + for (const item of order) { + const index = item.blocks.findIndex((candidate) => candidate.state === id) + if (index >= 0) return { id: item.id, index } + } + return null + } + + if (initial.named) { + const found = locate(initial.state) + if (found) { + setActiveId(found.id) + setBlock(found.index) + } + } + + const [state, setState] = createSignal(intro() ? 'idle' : (cycle().blocks[block()]?.state ?? 'idle')) + const [view, setView] = createSignal(initial.named ? 'animations' : 'personnaliser') + const [preview, setPreview] = createSignal(false) + const [playing, setPlaying] = createSignal(intro() || (initial.playing && view() === 'animations')) + const [shape, setShape] = createSignal(stored('forme', DEFAULT_SHAPE, (value) => SHAPE_BY_ID.has(value))) + const [color, setColor] = createSignal(stored('couleur', DEFAULT_COLOR, (value) => COLOR_BY_ID.has(value))) + const [expression, setExpression] = createSignal(stored('expression', DEFAULT_EXPRESSION, (value) => EXPRESSION_BY_ID.has(value))) + const [mood, setMood] = createSignal(null) + + let pendingSave: ReturnType + function saveCycles() { + clearTimeout(pendingSave) + ecris('cycles', JSON.stringify(cycles())) + } + createEffect(on(cycles, () => { clearTimeout(pendingSave); pendingSave = setTimeout(saveCycles, 250) }, { defer: true })) + createEffect(on(activeId, (value) => ecris('cycle', value), { defer: true })) + createEffect(on(shape, (value) => ecris('forme', value), { defer: true })) + createEffect(on(color, (value) => ecris('couleur', value), { defer: true })) + createEffect(on(expression, (value) => ecris('expression', value), { defer: true })) + createEffect(on(preview, setPlaying, { defer: true })) + + let writtenHash = '' + createEffect(on(() => [state(), playing()] as const, ([id, on]) => { + if (view() !== 'animations') return + writtenHash = `#etat=${id}${on ? '' : '&stop'}` + location.replace(writtenHash) + }, { defer: true })) + + let resume = initial.named && initial.playing + let resumeBlock = block() + const played = createMemo(() => { + if (intro()) return INTRO + if (view() === 'animations') return cycle().blocks + if (view() !== 'reglages') return REST + return quiet() ? ENTREE_CALME : ENTREE + }) + + createEffect(on(view, (now, before) => { + setIntro(false) + if (before === 'animations') { + resume = playing() + resumeBlock = block() + } + if (now === 'animations') { + setPlaying(resume) + setBlock(resumeBlock) + } else { + setBlock(0) + setPlaying(now === 'reglages') + } + }, { defer: true })) + + createEffect(on(block, (index) => { + if (intro()) { + if (index >= INTRO.length - 1) { + setIntro(false) + setPlaying(false) + } + } else if (view() === 'reglages' && index > 0) setPlaying(false) + }, { defer: true })) + + const bare = createMemo(() => intro() && block() < POSE_AT) + const leftOpen = createMemo(() => !bare() && view() === 'reglages') + const rightOpen = createMemo(() => !bare() && view() !== 'reglages') + const displayedShape = createMemo(() => view() === 'reglages' || bare() ? DEFAULT_SHAPE : shape()) + const order = createMemo(() => SEQUENCE.map((id) => STATES.find((state) => state.id === id)!)) + + let moodTimer: ReturnType | undefined + createEffect(on(view, (current) => { + clearInterval(moodTimer) + if (current !== 'reglages') return setMood(null) + let index = 0 + moodTimer = setInterval(() => setMood(HUMEURS[index++ % HUMEURS.length]!), HUMEUR_MS) + })) + + function addBlock(id: StateId) { + setCycles((items) => items.map((item) => item.id === cycle().id ? { ...item, blocks: blocksWith(item.blocks, id) } : item)) + } + + let bot: BloubBotRef | undefined + function onSeek(seconds: number) { + const target = blockAt(cycle().blocks, seconds) + bot?.seek(target.index, target.elapsed) + } + + const [exportBarHidden, setExportBarHidden] = createSignal(false) + let exportBarTimer: ReturnType | undefined + createEffect(on(bare, (still, before) => { + if (!before || still) return + setExportBarHidden(true) + clearTimeout(exportBarTimer) + exportBarTimer = setTimeout(() => setExportBarHidden(false), RETARD_ARRIVEE) + }, { defer: true })) + + const [cycleDialog, setCycleDialog] = createSignal(false) + const [cycleFormat, setCycleFormat] = createSignal(FORMAT_CYCLE_DEFAUT) + const [cycleBackground, setCycleBackground] = createSignal(FOND_GIF_DEFAUT) + const [cycleProgress, setCycleProgress] = createSignal(null) + const [cycleError, setCycleError] = createSignal(false) + let cycleAbort: AbortController | null = null + + async function exportCycle() { + if (cycleProgress() !== null) return + setCycleError(false) + const controller = new AbortController() + cycleAbort = controller + const blocks = cycle().blocks + const format = cycleFormat() + const images = cycleImages(totalDuration(blocks), format) + const step = cyclePas(format) + const size = CYCLE_TAILLE[format] + const settings = { shape: shape(), color: color(), expression: expression() } + setCycleProgress(0) + try { + const mp4 = format === 'mp4' + const file = mp4 + ? await cycleVersMp4(settings, blocks, size, images, step, BLANC, (done, total) => setCycleProgress(done / total), controller.signal) + : await cycleVersGif(settings, blocks, size, images, step, couleurDeFond(cycleBackground()), (done, total) => setCycleProgress(done / total), controller.signal) + telecharge(file, nomFichier(nomDeCycle(cycle()), '', '', mp4 ? 'mp4' : 'gif')) + setCycleDialog(false) + } catch (error) { + if (!(error instanceof Abandon)) setCycleError(true) + } finally { + setCycleProgress(null) + cycleAbort = null + } + } + createEffect(on(cycleDialog, (open) => open && setCycleError(false), { defer: true })) + + const [exportState, setExportState] = createSignal('pret') + const [gifBackground, setGifBackground] = createSignal(FOND_GIF_DEFAUT) + const [gifDialog, setGifDialog] = createSignal(false) + let confirmation: ReturnType | undefined + + async function exportAvatar(id: ActionId, confirmed = false) { + if (exportState() === 'occupe') return + if (!confirmed && ACTION_BY_ID.get(id)?.mode === 'gif') return setGifDialog(true) + const action = ACTION_BY_ID.get(id) + const svg = bot?.svg + if (!action || !svg) return + clearTimeout(confirmation) + setExportState('occupe') + const filename = () => nomFichier(shape(), expression(), color(), action.extension, action.suffixe) + try { + if (action.mode === 'anime') { + telecharge(await versSvgAnime({ shape: shape(), color: color(), expression: expression() }, action.taille, ANIM_IMAGES, ANIM_PAS), filename()) + setExportState('exporte') + } else if (action.mode === 'gif') { + telecharge(await versGifAnime({ shape: shape(), color: color(), expression: expression() }, action.taille, GIF_IMAGES, GIF_PAS, couleurDeFond(gifBackground())), filename()) + setExportState('exporte') + } else { + const markup = svgAutonome(svg, action.taille) + if (action.mode === 'copieImage') { + await copie(versPng(markup, action.taille)) + setExportState('copie') + } else if (action.mode === 'copieTexte') { + await copieTexte(markup) + setExportState('copie') + } else { + telecharge(action.extension === 'svg' ? new Blob([markup], { type: 'image/svg+xml' }) : await versPng(markup, action.taille), filename()) + setExportState('exporte') + } + } + } catch { + setExportState('erreur') + } + confirmation = setTimeout(() => setExportState('pret'), CONFIRMATION_MS) + } + + createEffect(on(view, (current) => queueMicrotask(() => current === 'reglages' && bot?.seek(0, 0)), { defer: true })) + + function onHashChange() { + if (location.hash === writtenHash) { + writtenHash = '' + return + } + const next = readHash() + if (next.arrivee && !initial.arrivee) return location.reload() + setGallery(next.gallery) + if (next.gallery || !next.named) return + const found = locate(next.state) + if (!found) return + setView('animations') + setActiveId(found.id) + setBlock(found.index) + } + + function onKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') setPreview(false) + } + + onMount(() => { + const onQuiet = (event: MediaQueryListEvent) => setQuiet(event.matches) + quietQuery.addEventListener('change', onQuiet) + window.addEventListener('pagehide', saveCycles) + window.addEventListener('hashchange', onHashChange) + window.addEventListener('keydown', onKeyDown) + onCleanup(() => { + quietQuery.removeEventListener('change', onQuiet) + window.removeEventListener('pagehide', saveCycles) + window.removeEventListener('hashchange', onHashChange) + window.removeEventListener('keydown', onKeyDown) + }) + }) + onCleanup(() => { + clearInterval(moodTimer) + clearTimeout(exportBarTimer) + clearTimeout(confirmation) + clearTimeout(pendingSave) + }) + + return ( + + {t('gallery.back')} + + {(item) => {t(`states.${item.id}`)}} + + + } + > + {t('app.name')} + setPreview(false)}>{t('preview.exit')}{t('preview.key')}}> + + + + + + + { bot = value }} class="h-auto max-w-full" state={state()} block={block()} elapsed={elapsed()} playing={playing()} onStateChange={setState} onBlockChange={setBlock} onElapsedChange={setElapsed} onPlayingChange={setPlaying} cycle={played()} size={preview() ? 560 : 440} shape={displayedShape()} color={color()} expression={mood() ?? expression()} follow={view() === 'reglages'} gaze={intro() ? INTRO_GAZE : null} ariaLabel={t('app.botAria')} /> + + + + void exportAvatar('gif', true)} /> + + void exportCycle()} onCancel={() => cycleAbort?.abort()} /> + + + + + + {NOM} + setPreview(true)} onExport={() => setCycleDialog(true)} /> + + ) +} diff --git a/src/App.vue b/src/App.vue deleted file mode 100644 index a81a1ee..0000000 --- a/src/App.vue +++ /dev/null @@ -1,1006 +0,0 @@ - - - - - - {{ t('gallery.back') }} - - - - - {{ t(`states.${s.id}`) }} - - - - - - - {{ t('app.name') }} - - - - - - {{ t('preview.exit') }} - {{ t('preview.key') }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ NOM }} - - - - - diff --git a/src/components/BlockPicker.tsx b/src/components/BlockPicker.tsx new file mode 100644 index 0000000..5adc56f --- /dev/null +++ b/src/components/BlockPicker.tsx @@ -0,0 +1,50 @@ +import { For, createSignal } from 'solid-js' +import BotTile from './BotTile' +import { POSES, SEQUENCE, STATE_BY_ID, type StateId } from '@/bot/states' +import { t } from '@/i18n' + +const PALETTE = SEQUENCE.map((id) => STATE_BY_ID.get(id)!) +const WIDTH = 288 + +export default function BlockPicker(props: { + shape: string + color: string + expression: string + onPick: (state: StateId) => void +}) { + let trigger!: HTMLButtonElement + let panel!: HTMLDivElement + const [position, setPosition] = createSignal>({}) + const [open, setOpen] = createSignal(false) + + function toggle() { + if (panel.matches(':popover-open')) return panel.hidePopover() + const rect = trigger.getBoundingClientRect() + setPosition({ + position: 'fixed', + top: 'auto', + right: 'auto', + left: `${Math.max(8, Math.min(rect.right - WIDTH, window.innerWidth - WIDTH - 8))}px`, + bottom: `${window.innerHeight - rect.top + 8}px` + }) + panel.showPopover() + } + + function pick(state: StateId) { + props.onPick(state) + panel.hidePopover() + } + + return ( + <> + + + setOpen(event.newState === 'open')} class="m-0 w-72 rounded-xl bg-white p-2 shadow-lg ring-1 ring-black/5" style={position()}> + + + {(state) => pick(state.id)} />} + + + + > + ) +} diff --git a/src/components/BlockPicker.vue b/src/components/BlockPicker.vue deleted file mode 100644 index 039383c..0000000 --- a/src/components/BlockPicker.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - - - + - - - - - - - - - diff --git a/src/components/BloubBot.test.tsx b/src/components/BloubBot.test.tsx new file mode 100644 index 0000000..001a091 --- /dev/null +++ b/src/components/BloubBot.test.tsx @@ -0,0 +1,51 @@ +// @vitest-environment happy-dom +import { render } from 'solid-js/web' +import { describe, expect, it, vi } from 'vitest' +import BloubBot, { type BloubBotRef } from './BloubBot' +import { defaultCycle } from '@/bot/cycles' + +describe('BloubBot Solid', () => { + it('rend une image figee et expose son controleur', async () => { + const host = document.createElement('div') + document.body.appendChild(host) + let bot: BloubBotRef | undefined + const dispose = render( + () => { bot = value }} />, + host + ) + + await Promise.resolve() + expect(host.querySelector('svg')).toBe(bot?.svg) + expect(host.querySelector('mask path')?.getAttribute('d')).toBeTruthy() + + dispose() + host.remove() + }) + + it('notifie les changements controles et rend une date exacte', async () => { + const host = document.createElement('div') + const onBlockChange = vi.fn() + const onStateChange = vi.fn() + let bot: BloubBotRef | undefined + const dispose = render( + () => ( + { bot = value }} + /> + ), + host + ) + + await Promise.resolve() + bot!.seek(1) + expect(onBlockChange).toHaveBeenCalledWith(1) + bot!.renderAt(3) + expect(onStateChange).toHaveBeenCalled() + + dispose() + }) +}) diff --git a/src/components/BloubBot.tsx b/src/components/BloubBot.tsx new file mode 100644 index 0000000..7e2b978 --- /dev/null +++ b/src/components/BloubBot.tsx @@ -0,0 +1,436 @@ +import { + For, + Show, + createEffect, + createMemo, + createSignal, + createUniqueId, + mergeProps, + on, + onCleanup, + onMount, + type JSX +} from 'solid-js' +import { Dynamic } from 'solid-js/web' +import { NOTIF_BLUE } from '../bot/decor' +import { BotEngine, type BotFrame } from '../bot/engine' +import { clamp, easings } from '../bot/math' +import { lookTarget, TURN_TIME, type GazeScript } from '../ui/gaze' +import { DEFAULT_EXPRESSION, EXPRESSION_BY_ID } from '../bot/expressions' +import { COLOR_BY_ID, DEFAULT_COLOR, DEFAULT_SHAPE, SHAPE_BY_ID, mixHex } from '../bot/skins' +import { blockAt, defaultCycle, offsetOf, type Block } from '../bot/cycles' +import { DEMI_VIEWBOX, RAYON } from '../bot/repere' +import { STATE_BY_ID, type StateId } from '../bot/states' + +export interface BloubBotRef { + readonly svg: SVGSVGElement + seek(index: number, offset?: number): void + renderAt(seconds: number): void +} + +export interface BloubBotProps { + size?: number + shape?: string + color?: string + expression?: string + paper?: string + ariaLabel?: string + frozenAt?: number + cycle?: Block[] + follow?: boolean + gaze?: GazeScript | null + block?: number + state?: StateId + playing?: boolean + elapsed?: number + onBlockChange?: (value: number) => void + onStateChange?: (value: StateId) => void + onPlayingChange?: (value: boolean) => void + onElapsedChange?: (value: number) => void + ref?: (value: BloubBotRef) => void + class?: string + style?: JSX.CSSProperties | string +} + +export default function BloubBot(rawProps: BloubBotProps) { + const props = mergeProps( + { + size: 320, + shape: DEFAULT_SHAPE, + color: DEFAULT_COLOR, + expression: DEFAULT_EXPRESSION, + paper: '#f9f9f9', + cycle: defaultCycle().blocks, + follow: false, + gaze: null, + ariaLabel: 'Animated Bloub avatar' + }, + rawProps + ) + + const [internalBlock, setInternalBlock] = createSignal(0) + const [internalState, setInternalState] = createSignal('idle') + const [internalPlaying] = createSignal(false) + const [internalElapsed, setInternalElapsed] = createSignal(0) + + const block = () => rawProps.block ?? internalBlock() + const state = () => rawProps.state ?? internalState() + const playing = () => rawProps.playing ?? internalPlaying() + const elapsed = () => rawProps.elapsed ?? internalElapsed() + + function setBlock(value: number) { + if (rawProps.block === undefined) setInternalBlock(value) + rawProps.onBlockChange?.(value) + } + + function setState(value: StateId) { + if (rawProps.state === undefined) setInternalState(value) + rawProps.onStateChange?.(value) + } + + function setElapsed(value: number) { + if (rawProps.elapsed === undefined) setInternalElapsed(value) + rawProps.onElapsedChange?.(value) + } + + const shapeRadii = createMemo(() => SHAPE_BY_ID.get(props.shape)?.radii ?? null) + const ink = createMemo(() => COLOR_BY_ID.get(props.color)?.hex ?? '#0a0a0c') + const expression = createMemo(() => EXPRESSION_BY_ID.get(props.expression) ?? null) + + const engine = new BotEngine(RAYON, state(), shapeRadii(), expression()) + const [frame, setFrame] = createSignal(engine.sample(props.frozenAt ?? 0), { + equals: false + }) + const uid = createUniqueId() + const maskId = `bot-mask-${uid}` + + let svg!: SVGSVGElement + let raf = 0 + let nextAt = Infinity + let last = 0 + let clock = 0 + let blockStart = 0 + let pendingOffset = 0 + let lastRenderedBlock = -1 + + function apply(index: number, from = 0) { + const current = props.cycle[index] + if (!current) { + nextAt = Infinity + return + } + blockStart = clock - from + setElapsed(from) + setState(current.state) + engine.setState(current.state, clock) + nextAt = playing() ? blockStart + current.duration : Infinity + } + + function goToBlock(index: number) { + setBlock(index) + apply(index) + } + + function seek(index: number, offset = 0) { + if (block() === index) { + apply(index, offset) + return + } + pendingOffset = offset + setBlock(index) + } + + function renderAt(seconds: number) { + const blocks = props.cycle + if (!blocks.length) return + const { index } = blockAt(blocks, seconds) + if (index !== lastRenderedBlock) { + const current = blocks[index]! + setState(current.state) + if (index < lastRenderedBlock) engine.reset(current.state, offsetOf(blocks, index)) + else engine.setState(current.state, offsetOf(blocks, index)) + lastRenderedBlock = index + } + setFrame(engine.sample(seconds)) + } + + let pointer: { x: number; y: number } | null = null + let aiming = false + let turnSince = 0 + let gazeSince = 0 + let scripted = false + const SCRIPT_MORPH = 1 / 60 + + function onPointerMove(event: PointerEvent) { + if (event.pointerType === 'touch') return + pointer = { x: event.clientX, y: event.clientY } + } + + function onPointerLeave() { + pointer = null + } + + function release() { + if (!aiming) return + engine.setLook(null, clock, TURN_TIME) + aiming = false + } + + function aim() { + if (!STATE_BY_ID.get(state())?.baseFace) { + release() + return + } + const box = svg?.getBoundingClientRect() + if (!box || box.width === 0 || box.height === 0) return + if (!aiming) turnSince = clock + const halfWidth = Math.max(1, window.innerWidth / 2) + const halfHeight = Math.max(1, window.innerHeight / 2) + engine.setLook( + lookTarget({ + nx: pointer ? clamp((pointer.x - (box.left + box.width / 2)) / halfWidth, -1, 1) : 0, + ny: pointer ? clamp((pointer.y - (box.top + box.height / 2)) / halfHeight, -1, 1) : 0, + tour: easings.easeOutQuint(clamp((clock - turnSince) / TURN_TIME)), + pointer: pointer !== null + }), + clock + ) + aiming = true + } + + function scriptedGaze(run: GazeScript) { + engine.setLook(run(clock - gazeSince), clock, SCRIPT_MORPH) + } + + function detach() { + window.removeEventListener('pointermove', onPointerMove) + document.removeEventListener('pointerleave', onPointerLeave) + } + + function tick(ms: number) { + raf = requestAnimationFrame(tick) + const dt = last ? Math.min((ms - last) / 1000, 0.064) : 0 + last = ms + clock += dt + + if (playing()) { + if (clock >= nextAt && props.cycle.length) goToBlock((block() + 1) % props.cycle.length) + else setElapsed(clock - blockStart) + } + + if (props.follow) aim() + else if (props.gaze) scriptedGaze(props.gaze) + setFrame(engine.sample(clock)) + } + + function redrawFrozen() { + if (props.frozenAt === undefined) return + setFrame(engine.sample(props.frozenAt)) + } + + createEffect( + on( + () => props.gaze, + (run) => { + if (run) { + gazeSince = clock + scripted = true + engine.setLook(run(0), clock - SCRIPT_MORPH, SCRIPT_MORPH) + } else if (scripted) { + engine.setLook(null, clock) + scripted = false + } + } + ) + ) + + createEffect( + on(block, (index) => { + apply(index, pendingOffset) + pendingOffset = 0 + }, { defer: true }) + ) + + createEffect( + on(state, (id) => { + if (engine.state === id) return + engine.setState(id, clock) + redrawFrozen() + }, { defer: true }) + ) + + createEffect( + on(playing, (on) => { + if (on) apply(block(), elapsed()) + else nextAt = Infinity + }, { defer: true }) + ) + + createEffect( + on( + () => props.cycle, + (blocks) => { + if (!blocks.length) { + nextAt = Infinity + return + } + const index = Math.min(block(), blocks.length - 1) + if (index !== block()) goToBlock(index) + else nextAt = playing() ? blockStart + blocks[index]!.duration : Infinity + }, + { defer: true } + ) + ) + + createEffect(on(shapeRadii, (radii) => { + engine.setShape(radii, clock) + redrawFrozen() + }, { defer: true })) + + createEffect(on(expression, (value) => { + engine.setExpression(value, clock) + redrawFrozen() + }, { defer: true })) + + createEffect(on(() => props.frozenAt, redrawFrozen, { defer: true })) + + createEffect(() => { + const on = props.follow && props.frozenAt === undefined + if (on) { + window.addEventListener('pointermove', onPointerMove) + document.addEventListener('pointerleave', onPointerLeave) + } else { + detach() + release() + } + }) + + onMount(() => { + rawProps.ref?.({ get svg() { return svg }, seek, renderAt }) + if (props.frozenAt !== undefined) return + apply(block(), elapsed()) + raf = requestAnimationFrame(tick) + }) + + onCleanup(() => { + if (typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(raf) + if (typeof window !== 'undefined') detach() + }) + + function dotAttrs(dot: BotFrame['dots'][number]) { + const fill = + dot.color ?? (dot.depth === undefined ? ink() : mixHex(props.paper, ink(), dot.depth)) + const common = { fill, opacity: dot.opacity } + return dot.d + ? { + ...common, + d: dot.d, + transform: `translate(${dot.x} ${dot.y}) rotate(${dot.rot ?? 0}) scale(${RAYON})` + } + : { ...common, cx: dot.x, cy: dot.y, r: dot.r } + } + + const Dots = () => ( + + {(dot) => } + + ) + + return ( + + + + + + {(eye) => } + + + {(notch) => } + + + + + {(arc) => ( + + + {(color, index) => ( + + )} + + + )} + + + + + + {(arc) => ( + + )} + + + + + + + + + + + + + + + + {(notif) => } + + + + + {(arc) => ( + + )} + + + + ) +} diff --git a/src/components/BloubBot.vue b/src/components/BloubBot.vue deleted file mode 100644 index 6816052..0000000 --- a/src/components/BloubBot.vue +++ /dev/null @@ -1,617 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/BotTile.tsx b/src/components/BotTile.tsx new file mode 100644 index 0000000..4704de2 --- /dev/null +++ b/src/components/BotTile.tsx @@ -0,0 +1,42 @@ +import type { JSX } from 'solid-js' +import BloubBot from './BloubBot' +import { DEFAULT_EXPRESSION } from '@/bot/expressions' +import { DEFAULT_COLOR, DEFAULT_SHAPE } from '@/bot/skins' +import type { StateId } from '@/bot/states' + +export interface BotTileProps { + label: string + selected: boolean + frozenAt: number + state?: StateId + shape?: string + color?: string + expression?: string + size?: number + onClick?: JSX.EventHandlerUnion +} + +export default function BotTile(props: BotTileProps) { + return ( + + + {props.label} + + ) +} diff --git a/src/components/BotTile.vue b/src/components/BotTile.vue deleted file mode 100644 index dd79a7d..0000000 --- a/src/components/BotTile.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - {{ label }} - - diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..87fc495 --- /dev/null +++ b/src/components/ConfirmDialog.tsx @@ -0,0 +1,48 @@ +import { createSignal } from 'solid-js' +import { t } from '@/i18n' +import { useModalDialog } from '@/ui/useModalDialog' + +export default function ConfirmDialog(props: { + title: string + detail: string + confirmLabel: string + open: boolean + onOpenChange: (value: boolean) => void + onConfirm: () => void +}) { + const [box, setBox] = createSignal() + useModalDialog(() => props.open, box) + + function confirm() { + props.onConfirm() + props.onOpenChange(false) + } + + return ( + props.onOpenChange(false)} + onCancel={(event) => { + event.preventDefault() + props.onOpenChange(false) + }} + > + + + {props.title} + {props.detail} + + + props.onOpenChange(false)}> + {t('dialog.cancel')} + + + {props.confirmLabel} + + + + + ) +} diff --git a/src/components/ConfirmDialog.vue b/src/components/ConfirmDialog.vue deleted file mode 100644 index bd89390..0000000 --- a/src/components/ConfirmDialog.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - {{ props.title }} - {{ props.detail }} - - - - - {{ t('dialog.cancel') }} - - - {{ props.confirmLabel }} - - - - - diff --git a/src/components/Customizer.tsx b/src/components/Customizer.tsx new file mode 100644 index 0000000..96e23a7 --- /dev/null +++ b/src/components/Customizer.tsx @@ -0,0 +1,80 @@ +import { For } from 'solid-js' +import BotTile from './BotTile' +import { EXPRESSIONS } from '@/bot/expressions' +import { COLORS, SHAPES } from '@/bot/skins' +import { t } from '@/i18n' + +export interface CustomizerProps { + shape: string + color: string + expression: string + onShapeChange: (value: string) => void + onColorChange: (value: string) => void + onExpressionChange: (value: string) => void +} + +const PREVIEW_AT = 1 + +export default function Customizer(props: CustomizerProps) { + return ( + + {t('panel.shape')} + + + {(shape) => ( + props.onShapeChange(shape.id)} + /> + )} + + + + {t('panel.expression')} + + + {(expression) => ( + props.onExpressionChange(expression.id)} + /> + )} + + + + {t('panel.color')} + + + {(color) => ( + props.onColorChange(color.id)} + > + + + )} + + + + ) +} diff --git a/src/components/Customizer.vue b/src/components/Customizer.vue deleted file mode 100644 index 02b2145..0000000 --- a/src/components/Customizer.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - - {{ t('panel.shape') }} - - - - - {{ t('panel.expression') }} - - - - - {{ t('panel.color') }} - - - - - - - - diff --git a/src/components/CycleDialog.tsx b/src/components/CycleDialog.tsx new file mode 100644 index 0000000..efb5d5d --- /dev/null +++ b/src/components/CycleDialog.tsx @@ -0,0 +1,100 @@ +import { For, Show, createEffect, createMemo, createSignal } from 'solid-js' +import { t } from '@/i18n' +import { + FONDS_GIF, + FORMATS_CYCLE, + cycleAccepteTransparence, + videoPossible, + type FondGif, + type FormatCycle +} from '@/ui/export' +import { useModalDialog } from '@/ui/useModalDialog' + +export default function CycleDialog(props: { + avancement: number | null + erreur: boolean + open: boolean + format: FormatCycle + fond: FondGif + onOpenChange: (value: boolean) => void + onFormatChange: (value: FormatCycle) => void + onFondChange: (value: FondGif) => void + onConfirm: () => void + onCancel: () => void +}) { + const [box, setBox] = createSignal() + useModalDialog(() => props.open, box) + const formats = FORMATS_CYCLE.filter((format) => format !== 'mp4' || videoPossible()) + const busy = () => props.avancement !== null + const percent = createMemo(() => Math.round((props.avancement ?? 0) * 100)) + + createEffect(() => { + if (!formats.includes(props.format)) props.onFormatChange(formats[0]!) + }) + + function confirm(event: SubmitEvent) { + event.preventDefault() + if (!busy()) props.onConfirm() + } + + function close() { + if (busy()) props.onCancel() + props.onOpenChange(false) + } + + return ( + props.onOpenChange(false)} onCancel={(event) => { event.preventDefault(); close() }}> + + + {t('timeline.export')} + {t('export.cycleDetail')} + + + {t('export.cycleFormat')} + + {(choice, index) => ( + + props.onFormatChange(choice)} /> + + {t(`export.cycle_${choice}`)} + {t(`export.cycle_${choice}_aide`)} + + + )} + + + + + {t('export.gifBackground')} + + {(choice) => ( + + props.onFondChange(choice)} /> + {t(`export.fond_${choice}`)} + + )} + + + + {t('export.failed')} + + {t('dialog.cancel')} + {props.erreur ? t('export.cycleReessayer') : t('export.gifConfirm')} + + } + > + + + + {t('export.cycleProgress')} {percent()} % + {t('dialog.cancel')} + + + + + + ) +} diff --git a/src/components/CycleDialog.vue b/src/components/CycleDialog.vue deleted file mode 100644 index 7eca8df..0000000 --- a/src/components/CycleDialog.vue +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - {{ t('timeline.export') }} - {{ t('export.cycleDetail') }} - - - - {{ t('export.cycleFormat') }} - - - - {{ t(`export.cycle_${choix}`) }} - {{ t(`export.cycle_${choix}_aide`) }} - - - - - - - {{ t('export.gifBackground') }} - - - {{ t(`export.fond_${choix}`) }} - - - - - - - {{ t('export.failed') }} - - - - - - - - - - {{ t('export.cycleProgress') }} {{ pourcent }} % - - - {{ t('dialog.cancel') }} - - - - - - - {{ t('dialog.cancel') }} - - - {{ erreur ? t('export.cycleReessayer') : t('export.gifConfirm') }} - - - - - diff --git a/src/components/CycleMenu.tsx b/src/components/CycleMenu.tsx new file mode 100644 index 0000000..0fb90d6 --- /dev/null +++ b/src/components/CycleMenu.tsx @@ -0,0 +1,53 @@ +import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js' +import type { Cycle } from '@/bot/cycles' +import { nomDeCycle, t } from '@/i18n' + +export default function CycleMenu(props: { + cycles: Cycle[] + current: Cycle + activeId: string + onActiveIdChange: (id: string) => void + onCreate: () => void + onRemove: (id: string) => void + onRename: (id: string) => void +}) { + const [open, setOpen] = createSignal(false) + let root!: HTMLDivElement + const outside = (event: PointerEvent) => { + if (!root.contains(event.target as Node)) setOpen(false) + } + createEffect(() => open() ? window.addEventListener('pointerdown', outside) : window.removeEventListener('pointerdown', outside)) + onCleanup(() => window.removeEventListener('pointerdown', outside)) + + return ( + event.key === 'Escape' && setOpen(false)}> + setOpen(!open())}> + {nomDeCycle(props.current)} + + + + + + {(cycle) => ( + + { props.onActiveIdChange(cycle.id); setOpen(false) }}> + {cycle.id === props.activeId ? '✓' : ''}{nomDeCycle(cycle)} + + { setOpen(false); props.onRename(cycle.id) }}> + + + 1}> + { setOpen(false); props.onRemove(cycle.id) }}> + + + + + )} + + + { setOpen(false); props.onCreate() }}>+{t('cycles.menuNew')} + + + + ) +} diff --git a/src/components/CycleMenu.vue b/src/components/CycleMenu.vue deleted file mode 100644 index c76f46c..0000000 --- a/src/components/CycleMenu.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - {{ nomDeCycle(current) }} - - - - - - - - - - {{ c.id === activeId ? '✓' : '' }} - {{ nomDeCycle(c) }} - - - - - - - - - - - - - - - - - - - + - {{ t('cycles.menuNew') }} - - - - diff --git a/src/components/ExportBar.tsx b/src/components/ExportBar.tsx new file mode 100644 index 0000000..c8defe1 --- /dev/null +++ b/src/components/ExportBar.tsx @@ -0,0 +1,59 @@ +import { For, Show, createEffect, createMemo, createSignal, onCleanup } from 'solid-js' +import { t } from '@/i18n' +import { copiePossible } from '@/ui/capture' +import { ACTIONS, ACTION_DEFAUT, type ActionId, type EtatExport } from '@/ui/export' + +export default function ExportBar(props: { etat: EtatExport; onExport: (id: ActionId) => void; class?: string }) { + const [open, setOpen] = createSignal(false) + let root!: HTMLDivElement + const actions = ACTIONS.filter((action) => action.mode !== 'copieImage' || copiePossible()) + const busy = () => props.etat === 'occupe' + const confirmed = () => props.etat === 'exporte' || props.etat === 'copie' + const label = createMemo(() => { + if (props.etat === 'exporte') return t('export.done') + if (props.etat === 'copie') return t('export.copied') + if (props.etat === 'erreur') return t('export.failed') + return t('export.action') + }) + const outside = (event: PointerEvent) => { + if (!root.contains(event.target as Node)) setOpen(false) + } + createEffect(() => open() ? window.addEventListener('pointerdown', outside) : window.removeEventListener('pointerdown', outside)) + onCleanup(() => window.removeEventListener('pointerdown', outside)) + + function launch(id: ActionId) { + setOpen(false) + props.onExport(id) + } + + const Download = (iconProps: { size: number }) => ( + + ) + + return ( + event.key === 'Escape' && setOpen(false)}> + + launch(ACTION_DEFAUT)}> + }> + {label()} + + + setOpen(!open())}> + + + + + + + {(action) => ( + launch(action.id)}> + }> + {t(`export.${action.id}`)} + + )} + + + + + ) +} diff --git a/src/components/ExportBar.vue b/src/components/ExportBar.vue deleted file mode 100644 index 14814ae..0000000 --- a/src/components/ExportBar.vue +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - {{ libelle }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ t(`export.${action.id}`) }} - - - - diff --git a/src/components/GifDialog.tsx b/src/components/GifDialog.tsx new file mode 100644 index 0000000..f61358d --- /dev/null +++ b/src/components/GifDialog.tsx @@ -0,0 +1,50 @@ +import { For, createSignal } from 'solid-js' +import { t } from '@/i18n' +import { FONDS_GIF, type FondGif } from '@/ui/export' +import { useModalDialog } from '@/ui/useModalDialog' + +export default function GifDialog(props: { + open: boolean + fond: FondGif + onOpenChange: (value: boolean) => void + onFondChange: (value: FondGif) => void + onConfirm: () => void +}) { + const [box, setBox] = createSignal() + useModalDialog(() => props.open, box) + + function confirm(event: SubmitEvent) { + event.preventDefault() + props.onConfirm() + props.onOpenChange(false) + } + + return ( + props.onOpenChange(false)} onCancel={(event) => { event.preventDefault(); props.onOpenChange(false) }}> + + + {t('export.gifTitle')} + {t('export.gifDetail')} + + + {t('export.gifBackground')} + + {(choice, index) => ( + + props.onFondChange(choice)} /> + + {t(`export.fond_${choice}`)} + {t(`export.fond_${choice}_aide`)} + + + )} + + + + props.onOpenChange(false)}>{t('dialog.cancel')} + {t('export.gifConfirm')} + + + + ) +} diff --git a/src/components/GifDialog.vue b/src/components/GifDialog.vue deleted file mode 100644 index 33b4787..0000000 --- a/src/components/GifDialog.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - {{ t('export.gifTitle') }} - {{ t('export.gifDetail') }} - - - - {{ t('export.gifBackground') }} - - - - {{ t(`export.fond_${choix}`) }} - {{ t(`export.fond_${choix}_aide`) }} - - - - - - - {{ t('dialog.cancel') }} - - - {{ t('export.gifConfirm') }} - - - - - diff --git a/src/components/NameDialog.tsx b/src/components/NameDialog.tsx new file mode 100644 index 0000000..54935d7 --- /dev/null +++ b/src/components/NameDialog.tsx @@ -0,0 +1,48 @@ +import { createEffect, createSignal, on } from 'solid-js' +import { t } from '@/i18n' +import { useModalDialog } from '@/ui/useModalDialog' + +export default function NameDialog(props: { + title: string + label: string + submitLabel: string + open: boolean + value: string + onOpenChange: (value: boolean) => void + onSubmit: (name: string) => void +}) { + const [box, setBox] = createSignal() + const [field, setField] = createSignal() + const [draft, setDraft] = createSignal('') + useModalDialog(() => props.open, box) + + createEffect(on(() => props.open, (open) => { + if (!open) return + setDraft(props.value) + queueMicrotask(() => field()?.select()) + })) + + function submit(event: SubmitEvent) { + event.preventDefault() + const clean = draft().trim() + if (!clean) return field()?.focus() + props.onSubmit(clean) + props.onOpenChange(false) + } + + return ( + props.onOpenChange(false)} onCancel={(event) => { event.preventDefault(); props.onOpenChange(false) }}> + + {props.title} + + {props.label} + setDraft(event.currentTarget.value)} class="h-9 rounded-lg bg-black/5 px-2.5 text-sm text-[var(--ink)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--ink)]" type="text" maxlength="40" required /> + + + props.onOpenChange(false)}>{t('dialog.cancel')} + {props.submitLabel} + + + + ) +} diff --git a/src/components/NameDialog.vue b/src/components/NameDialog.vue deleted file mode 100644 index afd5ff9..0000000 --- a/src/components/NameDialog.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - {{ props.title }} - - - {{ props.label }} - - - - - - {{ t('dialog.cancel') }} - - - {{ props.submitLabel }} - - - - - diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx new file mode 100644 index 0000000..d857017 --- /dev/null +++ b/src/components/Settings.tsx @@ -0,0 +1,47 @@ +import { For, Show, createMemo } from 'solid-js' +import { langue, LANGUES, setLangue, t } from '@/i18n' + +const X = 'https://x.com/worlz_' +const GITHUB = 'https://github.com/jeremy-prt/bloub' + +export default function Settings() { + const credits = createMemo(() => { + const [before = '', after = ''] = t('settings.credits').split('{name}') + return { before, after } + }) + + function onKeyboard(event: KeyboardEvent, index: number) { + const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key] + if (!step) return + event.preventDefault() + const target = LANGUES[(index + step + LANGUES.length) % LANGUES.length]! + setLangue(target.id) + const buttons = (event.currentTarget as HTMLElement).parentElement?.children + const next = buttons?.[LANGUES.indexOf(target)] + if (next instanceof HTMLElement) next.focus() + } + + return ( + + {t('settings.language')} + + + {(language, index) => ( + onKeyboard(event, index())} class={`flex cursor-pointer items-center gap-2.5 rounded-xl border px-3 py-2 text-left text-sm transition ${language.id === langue() ? 'border-[var(--ink)] bg-white font-medium' : 'border-[var(--line)] text-[var(--muted)] hover:border-[var(--muted)] hover:text-[var(--ink)]'}`} onClick={() => setLangue(language.id)}> + {language.emoji} + {language.nom} + + + )} + + + {t('settings.about')} + + + {t('settings.github')} + + + {credits().before}Jérémy{credits().after} + + ) +} diff --git a/src/components/Settings.vue b/src/components/Settings.vue deleted file mode 100644 index d98dee4..0000000 --- a/src/components/Settings.vue +++ /dev/null @@ -1,154 +0,0 @@ - - - - - {{ t('settings.language') }} - - - - - - {{ l.emoji }} - {{ l.nom }} - - - - - - - {{ t('settings.about') }} - - - - - - - {{ t('settings.github') }} - - - - - - - - {{ credits.avant - }}Jérémy{{ credits.apres }} - - - diff --git a/src/components/SideRail.tsx b/src/components/SideRail.tsx new file mode 100644 index 0000000..b638e2f --- /dev/null +++ b/src/components/SideRail.tsx @@ -0,0 +1,34 @@ +import { For, Match, Switch, createMemo, createSignal } from 'solid-js' +import { t } from '@/i18n' + +export type ViewId = 'animations' | 'personnaliser' | 'reglages' + +export default function SideRail(props: { view: ViewId; onViewChange: (view: ViewId) => void; class?: string; inert?: boolean }) { + const items = createMemo>(() => [ + { id: 'personnaliser', label: t('rail.customize') }, + { id: 'animations', label: t('rail.animations') }, + { id: 'reglages', label: t('rail.settings') } + ]) + const [muted, setMuted] = createSignal(null) + + return ( + + + + {(item) => ( + setMuted(null)}> + setMuted(item.id)} onClick={() => props.onViewChange(item.id)}> + + + + + + + {item.label} + + )} + + + + ) +} diff --git a/src/components/SideRail.vue b/src/components/SideRail.vue deleted file mode 100644 index f04e231..0000000 --- a/src/components/SideRail.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ item.label }} - - - - - diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx new file mode 100644 index 0000000..8c7802b --- /dev/null +++ b/src/components/Timeline.tsx @@ -0,0 +1,110 @@ +import { createMemo, createSignal } from 'solid-js' +import ConfirmDialog from './ConfirmDialog' +import CycleMenu from './CycleMenu' +import NameDialog from './NameDialog' +import TimelineTrack from './TimelineTrack' +import ZoomSlider from './ZoomSlider' +import { blocksWith, makeBlock, nextCycleId, offsetOf, totalDuration, uniqueName, type Block, type Cycle } from '@/bot/cycles' +import type { StateId } from '@/bot/states' +import { MAX_ZOOM, MIN_ZOOM, mmss } from '@/ui/timeline' +import { nomDeCycle, pluriel, t } from '@/i18n' + +export default function Timeline(props: { + elapsed: number + shape: string + color: string + expression: string + cycles: Cycle[] + activeId: string + block: number + playing: boolean + onCyclesChange: (cycles: Cycle[]) => void + onActiveIdChange: (id: string) => void + onBlockChange: (block: number) => void + onPlayingChange: (playing: boolean) => void + onSeek: (seconds: number) => void + onPreview: () => void + onExport: () => void +}) { + const [zoom, setZoom] = createSignal(1) + const cycle = createMemo(() => props.cycles.find((item) => item.id === props.activeId) ?? props.cycles[0]!) + const blocks = () => cycle().blocks + const total = createMemo(() => totalDuration(blocks())) + const at = createMemo(() => offsetOf(blocks(), props.block) + props.elapsed) + const [naming, setNaming] = createSignal<{ mode: 'create' | 'rename'; id?: string } | null>(null) + const [nameDraft, setNameDraft] = createSignal('') + const [nameOpen, setNameOpen] = createSignal(false) + const [removing, setRemoving] = createSignal(null) + const [confirmOpen, setConfirmOpen] = createSignal(false) + + function edit(next: Partial) { + props.onCyclesChange(props.cycles.map((item) => item.id === cycle().id ? { ...item, ...next } : item)) + } + function select(id: string) { + props.onActiveIdChange(id) + props.onBlockChange(0) + } + function askCreate() { + setNaming({ mode: 'create' }) + setNameDraft(uniqueName(t('cycles.newName'), props.cycles)) + setNameOpen(true) + } + function askRename(id: string) { + setNaming({ mode: 'rename', id }) + const target = props.cycles.find((item) => item.id === id) + setNameDraft(target ? nomDeCycle(target) : '') + setNameOpen(true) + } + function onNamed(name: string) { + const request = naming() + setNaming(null) + if (!request) return + if (request.mode === 'create') { + const next: Cycle = { id: nextCycleId(props.cycles), name: uniqueName(name, props.cycles), blocks: [makeBlock('idle')] } + props.onCyclesChange([...props.cycles, next]) + select(next.id) + return + } + const others = props.cycles.filter((item) => item.id !== request.id) + const unique = uniqueName(name, others) + props.onCyclesChange(props.cycles.map((item) => item.id === request.id ? { ...item, name: unique } : item)) + } + function askRemove(id: string) { + setRemoving(props.cycles.find((item) => item.id === id) ?? null) + setConfirmOpen(true) + } + function onRemove() { + const target = removing() + setRemoving(null) + if (!target) return + const rest = props.cycles.filter((item) => item.id !== target.id) + props.onCyclesChange(rest) + if (target.id === props.activeId) select(rest[0]!.id) + } + + return ( + + + {mmss(at())} + props.onPlayingChange(!props.playing)}> + {props.playing ? : } + + {mmss(total())} + + + + + {t('timeline.export')} + + edit({ blocks: next })} onBlockChange={props.onBlockChange} onZoomChange={setZoom} onAdd={(state: StateId) => edit({ blocks: blocksWith(blocks(), state) })} onSeek={props.onSeek} /> + + + {mmss(at())} / {mmss(total())} + {t('timeline.preview')} + + + + + + ) +} diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue deleted file mode 100644 index 3f96ee9..0000000 --- a/src/components/Timeline.vue +++ /dev/null @@ -1,324 +0,0 @@ - - - - - - - - {{ mmss(at) }} - - - - - - - - - - - - - {{ mmss(total) }} - - - - - - - - - - - - - - - - - - {{ t('timeline.export') }} - - - - edit({ blocks: b })" - @add="(s: StateId) => edit({ blocks: blocksWith(blocks, s) })" - @seek="emit('seek', $event)" - /> - - - - - - - {{ mmss(at) }} / {{ mmss(total) }} - - - - - - - - - - - - - - - {{ t('timeline.preview') }} - - - - - - - - - - - diff --git a/src/components/TimelineTrack.tsx b/src/components/TimelineTrack.tsx new file mode 100644 index 0000000..4d8a943 --- /dev/null +++ b/src/components/TimelineTrack.tsx @@ -0,0 +1,199 @@ +import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' +import BlockPicker from './BlockPicker' +import BloubBot from './BloubBot' +import { clampDuration, moveBlock, offsetOf, STEP, totalDuration, type Block } from '@/bot/cycles' +import { POSES, type StateId } from '@/bot/states' +import { BASE_SCALE, clampZoom, ticksFor } from '@/ui/timeline' +import { secondes, secondesCourtes, t } from '@/i18n' + +type Drag = { from: number; to: number; startX: number; dx: number; moved: boolean } +type Resize = { index: number; startX: number; startDuration: number } + +export default function TimelineTrack(props: { + blocks: Block[] + elapsed: number + shape: string + color: string + expression: string + block: number + zoom: number + onBlocksChange: (blocks: Block[]) => void + onBlockChange: (block: number) => void + onZoomChange: (zoom: number) => void + onSeek: (seconds: number) => void + onAdd: (state: StateId) => void +}) { + const scale = createMemo(() => BASE_SCALE * props.zoom) + const total = createMemo(() => totalDuration(props.blocks)) + const at = createMemo(() => offsetOf(props.blocks, props.block) + props.elapsed) + const ticks = createMemo(() => ticksFor(total(), scale())) + let track!: HTMLDivElement + const [overflow, setOverflow] = createSignal({ left: false, right: false }) + const [scrolled, setScrolled] = createSignal(0) + const [drag, setDrag] = createSignal(null) + const [resize, setResize] = createSignal(null) + const [scrubbing, setScrubbing] = createSignal(false) + let anchorX: number | null = null + + const width = (index: number) => props.blocks[index]!.duration * scale() + const label = (index: number) => t(`states.${props.blocks[index]!.state}`) + + function onScroll() { + if (!track) return + setScrolled(track.scrollLeft) + setOverflow({ left: track.scrollLeft > 4, right: track.scrollLeft + track.clientWidth < track.scrollWidth - 4 }) + } + + function setZoom(next: number, clientX?: number) { + anchorX = clientX ?? null + props.onZoomChange(clampZoom(next)) + } + + let oldScale = scale() + createEffect(on(scale, (now) => { + if (!track) return + const box = track.getBoundingClientRect() + const x = (anchorX ?? box.left + track.clientWidth / 2) - box.left + const second = (track.scrollLeft + x) / oldScale + oldScale = now + anchorX = null + queueMicrotask(() => { track.scrollLeft = second * now - x; onScroll() }) + }, { defer: true })) + + function onWheel(event: WheelEvent) { + const unit = event.deltaMode === 1 ? 16 : 1 + if (event.ctrlKey || event.metaKey) { + event.preventDefault() + setZoom(props.zoom * Math.exp((-event.deltaY * unit) / 180), event.clientX) + return + } + if (track.scrollWidth <= track.clientWidth) return + const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY + if (!delta) return + event.preventDefault() + track.scrollLeft += delta * unit + } + + onMount(onScroll) + createEffect(() => { total(); props.blocks; queueMicrotask(onScroll) }) + createEffect(on(() => props.block, (block) => { + const x = offsetOf(props.blocks, block) * scale() + if (x < track.scrollLeft || x + width(block) > track.scrollLeft + track.clientWidth) track.scrollTo({ left: Math.max(0, x - 24), behavior: 'smooth' }) + }, { defer: true })) + + function removeBlock(index: number) { + if (props.blocks.length < 2) return + props.onBlocksChange(props.blocks.filter((_, i) => i !== index)) + if (index < props.block) props.onBlockChange(props.block - 1) + else if (props.block >= props.blocks.length - 1) props.onBlockChange(props.blocks.length - 2) + } + + function setDuration(index: number, wanted: number) { + const block = props.blocks[index] + if (!block) return + const duration = clampDuration(block.state, wanted) + if (duration !== block.duration) props.onBlocksChange(props.blocks.map((old, i) => i === index ? { ...old, duration } : old)) + } + + function shiftOf(index: number) { + const current = drag() + if (!current?.moved) return 0 + if (index === current.from) return current.dx + const w = width(current.from) + if (current.to > current.from && index > current.from && index <= current.to) return -w + if (current.to < current.from && index >= current.to && index < current.from) return w + return 0 + } + + function indexAt(seconds: number) { + let sum = 0 + for (let i = 0; i < props.blocks.length; i++) { + sum += props.blocks[i]!.duration + if (seconds < sum) return i + } + return props.blocks.length - 1 + } + + function pointerSeconds(event: PointerEvent) { + const box = track.getBoundingClientRect() + return (event.clientX - box.left + track.scrollLeft) / scale() + } + + function onBlockDown(index: number, event: PointerEvent) { + ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId) + setDrag({ from: index, to: index, startX: event.clientX, dx: 0, moved: false }) + } + + function onBlockMove(event: PointerEvent) { + const current = drag() + if (!current || (!current.moved && Math.abs(event.clientX - current.startX) <= 4)) return + setDrag({ ...current, moved: true, dx: event.clientX - current.startX, to: Math.max(0, indexAt(pointerSeconds(event))) }) + } + + function onBlockUp(index: number) { + const current = drag() + setDrag(null) + if (!current) return + if (!current.moved) return props.onBlockChange(index) + if (current.to === current.from) return + props.onBlocksChange(moveBlock(props.blocks, current.from, current.to)) + if (props.block === current.from) props.onBlockChange(current.to) + } + + function onResizeDown(index: number, event: PointerEvent) { + ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId) + setResize({ index, startX: event.clientX, startDuration: props.blocks[index]!.duration }) + } + + function onResizeMove(event: PointerEvent) { + const current = resize() + if (current) setDuration(current.index, current.startDuration + (event.clientX - current.startX) / scale()) + } + + async function onCardKey(index: number, event: KeyboardEvent) { + const direction = event.key === 'ArrowLeft' ? -1 : event.key === 'ArrowRight' ? 1 : 0 + if (!direction) return + event.preventDefault() + if (!event.altKey) return props.onSeek(Math.max(0, Math.min(total() - 0.001, at() + direction * STEP))) + const target = index + direction + if (target < 0 || target >= props.blocks.length) return + props.onBlocksChange(moveBlock(props.blocks, index, target)) + props.onBlockChange(target) + queueMicrotask(() => track.querySelectorAll('[data-carte]')[target]?.focus()) + } + + function scrubTo(event: PointerEvent) { + props.onSeek(Math.max(0, Math.min(total() - 0.001, pointerSeconds(event)))) + } + + return ( + + + + { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); setScrubbing(true); scrubTo(event) }} onPointerMove={(event) => scrubbing() && scrubTo(event)} onPointerUp={() => setScrubbing(false)} onPointerCancel={() => setScrubbing(false)}> + {(tick) => {secondesCourtes(tick.t, Number.isInteger(tick.t) ? 0 : 1)}} + + + + {(item, index) => ( + + onBlockDown(index(), event)} onPointerMove={onBlockMove} onPointerUp={() => onBlockUp(index())} onPointerCancel={() => setDrag(null)} data-carte aria-keyshortcuts="Alt+ArrowLeft Alt+ArrowRight ArrowLeft ArrowRight" onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); props.onBlockChange(index()) } else void onCardKey(index(), event) }}> + 44}> + 50}>{secondes(item.duration)} + + onResizeDown(index(), event)} onPointerMove={onResizeMove} onPointerUp={() => setResize(null)} onPointerCancel={() => setResize(null)} onKeyDown={(event) => { if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { event.preventDefault(); setDuration(index(), item.duration + (event.key === 'ArrowLeft' ? -STEP : STEP)) } }} /> + 1}> removeBlock(index())}> + + )} + + + + + + + {secondes(at())} + + + + ) +} diff --git a/src/components/TimelineTrack.vue b/src/components/TimelineTrack.vue deleted file mode 100644 index 7ac3fd1..0000000 --- a/src/components/TimelineTrack.vue +++ /dev/null @@ -1,516 +0,0 @@ - - - - - - - - - - - - - {{ secondesCourtes(tick.t, Number.isInteger(tick.t) ? 0 : 1) }} - - - - - - - - - - - - - - {{ secondes(b.duration) }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ exact }} - - - - - - - - diff --git a/src/components/ZoomSlider.tsx b/src/components/ZoomSlider.tsx new file mode 100644 index 0000000..c4c0d71 --- /dev/null +++ b/src/components/ZoomSlider.tsx @@ -0,0 +1,29 @@ +import { pourcentage, t } from '@/i18n' + +export default function ZoomSlider(props: { + zoom: number + min: number + max: number + onZoomChange: (value: number) => void +}) { + return ( + + + props.onZoomChange(Number(event.currentTarget.value))} + /> + + + {pourcentage(props.zoom)} + + + ) +} diff --git a/src/components/ZoomSlider.vue b/src/components/ZoomSlider.vue deleted file mode 100644 index c983e15..0000000 --- a/src/components/ZoomSlider.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - {{ percent }} - - diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 3731817..19440f4 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,4 +1,11 @@ -import { computed, ref, watchEffect } from 'vue' +import { + createEffect, + createMemo, + createRoot, + createSignal, + type Accessor, + type Setter +} from 'solid-js' import { ecris, lis } from '@/ui/stockage' import { formePlurielle, interpoler } from './format' import { choisirLangue, estLangue, type Langue, tagDe } from './langues' @@ -25,9 +32,27 @@ type Chemins = { export type Cle = Chemins -const courante = ref( - choisirLangue(lis('langue'), navigator.languages ?? [navigator.language]) -) +const languesNavigateur = + typeof navigator === 'undefined' ? ['fr'] : (navigator.languages ?? [navigator.language]) +let courante!: Accessor +let setCourante!: Setter +let dictionnaire!: Accessor +let tag!: Accessor + +// Singleton reactif de l'application. La racine vit aussi longtemps que le module. +createRoot(() => { + ;[courante, setCourante] = createSignal( + choisirLangue(lis('langue'), languesNavigateur) + ) + dictionnaire = createMemo(() => dictionnaires[courante()]) + tag = createMemo(() => tagDe(courante())) + + createEffect(() => { + if (typeof document === 'undefined') return + document.documentElement.lang = tag() + document.title = t('app.title') + }) +}) /** * Langue courante, en lecture et en ecriture (`v-model` compris). @@ -37,38 +62,22 @@ const courante = ref( * l'anglais pour toujours, alors que la detection doit rester une hypothese * qu'on refait a chaque visite tant que l'utilisateur n'a pas tranche. */ -export const langue = computed({ - get: () => courante.value, - set: (valeur) => { - if (!estLangue(valeur)) return - courante.value = valeur - ecris('langue', valeur) - } -}) - -const dictionnaire = computed(() => dictionnaires[courante.value]) - -/** Etiquette BCP 47 de la langue courante, pour `Intl` et l'attribut `lang`. */ -const tag = computed(() => tagDe(courante.value)) +export const langue = courante -/** - * L'attribut `lang` du document suit la langue : c'est lui qui fait choisir la - * bonne voix au lecteur d'ecran et les bonnes regles de cesure. Le titre de - * l'onglet suit aussi — `index.html` ne peut en porter qu'un, statique. - */ -watchEffect(() => { - document.documentElement.lang = tag.value - document.title = t('app.title') -}) +export function setLangue(valeur: Langue) { + if (!estLangue(valeur)) return + setCourante(valeur) + ecris('langue', valeur) +} /** Les formateurs sont chers a construire et relus a chaque image de la piste. */ const formateurs = new Map() function formateur(cle: string, options: Intl.NumberFormatOptions): Intl.NumberFormat { - const memo = `${tag.value}:${cle}` + const memo = `${tag()}:${cle}` let f = formateurs.get(memo) if (!f) { - f = new Intl.NumberFormat(tag.value, options) + f = new Intl.NumberFormat(tag(), options) formateurs.set(memo, f) } return f @@ -99,7 +108,7 @@ export function pourcentage(fraction: number): string { function brut(cle: Cle): string { const noeud = cle .split('.') - .reduce((n, k) => (n as Record)[k], dictionnaire.value) + .reduce((n, k) => (n as Record)[k], dictionnaire()) return noeud as string } @@ -110,7 +119,7 @@ export function t(cle: Cle, valeurs?: Record): string { /** Pluriel : `n` est toujours disponible comme `{n}` dans le gabarit. */ export function pluriel(cle: Cle, n: number, valeurs?: Record): string { - return interpoler(formePlurielle(brut(cle), n, tag.value), { n, ...valeurs }) + return interpoler(formePlurielle(brut(cle), n, tag()), { n, ...valeurs }) } /** diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..32d8e0f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,10 @@ +export { default as BloubBot } from './components/BloubBot' +export type { BloubBotProps, BloubBotRef } from './components/BloubBot' +export * from './bot/engine' +export * from './bot/cycles' +export * from './bot/states' +export * from './bot/skins' +export * from './bot/expressions' +export { RAYON, DEMI_VIEWBOX } from './bot/repere' +export type { GazeScript, Aim } from './ui/gaze' +export { lookTarget, tourLook, TOUR_TIME, TURN_TIME } from './ui/gaze' diff --git a/src/main.ts b/src/main.ts index fdbdce5..9e47422 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,7 @@ -import { createApp } from 'vue' -import App from './App.vue' +import { render } from 'solid-js/web' +import App from './App' import './styles.css' -createApp(App).mount('#app') +const root = document.getElementById('app') +if (!root) throw new Error('element #app introuvable') +render(() => App(), root) diff --git a/src/ui/capture.ts b/src/ui/capture.ts index a3500e6..9dc6dc2 100644 --- a/src/ui/capture.ts +++ b/src/ui/capture.ts @@ -10,8 +10,9 @@ * forme porte son `fill` en hex. */ -import { createApp, h, nextTick, ref } from 'vue' -import BloubBot from '@/components/BloubBot.vue' +import { createComponent, createSignal } from 'solid-js' +import { render } from 'solid-js/web' +import BloubBot, { type BloubBotRef } from '@/components/BloubBot' import type { Block } from '@/bot/cycles' import { gifAnime, gifIndexe, indexe, nouvellePalette, recense, svgAnime } from './anime' import { arrete, DEMI_ECRAN, sansCommentaires, viewBoxExport } from './export' @@ -280,25 +281,32 @@ export async function sequenceDuBot( hote.style.cssText = 'position:fixed;left:-99999px;top:0;width:0;height:0;overflow:hidden' document.body.appendChild(hote) - const date = ref(0) - const app = createApp({ - render: () => - h(BloubBot, { ...reglages, size: taille, frozenAt: date.value, ...(paper ? { paper } : {}) }) - }) - app.mount(hote) + const [date, setDate] = createSignal(0) + const dispose = render( + () => + createComponent(BloubBot, { + ...reglages, + size: taille, + get frozenAt() { + return date() + }, + ...(paper ? { paper } : {}) + }), + hote + ) try { const out: T[] = [] for (let i = 0; i < nombre; i++) { - date.value = i * pas - await nextTick() + setDate(i * pas) + await Promise.resolve() const svg = hote.querySelector('svg') if (!svg) throw new Error('bot hors ecran non rendu') out.push(await lis(svg, i)) } return out } finally { - app.unmount() + dispose() hote.remove() } } @@ -331,10 +339,10 @@ export async function ouvreCycle( hote.style.cssText = 'position:fixed;left:-99999px;top:0;width:0;height:0;overflow:hidden' document.body.appendChild(hote) - const bot = ref<{ rendAt: (t: number) => void } | null>(null) - const app = createApp({ - render: () => - h(BloubBot, { + let bot: BloubBotRef | undefined + const dispose = render( + () => + createComponent(BloubBot, { ...reglages, size: taille, cycle: blocs, @@ -346,31 +354,33 @@ export async function ouvreCycle( * meme precaution qu'a l'ecran, ou `state` est amorce sur le bloc courant * « pour ne pas entrer en morphant depuis un etat qui n'a jamais ete * affiche » (cf. `App.vue`). - */ + */ state: blocs[0]?.state ?? 'idle', frozenAt: 0, - ref: bot, + ref: (value) => { + bot = value + }, ...(paper ? { paper } : {}) - }) - }) - app.mount(hote) - await nextTick() + }), + hote + ) + await Promise.resolve() const svg = hote.querySelector('svg') - if (!svg || !bot.value) { - app.unmount() + if (!svg || !bot) { + dispose() hote.remove() throw new Error('bot hors ecran non rendu') } return { rendre: async (t: number) => { - bot.value!.rendAt(t) - await nextTick() + bot!.renderAt(t) + await Promise.resolve() return svg }, ferme: () => { - app.unmount() + dispose() hote.remove() } } diff --git a/src/ui/stockage.ts b/src/ui/stockage.ts index fbd5674..775b662 100644 --- a/src/ui/stockage.ts +++ b/src/ui/stockage.ts @@ -35,6 +35,7 @@ export function cle(nom: NomStocke): string { */ export function lis(nom: NomStocke): string | null { try { + if (typeof localStorage === 'undefined') return null return localStorage.getItem(cle(nom)) } catch { return null @@ -50,6 +51,7 @@ export function lis(nom: NomStocke): string | null { */ export function ecris(nom: NomStocke, valeur: string) { try { + if (typeof localStorage === 'undefined') return localStorage.setItem(cle(nom), valeur) } catch { // stockage refuse ou plein : on continue sans persister diff --git a/src/ui/useModalDialog.ts b/src/ui/useModalDialog.ts index a3326db..a257496 100644 --- a/src/ui/useModalDialog.ts +++ b/src/ui/useModalDialog.ts @@ -1,4 +1,4 @@ -import { onBeforeUnmount, watch, type Ref, type ShallowRef } from 'vue' +import { createEffect, onCleanup, type Accessor } from 'solid-js' /** * Pilote un `` natif ouvert en modal depuis un booleen. @@ -14,19 +14,20 @@ import { onBeforeUnmount, watch, type Ref, type ShallowRef } from 'vue' * `@close="open = false" @cancel.prevent="open = false"`. */ export function useModalDialog( - open: Ref, - el: Readonly> + open: Accessor, + el: Accessor ) { - watch(open, (on) => { - const dialog = el.value + createEffect(() => { + const on = open() + const dialog = el() if (!dialog) return - if (on) dialog.showModal() + if (on && !dialog.open) dialog.showModal() else if (dialog.open) dialog.close() }) // le composant peut disparaitre alors que la boite est ouverte (changement de // vue) : un `` laisse en modal bloquerait la page entiere - onBeforeUnmount(() => { - if (el.value?.open) el.value.close() + onCleanup(() => { + if (el()?.open) el()!.close() }) } diff --git a/tests/package-consumer.tsx b/tests/package-consumer.tsx new file mode 100644 index 0000000..6d6e82b --- /dev/null +++ b/tests/package-consumer.tsx @@ -0,0 +1,25 @@ +import { createSignal } from 'solid-js' +import { + BloubBot, + BotEngine, + defaultCycle, + type BloubBotRef, + type StateId +} from 'bloub' + +const [playing, setPlaying] = createSignal(false) +let bot: BloubBotRef | undefined +const state: StateId = 'orbit' + +export const packageConsumer = ( + { bot = value }} + state={state} + cycle={defaultCycle().blocks} + playing={playing()} + onPlayingChange={setPlaying} + /> +) + +export const frame = new BotEngine().sample(0) +export const controller = () => bot diff --git a/tsconfig.json b/tsconfig.json index 2a4cded..6b797ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,8 @@ "verbatimModuleSyntax": true, "noEmit": true, "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["vite/client"], "strict": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, @@ -22,5 +24,5 @@ "@/*": ["./src/*"] } }, - "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"] + "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "vite.lib.config.ts", "vite.lib.ssr.config.ts"] } diff --git a/tsconfig.lib.json b/tsconfig.lib.json new file mode 100644 index 0000000..0af38f6 --- /dev/null +++ b/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "rootDir": "src", + "outDir": "dist/package/types" + }, + "include": ["src/index.ts"] +} diff --git a/tsconfig.package-test.json b/tsconfig.package-test.json new file mode 100644 index 0000000..66560e2 --- /dev/null +++ b/tsconfig.package-test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": {} + }, + "include": ["tests/package-consumer.tsx"] +} diff --git a/vite.config.ts b/vite.config.ts index faa4c21..010fd15 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,17 @@ import { fileURLToPath, URL } from 'node:url' import tailwindcss from '@tailwindcss/vite' -import vue from '@vitejs/plugin-vue' +import solid from 'vite-plugin-solid' import { defineConfig } from 'vite' export default defineConfig({ - plugins: [vue(), tailwindcss()], + plugins: [solid(), tailwindcss()], // Le port est ici et pas seulement dans `.claude/launch.json` : c'est celui que - // le README annonce, il doit donc valoir pour un `pnpm dev` nu. + // le README annonce, il doit donc valoir pour un `bun run dev` nu. server: { port: 5190 }, resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } + }, + build: { + outDir: 'dist/app' } }) diff --git a/vite.lib.config.ts b/vite.lib.config.ts new file mode 100644 index 0000000..d83e3a1 --- /dev/null +++ b/vite.lib.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath, URL } from 'node:url' +import solid from 'vite-plugin-solid' +import { defineConfig } from 'vite' + +export default defineConfig({ + publicDir: false, + plugins: [solid()], + resolve: { + alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } + }, + build: { + outDir: 'dist/package', + sourcemap: true, + lib: { + entry: 'src/index.ts', + formats: ['es'], + fileName: 'index' + }, + rollupOptions: { + external: [/^solid-js(?:\/|$)/] + } + } +}) diff --git a/vite.lib.ssr.config.ts b/vite.lib.ssr.config.ts new file mode 100644 index 0000000..5f2c209 --- /dev/null +++ b/vite.lib.ssr.config.ts @@ -0,0 +1,21 @@ +import { fileURLToPath, URL } from 'node:url' +import solid from 'vite-plugin-solid' +import { defineConfig } from 'vite' + +export default defineConfig({ + publicDir: false, + plugins: [solid({ ssr: true })], + resolve: { + alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } + }, + build: { + outDir: 'dist/package', + emptyOutDir: false, + sourcemap: true, + ssr: 'src/index.ts', + rollupOptions: { + external: [/^solid-js(?:\/|$)/], + output: { entryFileNames: 'server.js' } + } + } +}) diff --git a/vitest.config.ts b/vitest.config.ts index 2aebbd7..781bdb4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,25 +1,24 @@ import { fileURLToPath, URL } from 'node:url' -import vue from '@vitejs/plugin-vue' +import solid from 'vite-plugin-solid' import { defineConfig } from 'vitest/config' export default defineConfig({ /* - * Le plugin Vue est la pour un seul FICHIER, `ui/capture.test.ts` : il monte `BloubBot.vue` - * parce que le rendu exporte doit etre celui du composant, pas un second dessin monte a - * cote. Sans le plugin, importer `capture.ts` suffit a faire echouer la collecte. + * Le plugin Solid compile les composants utilises par les tests DOM. Le rendu exporte + * doit etre celui du composant, pas un second dessin monte a cote. * - * Il ne change rien aux autres : `src/bot/` n'importe aucun `.vue`, et l'environnement + * Il ne change rien aux autres : `src/bot/` n'importe aucun composant, et l'environnement * reste `node` par defaut — un DOM se demande fichier par fichier, en tete de celui qui en * a besoin (`// @vitest-environment happy-dom`). C'est ce qui garde la suite a quelques * secondes. */ - plugins: [vue()], + plugins: [solid()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, test: { globals: true, environment: 'node', - include: ['src/**/*.test.ts'] + include: ['src/**/*.test.{ts,tsx}'] } })
{NOM}
- {{ NOM }} -
{props.detail}
{{ props.detail }}
{t('export.cycleDetail')}
{t('export.failed')}
{t('export.cycleProgress')} {percent()} %
{{ t('export.cycleDetail') }}
- {{ t('export.failed') }} -
- {{ t('export.cycleProgress') }} {{ pourcent }} % -
{t('export.gifDetail')}
{{ t('export.gifDetail') }}
{credits().before}Jérémy{credits().after}
- {{ credits.avant - }}Jérémy{{ credits.apres }} -
{mmss(at())} / {mmss(total())}
- {{ mmss(at) }} / {{ mmss(total) }} -