From e3254388d6ae1f81b9b7eb77469b489e5877ded2 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Wed, 28 Oct 2026 23:44:09 -0400 Subject: [PATCH 01/58] feat: add the repo file source with GitHub and fixture adapters The renderer reads each pull request's changed files and file contents through one interface, `RepoFileSource`. `REPO_FILE_SOURCE` picks the adapter: `github` reads the REST API with `GITHUB_TOKEN`, and `fixture` (the default) reads a recorded snapshot. The GitHub adapter lists files with `compare/{base}...{head}`, which never changes for fixed SHAs. The PR files endpoint follows the PR's current head instead. `scripts/record-fixture.ts` records a snapshot through the GitHub adapter, so the snapshot matches what production sees, and checks each head file against its blob SHA. This commit also scaffolds the Astro 7 project: pnpm 11, Node 24, and `base: '/guides'`. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 7 + .node-version | 1 + astro.config.mjs | 12 + package.json | 38 + pnpm-lock.yaml | 4444 +++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 5 + scripts/record-fixture.ts | 83 + src/guide/schema.ts | 29 + src/repo-files/fixture.ts | 41 + src/repo-files/github.ts | 77 + src/repo-files/index.ts | 23 + src/repo-files/types.ts | 47 + tsconfig.json | 9 + 13 files changed, 4816 insertions(+) create mode 100644 .gitignore create mode 100644 .node-version create mode 100644 astro.config.mjs create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 scripts/record-fixture.ts create mode 100644 src/guide/schema.ts create mode 100644 src/repo-files/fixture.ts create mode 100644 src/repo-files/github.ts create mode 100644 src/repo-files/index.ts create mode 100644 src/repo-files/types.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4654e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.astro/ +test-results/ +playwright-report/ +# The deploy workflow checks out the content branch here. +content/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..89f31ea --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,12 @@ +// @ts-check +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + site: 'https://campdotdev.github.io', + base: '/guides', + output: 'static', + devToolbar: { enabled: false }, + markdown: { + shikiConfig: { themes: { light: 'github-light', dark: 'github-dark' } }, + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..99fa601 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "guides", + "private": true, + "type": "module", + "packageManager": "pnpm@11.15.1", + "engines": { + "node": ">=24" + }, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "check": "astro check", + "test": "playwright test", + "record-fixture": "node scripts/record-fixture.ts" + }, + "dependencies": { + "astro": "7.4.4", + "mdast-util-to-string": "^4.0.0", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "shiki": "^4.5.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.2.0" + }, + "devDependencies": { + "@astrojs/check": "^0.9.12", + "@playwright/test": "1.59.1", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/node": "^24.19.2", + "typescript": "^6.0.3", + "yaml": "^2.9.1" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..724911a --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4444 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + astro: + specifier: 7.4.4 + version: 7.4.4(@astrojs/markdown-remark@7.3.4)(@types/node@24.19.2)(yaml@2.9.1) + mdast-util-to-string: + specifier: ^4.0.0 + version: 4.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + rehype-stringify: + specifier: ^10.0.1 + version: 10.0.1 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + remark-parse: + specifier: ^11.0.0 + version: 11.0.0 + remark-rehype: + specifier: ^11.1.2 + version: 11.1.2 + shiki: + specifier: ^4.5.0 + version: 4.5.0 + unified: + specifier: ^11.0.5 + version: 11.0.5 + unist-util-visit: + specifier: ^5.2.0 + version: 5.2.0 + devDependencies: + '@astrojs/check': + specifier: ^0.9.12 + version: 0.9.12(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@6.0.3) + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@types/hast': + specifier: ^3.0.5 + version: 3.0.5 + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 + '@types/node': + specifier: ^24.19.2 + version: 24.19.2 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + yaml: + specifier: ^2.9.1 + version: 2.9.1 + +packages: + + '@astrojs/astro2tsx@0.1.3': + resolution: {integrity: sha512-GMDC7Eo0myBUA/QLBl0u+OcegJ8LiBhccmNZR34KmprQNP8gQUJlWlOOZWUsxNA2rNS6GuPYcGDyLoPnjmn4/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@astrojs/check@0.9.12': + resolution: {integrity: sha512-hazpqxIfZWj2VhyPyR0pOEQ/IhFDM/2nIApL96zMXABdD8NrUKYDmhxTy7h+drTDl9MQG/Sd1ET/4amadVJUUA==} + hasBin: true + peerDependencies: + typescript: ^5.0.0 || ^6.0.0 + + '@astrojs/compiler-binding-android-arm64@0.5.2': + resolution: {integrity: sha512-kckIxon6Rm/Bxzh9mGPYpRRqUbToUQoHptCmpMmE+Yt0Xumq8CqrKUidyYhrBsrHVO7bxuRzzWUUqvpf+iYceQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@astrojs/compiler-binding-darwin-arm64@0.5.2': + resolution: {integrity: sha512-1ZG5Ar3dD8HtodX9XH1mRmE0qBXZlvjYXSFfpgFwocqfWnvhFuqtGUNseL8wgfImiRMztIjpQqlogQFQQtgiag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@astrojs/compiler-binding-darwin-x64@0.5.2': + resolution: {integrity: sha512-+kl35S7hiHJ2cb7eujwEsPFYYuSgE11uEE2Z20lAJ+undpgulMcW+weQqFPOSSZccEKHOiLIY+kQHkb4e4092w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@astrojs/compiler-binding-linux-arm64-gnu@0.5.2': + resolution: {integrity: sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-arm64-musl@0.5.2': + resolution: {integrity: sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-linux-x64-gnu@0.5.2': + resolution: {integrity: sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-x64-musl@0.5.2': + resolution: {integrity: sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-wasm32-wasi@0.5.2': + resolution: {integrity: sha512-Z7p64doar1uvR5C9Xs6j1CV9lGlPVQX7OQ2aAtZ/xKC5aGS6GB13nyc2UfQXTQLvZrvVwsyaflTQy6ziY5Bekw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@astrojs/compiler-binding-win32-arm64-msvc@0.5.2': + resolution: {integrity: sha512-Tr13bh+ahTWsjXjzctK7pFpWV+dUU4FtBfETlR7gO+ueex0F+RDch7eRr0YfKysaRhk9tWm14lsCJ5uWyFPNHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@astrojs/compiler-binding-win32-x64-msvc@0.5.2': + resolution: {integrity: sha512-SfKkI+jOQYLh6wojPvVPV0lFfQnxisweN9B+qCzRvZp7DHAuGif8Y0WcC5PUgN4jb8VKy57LggQDtggR46Du1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@astrojs/compiler-binding@0.5.2': + resolution: {integrity: sha512-lEn76+0JcMQ5RDXU0wSMWJg2ZppiZvSULpghC29cfv4o9bhvWFDbQjsCtvjjXN9iVLFQ7fL3wTR2L2UU13mP8g==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@astrojs/compiler-rs@0.5.2': + resolution: {integrity: sha512-IvwYLB8t1M86rKO4oPdBi9GP4gzXcgrVlNgocjwos1jeeUuEIV8d9Crn1PvYD/5IKteWOxpIHomf7e8iYlkE+Q==} + engines: {node: '>=22.12.0'} + + '@astrojs/internal-helpers@0.13.0': + resolution: {integrity: sha512-20jxQqnSmpskcGyad7N5l6Xf1gz0UCa6lNde44bEBdaXGpncEYfAZJ142r2ZgzBaG4dUyzGmhqZWv1INU2eiHQ==} + + '@astrojs/language-server@2.17.3': + resolution: {integrity: sha512-QIAFu1e/4quJMTPdmBOm+ZPXc5NDGZE4/YQjgR3EgjhGLl/tdidaWJDr5WmO2lesVc+BW1qN/xNBURmuePDG2A==} + hasBin: true + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-astro: '>=0.11.0' + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + + '@astrojs/markdown-remark@7.3.4': + resolution: {integrity: sha512-AB8S4EQKSryzH624iMDtvC+1AD5nSPzYpCXxc7e1H5ydXhv9B5Td2QfCPA0JyEwqsJpsIsPlW0iOqiy1imdqYw==} + + '@astrojs/markdown-satteri@0.4.6': + resolution: {integrity: sha512-6biKp2lEAYycF3jhv91br+Otyf4EgDFkF1oRL9ks0u/m/YRGmI7XkGQ2MgaM35DslT03f7NtozKtlqfBNkp6bw==} + + '@astrojs/prism@4.1.1': + resolution: {integrity: sha512-sXlAXJX6t4QfuLg+CuCDzpg7NoxWcyb4paFprgtFppLeisCBmh935h+tJ5CWXfsXEoW2IDy/ihbRMtiu28Chjw==} + engines: {node: '>=22.23.4'} + + '@astrojs/prism@4.1.2': + resolution: {integrity: sha512-jMrUEpFDarDO1KWPvVc9eSa8DSfwBlwEOHwX+8zvZQpiqXRGDxFnOXHsYqV6DP7w+0qIgTr98gS1GdQlm8ypWg==} + engines: {node: '>=22.23.4'} + + '@astrojs/telemetry@3.4.2': + resolution: {integrity: sha512-2JOMRUxJrBSfiVDfeKLv1wBmvSIx1Y6u2YEO8awZ9qSq3u/rNAiUyFdOYi8dHv91kHrmYEQX7pRDw0J4xyj3UA==} + engines: {node: '>=22.23.4'} + + '@astrojs/yaml2ts@0.2.4': + resolution: {integrity: sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==} + + '@babel/helper-string-parser@7.29.11': + resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} + 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.11': + resolution: {integrity: sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + + '@bruits/satteri-darwin-arm64@0.10.5': + resolution: {integrity: sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==} + cpu: [arm64] + os: [darwin] + + '@bruits/satteri-darwin-x64@0.10.5': + resolution: {integrity: sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==} + cpu: [x64] + os: [darwin] + + '@bruits/satteri-linux-arm64-gnu@0.10.5': + resolution: {integrity: sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-arm64-musl@0.10.5': + resolution: {integrity: sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@bruits/satteri-linux-x64-gnu@0.10.5': + resolution: {integrity: sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-x64-musl@0.10.5': + resolution: {integrity: sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@bruits/satteri-wasm32-wasi@0.10.5': + resolution: {integrity: sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@bruits/satteri-win32-arm64-msvc@0.10.5': + resolution: {integrity: sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==} + cpu: [arm64] + os: [win32] + + '@bruits/satteri-win32-x64-msvc@0.10.5': + resolution: {integrity: sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==} + cpu: [x64] + os: [win32] + + '@capsizecss/unpack@4.0.1': + resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} + engines: {node: '>=18'} + + '@clack/core@1.5.1': + resolution: {integrity: sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.8.1': + resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==} + engines: {node: '>= 20.12.0'} + + '@emmetio/abbreviation@2.3.3': + resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} + + '@emmetio/css-abbreviation@2.1.8': + resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} + + '@emmetio/css-parser@0.4.1': + resolution: {integrity: sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==} + + '@emmetio/html-matcher@1.3.0': + resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} + + '@emmetio/scanner@1.0.4': + resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} + + '@emmetio/stream-reader-utils@0.1.0': + resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} + + '@emmetio/stream-reader@2.2.0': + resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@napi-rs/wasm-runtime@1.2.6': + resolution: {integrity: sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || 2.0.0-beta.2 + '@emnapi/runtime': ^1.7.1 || 2.0.0-beta.2 + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@oxc-project/types@0.155.0': + resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + + '@rolldown/binding-android-arm-eabi@1.2.15': + resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.15': + resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.15': + resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.15': + resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.15': + resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.15': + resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.15': + resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.15': + resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.15': + resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.15': + resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.15': + resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.15': + resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.15': + resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.15': + resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.15': + resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@shikijs/core@4.5.0': + resolution: {integrity: sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.5.0': + resolution: {integrity: sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.5.0': + resolution: {integrity: sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==} + engines: {node: '>=20'} + + '@shikijs/langs@4.5.0': + resolution: {integrity: sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.5.0': + resolution: {integrity: sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==} + engines: {node: '>=20'} + + '@shikijs/themes@4.5.0': + resolution: {integrity: sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==} + engines: {node: '>=20'} + + '@shikijs/types@4.5.0': + resolution: {integrity: sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.1.0': + resolution: {integrity: sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==} + + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.14': + resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/node@24.19.2': + resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} + + '@volar/kit@2.4.30': + resolution: {integrity: sha512-VocLtumfXnqnPQEWc9Yx7uC1uOh9gyjVNlrn2+6Zx/z9aM6yTLhPK8lSqM9Ilu81XRa48I00lcYJWLi2BLxx8Q==} + peerDependencies: + typescript: <7 + + '@volar/language-core@2.4.30': + resolution: {integrity: sha512-73fjwEV8wat6aOZ6U/y/hqtE30ocrIWECg67b0txGczOMNw+MffLmvJ5fnFfN9drVUEYZzLdaNo6VAy6G6avMw==} + + '@volar/language-server@2.4.30': + resolution: {integrity: sha512-tcP9fzzUBbsLIFHhgnRU4ncQjMn9G9Vax0ffkQmdCrXVEYqRroUepmyC5AVgiju5bCiLD6a55qen5iOTQHkb3g==} + peerDependencies: + typescript: <7 + peerDependenciesMeta: + typescript: + optional: true + + '@volar/language-service@2.4.30': + resolution: {integrity: sha512-w/xDsASiqC71ggCOm7sTDXDNk/l72XEptodeRC68tgZ0e5QM4hM3zAqBGC7mBctvhrpYwKfMG+t9xcUo2hEFtA==} + peerDependencies: + typescript: <7 + peerDependenciesMeta: + typescript: + optional: true + + '@volar/source-map@2.4.30': + resolution: {integrity: sha512-zym1ob/KwDDyN+0jGVtxwbxJSRRnUvv5HspCtz3yEfVVdCG4/dzxJktMGPAgemqRuVs81O0o/y0q3qOUErR8kg==} + + '@volar/typescript@2.4.30': + resolution: {integrity: sha512-/DgA5tgYFPfrLFuSjIMXMGHk1LQv2+P8eny6+BhGNzFJELWIW7UZlc56ZLIRFhLsPpMfIFxM8/HvOPBggrjTZg==} + peerDependencies: + typescript: <7 + peerDependenciesMeta: + typescript: + optional: true + + '@vscode/emmet-helper@2.11.0': + resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-i18n@4.2.0: + resolution: {integrity: sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==} + peerDependencies: + ajv: ^8.0.0-beta.0 + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + am-i-vibing@0.5.0: + resolution: {integrity: sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==} + hasBin: true + + ansi-regex@6.4.0: + resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + astro@7.4.4: + resolution: {integrity: sha512-e+BzhGCaIY/H1tV/BQPTbOPZVZ8wuu6+bbpc+3oSMH2UgoB1l9peSGASk6nswtYra9PO9OoMREhldKtQkZ5TVw==} + engines: {node: '>=22.23.4', npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + peerDependencies: + '@astrojs/markdown-remark': ^7.3.0 + peerDependenciesMeta: + '@astrojs/markdown-remark': + optional: true + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} + engines: {node: '>= 18'} + + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-select@6.0.0: + resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@7.0.0: + resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==} + engines: {node: '>= 6'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@6.0.3: + resolution: {integrity: sha512-3woMEITftG1wKeO/4d/QXH1DVKctTSZg3jNGPpjp9hNr5Hos8MCFF5McTML5ZtgKDObm2R591B0spRp2IVernA==} + engines: {node: '>=18'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + emmet@2.4.11: + resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@3.0.4: + resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.1: + resolution: {integrity: sha512-B0np3dcdxqILX5e9nEi5/Fr4K7gL4oYFVPV1zRa2e9wRCbQoZZNWOZFYyoInvXUPJXBXjss+QXlWLJChDEHDkA==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-uri@3.1.9: + resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fnv1a-64@0.1.2: + resolution: {integrity: sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==} + + fontace@0.4.1: + resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} + engines: {node: '>=18'} + + get-tsconfig@5.0.0-beta.6: + resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} + engines: {node: '>=20.20.0'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-text@4.0.3: + resolution: {integrity: sha512-5yrlWOvGFs6CqHriUpSsNrjzT/Tdo+IOQudbznxOn8K5qblp18MJyzOg8IgDOKQ8MYgQcJA7SusuCjSpDr8mSA==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.3.0: + resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@4.0.0: + resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} + engines: {node: '>=20'} + hasBin: true + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jsonc-parser@2.3.1: + resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + 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.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + 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.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + 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.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + 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.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + 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.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + magic-string@1.4.5: + resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} + + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.4: + resolution: {integrity: sha512-z4B5c6jrnJ1ltOQemhWYPBSAo6y42Z8Gt2yEgfgNTMcLSU59B8bfj8Z8AnAlJrBGLnqkdAUZ7jdDEqQw3KuipQ==} + + mdast-util-from-markdown@2.1.0: + resolution: {integrity: sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.1: + resolution: {integrity: sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.2.1: + resolution: {integrity: sha512-ThatlCBxG2GE+mljhNSfZPv56d3QrGiRh97jjYgIk80IzS4uU6d7cHAaexCYSAmUCCcOs3grJ07kX3oTGsFUOw==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + micromark-core-commonmark@2.0.4: + resolution: {integrity: sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.2: + resolution: {integrity: sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@2.1.0: + resolution: {integrity: sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-edit-map@1.0.0: + resolution: {integrity: sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.3: + resolution: {integrity: sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==} + + micromark@4.0.3: + resolution: {integrity: sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@1.0.1: + resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.5: + resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + p-limit@7.3.3: + resolution: {integrity: sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==} + engines: {node: '>=20'} + + p-queue@9.3.5: + resolution: {integrity: sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==} + engines: {node: '>=20'} + + p-timeout@7.0.2: + resolution: {integrity: sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==} + engines: {node: '>=20'} + + package-manager-detector@1.9.0: + resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + piccolore@0.1.3: + resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} + engines: {node: ^10 || ^12 || >=14} + + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} + engines: {node: '>=14'} + hasBin: true + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + process-ancestry@0.1.0: + resolution: {integrity: sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==} + engines: {node: '>=18.0.0'} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.3: + resolution: {integrity: sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + request-light@0.5.8: + resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} + + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + rolldown@1.2.15: + resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + satteri@0.10.5: + resolution: {integrity: sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shiki@4.5.0: + resolution: {integrity: sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==} + engines: {node: '>=20'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.9.1: + resolution: {integrity: sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==} + engines: {node: '>= 18'} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + source-map@0.8.0: + resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.3.0: + resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==} + engines: {node: '>=20'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + svgo@4.1.0: + resolution: {integrity: sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==} + engines: {node: '>=16'} + hasBin: true + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyclip@1.0.3: + resolution: {integrity: sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==} + engines: {node: ^16.14.0 || >= 17.3.0} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyps@0.5.1: + resolution: {integrity: sha512-B6Ykif5HflC3AdDkS4Zst/2muYVfeXoVMy+KorqH80s/1x4vunIORpNySH681X7fjj+dFM9MDKD6/Hsccjb87Q==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + + typescript-auto-import-cache@0.3.6: + resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + ultrahtml@1.7.0: + resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici@8.12.3: + resolution: {integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==} + engines: {node: '>=22.19.0'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@1.0.3: + resolution: {integrity: sha512-XGnEwj5sWdCbiBX01mWLSBncS0yMRjnLygau3IfdOlfjMacNIQvTGW4VH7pWBq+gGWArfSef/CensMLjMmIr/A==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.1.0: + resolution: {integrity: sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==} + + unist-util-visit@5.2.0: + resolution: {integrity: sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==} + + unstorage@1.17.6: + resolution: {integrity: sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + verkit@0.5.0: + resolution: {integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==} + engines: {node: '>=18.12.0'} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@8.3.5: + resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 || ^0.8.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + volar-service-css@0.0.71: + resolution: {integrity: sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-emmet@0.0.71: + resolution: {integrity: sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-html@0.0.71: + resolution: {integrity: sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-prettier@0.0.71: + resolution: {integrity: sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg==} + peerDependencies: + '@volar/language-service': ~2.4.0 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + prettier: + optional: true + + volar-service-typescript-twoslash-queries@0.0.71: + resolution: {integrity: sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.71: + resolution: {integrity: sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-yaml@0.0.71: + resolution: {integrity: sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-css-languageservice@6.3.10: + resolution: {integrity: sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==} + + vscode-html-languageservice@5.6.2: + resolution: {integrity: sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==} + + vscode-json-languageservice@4.1.8: + resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} + engines: {npm: '>=7.0.0'} + + vscode-jsonrpc@10.0.0: + resolution: {integrity: sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA==} + engines: {node: '>=14.17.0'} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-protocol@3.18.5: + resolution: {integrity: sha512-FW4fBMzZ99N8xeob4py0l050wgCoRgTpEq5Xcsb9lM1bc29UKGR/wSV+4knaqFxssFHajy7OzNiKP/m3U1ZEuA==} + + vscode-languageserver-textdocument@1.0.16: + resolution: {integrity: sha512-qeNEuOi9a3qkqRf7Gfez2WL2VwlBTLCNekHBGO9kxPVUp9XuY15yfTJ9jwvOwKI0I2MhUUfZq95NSp+LK1CLQg==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver-types@3.18.5: + resolution: {integrity: sha512-kPd6+JQNz3HMJvjKlqUxERSlZHAntl/aGpsk7ImwIh0ewDKDcjgAEyVSYciPiOJD3ftbK9IrfiqUeTZLSbCizw==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml-language-server@1.23.0: + resolution: {integrity: sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A==} + hasBin: true + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.2.0: + resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@astrojs/astro2tsx@0.1.3(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + transitivePeerDependencies: + - '@emnapi/runtime' + + '@astrojs/check@0.9.12(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@6.0.3)': + dependencies: + '@astrojs/language-server': 2.17.3(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@6.0.3) + chokidar: 5.0.0 + kleur: 4.1.5 + typescript: 6.0.3 + yargs: 18.2.0 + transitivePeerDependencies: + - '@emnapi/runtime' + - prettier + - prettier-plugin-astro + + '@astrojs/compiler-binding-android-arm64@0.5.2': + optional: true + + '@astrojs/compiler-binding-darwin-arm64@0.5.2': + optional: true + + '@astrojs/compiler-binding-darwin-x64@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-gnu@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-musl@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-gnu@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-musl@0.5.2': + optional: true + + '@astrojs/compiler-binding-wasm32-wasi@0.5.2': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + optional: true + + '@astrojs/compiler-binding-win32-arm64-msvc@0.5.2': + optional: true + + '@astrojs/compiler-binding-win32-x64-msvc@0.5.2': + optional: true + + '@astrojs/compiler-binding@0.5.2': + optionalDependencies: + '@astrojs/compiler-binding-android-arm64': 0.5.2 + '@astrojs/compiler-binding-darwin-arm64': 0.5.2 + '@astrojs/compiler-binding-darwin-x64': 0.5.2 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.5.2 + '@astrojs/compiler-binding-linux-arm64-musl': 0.5.2 + '@astrojs/compiler-binding-linux-x64-gnu': 0.5.2 + '@astrojs/compiler-binding-linux-x64-musl': 0.5.2 + '@astrojs/compiler-binding-wasm32-wasi': 0.5.2 + '@astrojs/compiler-binding-win32-arm64-msvc': 0.5.2 + '@astrojs/compiler-binding-win32-x64-msvc': 0.5.2 + + '@astrojs/compiler-rs@0.5.2': + dependencies: + '@astrojs/compiler-binding': 0.5.2 + + '@astrojs/internal-helpers@0.13.0': + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + picomatch: 4.0.7 + retext-smartypants: 6.2.0 + shiki: 4.5.0 + smol-toml: 1.9.1 + unified: 11.0.5 + yaml: 2.9.1 + + '@astrojs/language-server@2.17.3(@emnapi/runtime@1.11.3)(prettier@3.9.10)(typescript@6.0.3)': + dependencies: + '@astrojs/astro2tsx': 0.1.3(@emnapi/runtime@1.11.3) + '@astrojs/yaml2ts': 0.2.4 + '@volar/kit': 2.4.30(typescript@6.0.3) + '@volar/language-core': 2.4.30 + '@volar/language-server': 2.4.30(typescript@6.0.3) + '@volar/language-service': 2.4.30(typescript@6.0.3) + muggle-string: 0.4.1 + tinyglobby: 0.2.17 + volar-service-css: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-emmet: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-html: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-prettier: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3))(prettier@3.9.10) + volar-service-typescript: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-typescript-twoslash-queries: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + volar-service-yaml: 0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)) + vscode-html-languageservice: 5.6.2 + vscode-uri: 3.2.0 + optionalDependencies: + prettier: 3.9.10 + transitivePeerDependencies: + - '@emnapi/runtime' + - typescript + + '@astrojs/markdown-remark@7.3.4': + dependencies: + '@astrojs/internal-helpers': 0.13.0 + '@astrojs/prism': 4.1.1 + '@mdx-js/mdx': 3.1.1 + acorn: 8.19.0 + estree-util-visit: 2.0.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-html: 9.0.5 + hast-util-to-text: 4.0.3 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.3 + source-map: 0.8.0 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.2.0 + unist-util-visit-parents: 6.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + optional: true + + '@astrojs/markdown-satteri@0.4.6': + dependencies: + '@astrojs/internal-helpers': 0.13.0 + '@astrojs/prism': 4.1.2 + github-slugger: 2.0.0 + satteri: 0.10.5 + + '@astrojs/prism@4.1.1': + dependencies: + prismjs: 1.30.0 + optional: true + + '@astrojs/prism@4.1.2': + dependencies: + prismjs: 1.30.0 + + '@astrojs/telemetry@3.4.2': + dependencies: + ci-info: 4.4.0 + dset: 3.1.4 + is-docker: 4.0.0 + package-manager-detector: 1.9.0 + + '@astrojs/yaml2ts@0.2.4': + dependencies: + yaml: 2.9.1 + + '@babel/helper-string-parser@7.29.11': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.11': + dependencies: + '@babel/types': 7.29.11 + + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.11 + '@babel/helper-validator-identifier': 7.29.7 + + '@bruits/satteri-darwin-arm64@0.10.5': + optional: true + + '@bruits/satteri-darwin-x64@0.10.5': + optional: true + + '@bruits/satteri-linux-arm64-gnu@0.10.5': + optional: true + + '@bruits/satteri-linux-arm64-musl@0.10.5': + optional: true + + '@bruits/satteri-linux-x64-gnu@0.10.5': + optional: true + + '@bruits/satteri-linux-x64-musl@0.10.5': + optional: true + + '@bruits/satteri-wasm32-wasi@0.10.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.2.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@bruits/satteri-win32-arm64-msvc@0.10.5': + optional: true + + '@bruits/satteri-win32-x64-msvc@0.10.5': + optional: true + + '@capsizecss/unpack@4.0.1': + dependencies: + fontkitten: 1.0.3 + + '@clack/core@1.5.1': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.8.1': + dependencies: + '@clack/core': 1.5.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@emmetio/abbreviation@2.3.3': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-abbreviation@2.1.8': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-parser@0.4.1': + dependencies: + '@emmetio/stream-reader': 2.2.0 + '@emmetio/stream-reader-utils': 0.1.0 + + '@emmetio/html-matcher@1.3.0': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/scanner@1.0.4': {} + + '@emmetio/stream-reader-utils@0.1.0': {} + + '@emmetio/stream-reader@2.2.0': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.3': + dependencies: + '@emnapi/wasi-threads': 1.2.3 + tslib: 2.8.1 + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.3': + dependencies: + tslib: 2.8.1 + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdx': 2.0.14 + acorn: 8.19.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.1 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.19.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.2.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + optional: true + + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.4 + + '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@oslojs/encoding@1.1.0': {} + + '@oxc-project/types@0.155.0': {} + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + + '@rolldown/binding-android-arm-eabi@1.2.15': + optional: true + + '@rolldown/binding-android-arm64@1.2.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.15': + optional: true + + '@rolldown/binding-darwin-x64@1.2.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.15': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.15': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@shikijs/core@4.5.0': + dependencies: + '@shikijs/primitive': 4.5.0 + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + + '@shikijs/langs@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + + '@shikijs/primitive@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.5.0': + dependencies: + '@shikijs/types': 4.5.0 + + '@shikijs/types@4.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.1.0': {} + + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.14': + optional: true + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@24.19.2': + dependencies: + undici-types: 7.24.6 + + '@types/unist@2.0.11': + optional: true + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.4.0': {} + + '@volar/kit@2.4.30(typescript@6.0.3)': + dependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + '@volar/typescript': 2.4.30(typescript@6.0.3) + typesafe-path: 0.2.2 + typescript: 6.0.3 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + + '@volar/language-core@2.4.30': + dependencies: + '@volar/source-map': 2.4.30 + + '@volar/language-server@2.4.30(typescript@6.0.3)': + dependencies: + '@volar/language-core': 2.4.30 + '@volar/language-service': 2.4.30(typescript@6.0.3) + '@volar/typescript': 2.4.30(typescript@6.0.3) + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.18.5 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + optionalDependencies: + typescript: 6.0.3 + + '@volar/language-service@2.4.30(typescript@6.0.3)': + dependencies: + '@volar/language-core': 2.4.30 + vscode-languageserver-protocol: 3.18.5 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + optionalDependencies: + typescript: 6.0.3 + + '@volar/source-map@2.4.30': {} + + '@volar/typescript@2.4.30(typescript@6.0.3)': + dependencies: + '@volar/language-core': 2.4.30 + '@volar/language-service': 2.4.30(typescript@6.0.3) + path-browserify: 1.0.1 + vscode-uri: 3.2.0 + optionalDependencies: + typescript: 6.0.3 + + '@vscode/emmet-helper@2.11.0': + dependencies: + emmet: 2.4.11 + jsonc-parser: 2.3.1 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.18.5 + vscode-uri: 3.2.0 + + '@vscode/l10n@0.0.18': {} + + acorn-jsx@5.3.2(acorn@8.19.0): + dependencies: + acorn: 8.19.0 + optional: true + + acorn@8.19.0: + optional: true + + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-i18n@4.2.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.9 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + am-i-vibing@0.5.0: + dependencies: + process-ancestry: 0.1.0 + + ansi-regex@6.4.0: {} + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + aria-query@5.3.2: {} + + array-iterate@2.0.1: + optional: true + + astring@1.9.0: + optional: true + + astro@7.4.4(@astrojs/markdown-remark@7.3.4)(@types/node@24.19.2)(yaml@2.9.1): + dependencies: + '@astrojs/compiler-rs': 0.5.2 + '@astrojs/internal-helpers': 0.13.0 + '@astrojs/markdown-satteri': 0.4.6 + '@astrojs/telemetry': 3.4.2 + '@capsizecss/unpack': 4.0.1 + '@clack/prompts': 1.8.1 + '@oslojs/encoding': 1.1.0 + am-i-vibing: 0.5.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + ci-info: 4.4.0 + clsx: 2.1.1 + common-ancestor-path: 2.0.0 + cookie: 2.0.1 + devalue: 6.0.3 + diff: 9.0.0 + dset: 3.1.4 + es-module-lexer: 3.0.4 + flattie: 1.1.1 + fontace: 0.4.1 + get-tsconfig: 5.0.0-beta.6 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.3.0 + jsonc-parser: 3.3.1 + magic-string: 1.4.5 + magicast: 0.5.5 + mrmime: 2.0.1 + neotraverse: 1.0.1 + obug: 3.0.0 + p-limit: 7.3.3 + p-queue: 9.3.5 + package-manager-detector: 1.9.0 + piccolore: 0.1.3 + picomatch: 4.0.7 + rolldown: 1.2.15 + shiki: 4.5.0 + smol-toml: 1.9.1 + svgo: 4.1.0 + tinyclip: 1.0.3 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyps: 0.5.1 + ultrahtml: 1.7.0 + unifont: 1.0.3 + unstorage: 1.17.6 + verkit: 0.5.0 + vite: 8.3.5(@types/node@24.19.2)(yaml@2.9.1) + vitefu: 1.1.3(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 22.0.0 + zod: 4.6.7 + optionalDependencies: + '@astrojs/markdown-remark': 7.3.4 + sharp: 0.35.5(@types/node@24.19.2) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools' + - aws4fetch + - db0 + - esbuild + - idb-keyval + - ioredis + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - uploadthing + - yaml + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + boolbase@1.0.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: + optional: true + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + ci-info@4.4.0: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + clsx@2.1.1: {} + + collapse-white-space@2.1.0: + optional: true + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + common-ancestor-path@2.0.0: {} + + cookie-es@1.2.3: {} + + cookie@2.0.1: {} + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-select@6.0.0: + dependencies: + boolbase: 1.0.0 + css-what: 7.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.2 + + css-what@7.0.0: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + devalue@6.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@9.0.0: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dset@3.1.4: {} + + emmet@2.4.11: + dependencies: + '@emmetio/abbreviation': 2.3.3 + '@emmetio/css-abbreviation': 2.1.8 + + emoji-regex@10.6.0: {} + + entities@4.5.0: {} + + entities@6.0.1: + optional: true + + es-module-lexer@3.0.4: {} + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + optional: true + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.19.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + optional: true + + escalade@3.2.0: {} + + escape-string-regexp@5.0.0: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.9 + optional: true + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + optional: true + + estree-util-is-identifier-name@3.0.0: + optional: true + + estree-util-scope@1.0.1: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + optional: true + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + optional: true + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + optional: true + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + optional: true + + eventemitter3@5.0.4: {} + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-uri@3.1.9: {} + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + flattie@1.1.1: {} + + fnv1a-64@0.1.2: {} + + fontace@0.4.1: + dependencies: + fontkitten: 1.0.3 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.7.0: {} + + get-tsconfig@5.0.0-beta.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-slugger@2.0.0: {} + + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.5 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.5 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + optional: true + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.2.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + optional: true + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.5 + optional: true + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.4.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.2.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + optional: true + + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.5 + '@ungap/structured-clone': 1.4.0 + unist-util-position: 5.0.0 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + optional: true + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + optional: true + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + optional: true + + hast-util-to-text@4.0.3: + dependencies: + '@types/hast': 3.0.5 + optional: true + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + optional: true + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.3.0: {} + + inline-style-parser@0.2.7: + optional: true + + iron-webcrypto@1.2.1: {} + + is-alphabetical@2.0.1: + optional: true + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + optional: true + + is-decimal@2.0.1: + optional: true + + is-docker@4.0.0: {} + + is-hexadecimal@2.0.1: + optional: true + + is-plain-obj@4.1.0: {} + + json-schema-traverse@1.0.0: {} + + jsonc-parser@2.3.1: {} + + jsonc-parser@3.3.1: {} + + kleur@4.1.5: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + 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 + + longest-streak@3.1.0: {} + + lru-cache@11.5.3: {} + + magic-string@1.4.5: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magicast@0.5.5: + dependencies: + '@babel/parser': 7.29.11 + '@babel/types': 7.29.11 + source-map-js: 1.2.2 + + markdown-extensions@2.0.0: + optional: true + + markdown-table@3.0.4: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.2.0 + optional: true + + mdast-util-find-and-replace@3.0.4: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.1.0 + + mdast-util-from-markdown@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.3 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.4 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.1.0 + mdast-util-to-markdown: 2.2.1 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.1.0 + mdast-util-to-markdown: 2.2.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.1.0 + mdast-util-to-markdown: 2.2.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.1.0 + mdast-util-to-markdown: 2.2.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.1.0 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.1 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.2.1 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.1.0 + mdast-util-to-markdown: 2.2.1 + transitivePeerDependencies: + - supports-color + optional: true + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.1.0 + mdast-util-to-markdown: 2.2.1 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + optional: true + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.1.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.2.1 + transitivePeerDependencies: + - supports-color + optional: true + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.1.0 + mdast-util-to-markdown: 2.2.1 + transitivePeerDependencies: + - supports-color + optional: true + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.4.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.2.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.2.1: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-character: 2.1.1 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + unist-util-visit: 5.2.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.28: {} + + mdn-data@2.27.1: {} + + micromark-core-commonmark@2.0.4: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.1.0 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-edit-map: 1.0.0 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.4 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-table@2.1.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.3 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.2 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + optional: true + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + vfile-message: 4.0.3 + optional: true + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.3 + optional: true + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.4 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + optional: true + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.19.0 + acorn-jsx: 5.3.2(acorn@8.19.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.3 + optional: true + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + optional: true + + micromark-factory-space@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.3 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-edit-map@1.0.0: + dependencies: + micromark-util-types: 2.0.3 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.9 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + vfile-message: 4.0.3 + optional: true + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.3 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.3: {} + + micromark@4.0.3: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.4 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-edit-map: 1.0.0 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + transitivePeerDependencies: + - supports-color + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.20: {} + + neotraverse@1.0.1: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.5: {} + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + obug@3.0.0: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + p-limit@7.3.3: + dependencies: + yocto-queue: 1.2.2 + + p-queue@9.3.5: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.2 + + p-timeout@7.0.2: {} + + package-manager-detector@1.9.0: {} + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + optional: true + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + optional: true + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + optional: true + + path-browserify@1.0.1: {} + + piccolore@0.1.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + prettier@3.9.10: {} + + prismjs@1.30.0: {} + + process-ancestry@0.1.0: {} + + property-information@7.2.0: {} + + radix3@1.1.2: {} + + readdirp@5.1.1: {} + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + optional: true + + recma-jsx@1.0.1(acorn@8.19.0): + dependencies: + acorn: 8.19.0 + acorn-jsx: 5.3.2(acorn@8.19.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + optional: true + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.9 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + optional: true + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + optional: true + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.5 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + optional: true + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + optional: true + + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.5 + hast-util-sanitize: 5.0.2 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + optional: true + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.1.0 + micromark-util-types: 2.0.3 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.3: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.2.0 + optional: true + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.2.1 + unified: 11.0.5 + + request-light@0.5.8: {} + + request-light@0.7.0: {} + + require-from-string@2.0.2: {} + + resolve-pkg-maps@1.0.0: {} + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + optional: true + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.2.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + optional: true + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + optional: true + + rolldown@1.2.15: + dependencies: + '@oxc-project/types': 0.155.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.15 + '@rolldown/binding-android-arm64': 1.2.15 + '@rolldown/binding-darwin-arm64': 1.2.15 + '@rolldown/binding-darwin-x64': 1.2.15 + '@rolldown/binding-freebsd-x64': 1.2.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.15 + '@rolldown/binding-linux-arm64-gnu': 1.2.15 + '@rolldown/binding-linux-arm64-musl': 1.2.15 + '@rolldown/binding-linux-ppc64-gnu': 1.2.15 + '@rolldown/binding-linux-s390x-gnu': 1.2.15 + '@rolldown/binding-linux-x64-gnu': 1.2.15 + '@rolldown/binding-linux-x64-musl': 1.2.15 + '@rolldown/binding-openharmony-arm64': 1.2.15 + '@rolldown/binding-win32-arm64-msvc': 1.2.15 + '@rolldown/binding-win32-x64-msvc': 1.2.15 + + satteri@0.10.5: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + optionalDependencies: + '@bruits/satteri-darwin-arm64': 0.10.5 + '@bruits/satteri-darwin-x64': 0.10.5 + '@bruits/satteri-linux-arm64-gnu': 0.10.5 + '@bruits/satteri-linux-arm64-musl': 0.10.5 + '@bruits/satteri-linux-x64-gnu': 0.10.5 + '@bruits/satteri-linux-x64-musl': 0.10.5 + '@bruits/satteri-wasm32-wasi': 0.10.5 + '@bruits/satteri-win32-arm64-msvc': 0.10.5 + '@bruits/satteri-win32-x64-msvc': 0.10.5 + + sax@1.6.1: {} + + semver@7.8.5: {} + + sharp@0.35.5(@types/node@24.19.2): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 24.19.2 + optional: true + + shiki@4.5.0: + dependencies: + '@shikijs/core': 4.5.0 + '@shikijs/engine-javascript': 4.5.0 + '@shikijs/engine-oniguruma': 4.5.0 + '@shikijs/langs': 4.5.0 + '@shikijs/themes': 4.5.0 + '@shikijs/types': 4.5.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + + sisteransi@1.0.5: {} + + smol-toml@1.9.1: {} + + source-map-js@1.2.2: {} + + source-map@0.7.6: + optional: true + + source-map@0.8.0: + optional: true + + space-separated-tokens@2.0.2: {} + + std-env@4.3.0: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + string-width@8.3.0: + dependencies: + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.4.0 + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + optional: true + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + optional: true + + svgo@4.1.0: + dependencies: + commander: 11.1.0 + css-select: 6.0.0 + css-tree: 3.2.1 + css-what: 7.0.0 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.1 + + tiny-inflate@1.0.3: {} + + tinyclip@1.0.3: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyps@0.5.1: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tslib@2.8.1: {} + + typesafe-path@0.2.2: {} + + typescript-auto-import-cache@0.3.6: + dependencies: + semver: 7.8.5 + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + ultrahtml@1.7.0: {} + + uncrypto@0.1.3: {} + + undici-types@7.24.6: {} + + undici@8.12.3: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@1.0.3: + dependencies: + css-tree: 3.2.1 + fnv1a-64: 0.1.2 + std-env: 4.3.0 + undici: 8.12.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + optional: true + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + optional: true + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.2.0 + optional: true + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + optional: true + + unist-util-visit-parents@6.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.2.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.1.0 + + unstorage@1.17.6: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.5.3 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + + verkit@0.5.0: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + optional: true + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@8.3.5(@types/node@24.19.2)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.32 + rolldown: 1.2.15 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.19.2 + fsevents: 2.3.3 + yaml: 2.9.1 + + vitefu@1.1.3(vite@8.3.5(@types/node@24.19.2)(yaml@2.9.1)): + optionalDependencies: + vite: 8.3.5(@types/node@24.19.2)(yaml@2.9.1) + + volar-service-css@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + vscode-css-languageservice: 6.3.10 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-emmet@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + '@emmetio/css-parser': 0.4.1 + '@emmetio/html-matcher': 1.3.0 + '@vscode/emmet-helper': 2.11.0 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-html@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + vscode-html-languageservice: 5.6.2 + vscode-languageserver-textdocument: 1.0.16 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-prettier@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3))(prettier@3.9.10): + dependencies: + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + prettier: 3.9.10 + + volar-service-typescript-twoslash-queries@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-typescript@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + path-browserify: 1.0.1 + semver: 7.8.5 + typescript-auto-import-cache: 0.3.6 + vscode-languageserver-textdocument: 1.0.16 + vscode-nls: 5.2.0 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + volar-service-yaml@0.0.71(@volar/language-service@2.4.30(typescript@6.0.3)): + dependencies: + vscode-uri: 3.2.0 + yaml-language-server: 1.23.0 + optionalDependencies: + '@volar/language-service': 2.4.30(typescript@6.0.3) + + vscode-css-languageservice@6.3.10: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.2.0 + + vscode-html-languageservice@5.6.2: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.18.5 + vscode-uri: 3.2.0 + + vscode-json-languageservice@4.1.8: + dependencies: + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.18.5 + vscode-nls: 5.2.0 + vscode-uri: 3.2.0 + + vscode-jsonrpc@10.0.0: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-protocol@3.18.5: + dependencies: + vscode-jsonrpc: 10.0.0 + vscode-languageserver-types: 3.18.5 + + vscode-languageserver-textdocument@1.0.16: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver-types@3.18.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@3.2.0: {} + + web-namespaces@2.0.1: + optional: true + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + xxhash-wasm@1.1.0: {} + + y18n@5.0.8: {} + + yaml-language-server@1.23.0: + dependencies: + '@vscode/l10n': 0.0.18 + ajv: 8.20.0 + ajv-draft-04: 1.0.0(ajv@8.20.0) + ajv-i18n: 4.2.0(ajv@8.20.0) + prettier: 3.9.10 + request-light: 0.5.8 + vscode-json-languageservice: 4.1.8 + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.16 + vscode-languageserver-types: 3.18.5 + vscode-uri: 3.2.0 + yaml: 2.8.3 + + yaml@2.8.3: {} + + yaml@2.9.1: {} + + yargs-parser@22.0.0: {} + + yargs@18.2.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.3.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yocto-queue@1.2.2: {} + + zod@4.6.7: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..2ca35f8 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +minimumReleaseAgeExclude: + - '@astrojs/markdown-satteri@0.4.6' + - '@astrojs/prism@4.1.2' + - '@astrojs/telemetry@3.4.2' + - astro@7.4.4 diff --git a/scripts/record-fixture.ts b/scripts/record-fixture.ts new file mode 100644 index 0000000..3f558ee --- /dev/null +++ b/scripts/record-fixture.ts @@ -0,0 +1,83 @@ +// Records the repo file snapshot that the fixture source serves to tests. +// +// Usage: +// GITHUB_TOKEN=$(gh auth token) pnpm record-fixture tests/fixtures/guides/shaders/264.md +// +// For each guide, the script asks the production GitHub source for the file list between the +// guide's baseSha and headSha, then saves every changed file at both commits where it exists. +// It writes to REPO_FILE_FIXTURES, or tests/fixtures/repo-files by default. + +import { createHash } from 'node:crypto'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname } from 'node:path'; +import { parse } from 'yaml'; +import { pullRequestOf, guideFrontmatter } from '../src/guide/schema.ts'; +import { comparePath, filePath } from '../src/repo-files/fixture.ts'; +import { githubRepoFileSource } from '../src/repo-files/github.ts'; +import { DEFAULT_FIXTURE_DIR, type ChangedFile, type PullRequestRef } from '../src/repo-files/index.ts'; + +const guides = process.argv.slice(2); +if (guides.length === 0) { + console.error('Usage: pnpm record-fixture [...]'); + process.exit(2); +} +const token = process.env.GITHUB_TOKEN; +if (!token) { + console.error('Set GITHUB_TOKEN, for example GITHUB_TOKEN=$(gh auth token).'); + process.exit(2); +} + +const outDir = process.env.REPO_FILE_FIXTURES ?? DEFAULT_FIXTURE_DIR; +const source = githubRepoFileSource(token); + +for (const guide of guides) { + const pull = pullRequestOf(guideFrontmatter.parse(readFrontmatter(await readFile(guide, 'utf8')))); + const files = await source.listChangedFiles(pull); + await write(comparePath(outDir, pull), `${JSON.stringify(files, null, '\t')}\n`); + + let saved = 0; + for (const file of files) { + if (file.status !== 'removed') { + saved += await save(pull, file, file.path, pull.headSha, file.blobSha); + } + if (file.status !== 'added') { + const basePath = file.previousPath ?? file.path; + const expectedBlob = file.status === 'removed' ? file.blobSha : undefined; + saved += await save(pull, file, basePath, pull.baseSha, expectedBlob); + } + } + console.log(`${pull.owner}/${pull.repo}#${pull.pr}: ${files.length} changed files, ${saved} file versions saved to ${outDir}`); +} + +function readFrontmatter(markdown: string): unknown { + const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown); + if (!match) throw new Error('The guide has no frontmatter block.'); + return parse(match[1]!); +} + +/** Saves one file version and checks it against the blob SHA GitHub listed, when there is one. */ +async function save( + pull: PullRequestRef, + file: ChangedFile, + path: string, + commitSha: string, + expectedBlob: string | undefined, +): Promise { + const bytes = await source.readFile(pull, path, commitSha); + if (bytes === null) throw new Error(`${path} is missing at ${commitSha}, but the compare view lists it as ${file.status}.`); + if (expectedBlob !== undefined && gitBlobSha(bytes) !== expectedBlob) { + throw new Error(`${path} at ${commitSha} does not hash to blob ${expectedBlob}.`); + } + await write(filePath(outDir, pull, path, commitSha), bytes); + return 1; +} + +/** The SHA that `git hash-object` prints for these bytes. */ +function gitBlobSha(bytes: Uint8Array): string { + return createHash('sha1').update(`blob ${bytes.byteLength}\0`).update(bytes).digest('hex'); +} + +async function write(path: string, data: string | Uint8Array): Promise { + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, data); +} diff --git a/src/guide/schema.ts b/src/guide/schema.ts new file mode 100644 index 0000000..eae3b59 --- /dev/null +++ b/src/guide/schema.ts @@ -0,0 +1,29 @@ +import { z } from 'astro/zod'; +import type { PullRequestRef } from '../repo-files/index.ts'; + +const commitSha = z + .string() + .regex(/^[0-9a-f]{40}$/, 'Expected a full 40-character lowercase commit SHA'); + +/** + * A guide file's frontmatter. `docs/guide-format.md` documents each field. + * Unknown keys fail, so a misspelled field names itself in the build error. + */ +export const guideFrontmatter = z.strictObject({ + repo: z + .string() + .regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, 'Expected owner/name, such as campdotdev/shaders'), + pr: z.number().int().positive(), + title: z.string().min(1), + scope: z.string().min(1).optional(), + baseSha: commitSha, + headSha: commitSha, + afterTheFact: z.boolean().default(false), +}); + +export type GuideFrontmatter = z.infer; + +export function pullRequestOf(frontmatter: GuideFrontmatter): PullRequestRef { + const [owner, repo] = frontmatter.repo.split('/') as [string, string]; + return { owner, repo, pr: frontmatter.pr, baseSha: frontmatter.baseSha, headSha: frontmatter.headSha }; +} diff --git a/src/repo-files/fixture.ts b/src/repo-files/fixture.ts new file mode 100644 index 0000000..a0c29dc --- /dev/null +++ b/src/repo-files/fixture.ts @@ -0,0 +1,41 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import type { ChangedFile, PullRequestRef, RepoFileSource, RepoRef } from './types.ts'; + +/** + * Where a recorded snapshot keeps the compare file list for a base and head pair. + * `scripts/record-fixture.ts` writes to the same paths. + */ +export function comparePath(dir: string, pull: PullRequestRef): string { + return join(dir, pull.owner, pull.repo, 'compare', `${pull.baseSha}...${pull.headSha}.json`); +} + +/** Where a recorded snapshot keeps one file's bytes at one commit. */ +export function filePath(dir: string, repo: RepoRef, path: string, commitSha: string): string { + return join(dir, repo.owner, repo.repo, 'files', commitSha, path); +} + +/** Serves files from a snapshot that `scripts/record-fixture.ts` recorded. It never touches the network. */ +export function fixtureRepoFileSource(dir: string): RepoFileSource { + return { + async listChangedFiles(pull: PullRequestRef): Promise { + const path = comparePath(dir, pull); + const json = await readFile(path, 'utf8').catch((error: NodeJS.ErrnoException) => { + if (error.code !== 'ENOENT') throw error; + throw new Error( + `No fixture snapshot for ${pull.owner}/${pull.repo}#${pull.pr} at ${path}. Record one with \`pnpm record-fixture \`.`, + ); + }); + return JSON.parse(json) as ChangedFile[]; + }, + + async readFile(repo: RepoRef, path: string, commitSha: string): Promise { + try { + return new Uint8Array(await readFile(filePath(dir, repo, path, commitSha))); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; + } + }, + }; +} diff --git a/src/repo-files/github.ts b/src/repo-files/github.ts new file mode 100644 index 0000000..82f5750 --- /dev/null +++ b/src/repo-files/github.ts @@ -0,0 +1,77 @@ +import type { ChangedFile, FileStatus, PullRequestRef, RepoFileSource, RepoRef } from './types.ts'; + +const API = 'https://api.github.com'; + +/** GitHub's compare endpoint lists at most this many files. */ +const COMPARE_FILE_CAP = 300; + +interface DiffEntry { + filename: string; + previous_filename?: string; + status: FileStatus; + sha: string; + additions: number; + deletions: number; + patch?: string; +} + +/** + * Reads files from the GitHub REST API. + * The file list comes from `compare/{base}...{head}`, which never changes for fixed SHAs. + * The PR's own files endpoint always follows the PR's current head instead. + */ +export function githubRepoFileSource(token: string, fetchImpl: typeof fetch = fetch): RepoFileSource { + async function get(path: string, accept: string): Promise { + return fetchImpl(`${API}${path}`, { + headers: { + Accept: accept, + Authorization: `Bearer ${token}`, + 'User-Agent': 'campdotdev-guides', + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + } + + return { + async listChangedFiles(pull: PullRequestRef): Promise { + const path = `/repos/${pull.owner}/${pull.repo}/compare/${pull.baseSha}...${pull.headSha}`; + const response = await get(path, 'application/vnd.github+json'); + if (!response.ok) throw await requestError(path, response); + const body = (await response.json()) as { files?: DiffEntry[] }; + const files = body.files ?? []; + if (files.length >= COMPARE_FILE_CAP) { + throw new Error( + `${path} lists ${files.length} files, which is GitHub's cap for compare. The guide renderer cannot tell whether files are missing.`, + ); + } + return files.map(toChangedFile); + }, + + async readFile(repo: RepoRef, filePath: string, commitSha: string): Promise { + const encoded = filePath.split('/').map(encodeURIComponent).join('/'); + const path = `/repos/${repo.owner}/${repo.repo}/contents/${encoded}?ref=${commitSha}`; + const response = await get(path, 'application/vnd.github.raw'); + if (response.status === 404) return null; + if (!response.ok) throw await requestError(path, response); + return new Uint8Array(await response.arrayBuffer()); + }, + }; +} + +function toChangedFile(entry: DiffEntry): ChangedFile { + const file: ChangedFile = { + path: entry.filename, + status: entry.status, + blobSha: entry.sha, + additions: entry.additions, + deletions: entry.deletions, + }; + if (entry.previous_filename !== undefined) file.previousPath = entry.previous_filename; + if (entry.patch !== undefined) file.patch = entry.patch; + return file; +} + +async function requestError(path: string, response: Response): Promise { + const text = await response.text(); + return new Error(`GitHub API GET ${path} returned ${response.status}: ${text.slice(0, 300)}`); +} diff --git a/src/repo-files/index.ts b/src/repo-files/index.ts new file mode 100644 index 0000000..8276067 --- /dev/null +++ b/src/repo-files/index.ts @@ -0,0 +1,23 @@ +import { fixtureRepoFileSource } from './fixture.ts'; +import { githubRepoFileSource } from './github.ts'; +import type { RepoFileSource } from './types.ts'; + +export type { ChangedFile, FileStatus, PullRequestRef, RepoFileSource, RepoRef } from './types.ts'; + +/** The snapshot the fixture source reads when `REPO_FILE_FIXTURES` is unset. */ +export const DEFAULT_FIXTURE_DIR = 'tests/fixtures/repo-files'; + +/** + * Picks the repo file source from `REPO_FILE_SOURCE`: + * `github` reads the GitHub API with `GITHUB_TOKEN`, and `fixture` (the default) reads `REPO_FILE_FIXTURES`. + */ +export function repoFileSource(env: NodeJS.ProcessEnv = process.env): RepoFileSource { + const kind = env.REPO_FILE_SOURCE ?? 'fixture'; + if (kind === 'fixture') return fixtureRepoFileSource(env.REPO_FILE_FIXTURES ?? DEFAULT_FIXTURE_DIR); + if (kind === 'github') { + const token = env.GITHUB_TOKEN; + if (!token) throw new Error('REPO_FILE_SOURCE=github needs a GITHUB_TOKEN.'); + return githubRepoFileSource(token); + } + throw new Error(`REPO_FILE_SOURCE must be "github" or "fixture", not "${kind}".`); +} diff --git a/src/repo-files/types.ts b/src/repo-files/types.ts new file mode 100644 index 0000000..0065b37 --- /dev/null +++ b/src/repo-files/types.ts @@ -0,0 +1,47 @@ +/** A GitHub repository, such as campdotdev/shaders. */ +export interface RepoRef { + owner: string; + repo: string; +} + +/** A pull request pinned to the base and head commits that a guide describes. */ +export interface PullRequestRef extends RepoRef { + pr: number; + baseSha: string; + headSha: string; +} + +/** GitHub's DiffEntry statuses. */ +export type FileStatus = + | 'added' + | 'removed' + | 'modified' + | 'renamed' + | 'copied' + | 'changed' + | 'unchanged'; + +/** One file that a pull request changes, as GitHub's compare view lists it. */ +export interface ChangedFile { + path: string; + /** The path at the base commit. GitHub sets it only for renamed and copied files. */ + previousPath?: string; + status: FileStatus; + /** The blob SHA at the head commit, or the deleted blob's SHA for a removed file. */ + blobSha: string; + additions: number; + deletions: number; + /** GitHub's unified-diff hunks. GitHub leaves it out for binary files and pure renames. */ + patch?: string; +} + +/** + * Where the renderer reads a pull request's files from. + * Production reads the GitHub API. Tests read a recorded snapshot. + */ +export interface RepoFileSource { + /** Lists the files that changed between the pull request's base and head commits. */ + listChangedFiles(pull: PullRequestRef): Promise; + /** Returns a file's bytes at a commit, or null when the path does not exist at that commit. */ + readFile(repo: RepoRef, path: string, commitSha: string): Promise; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ab4a8c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "erasableSyntaxOnly": true, + "types": ["node"] + }, + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist", "content", "tests/fixtures"] +} From 9452b14175b6eeac65e4dda60e1989b6f0906c66 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Wed, 28 Oct 2026 23:44:09 -0400 Subject: [PATCH 02/58] test: record the shaders#264 snapshot for the fixture source The fixture guide `tests/fixtures/guides/shaders/264.md` has an overview and two steps. The snapshot holds the compare file list for #264 and every changed file at its base and head SHAs. `pnpm record-fixture` regenerates it. The tests own this guide, so the sample on `content` can change without breaking the specs. Co-Authored-By: Claude Opus 5.5 --- tests/fixtures/guides/shaders/264.md | 48 +++ ...c41703117a566f6e181dd8aec5ec7ce889521.json | 206 ++++++++++ .../GLOSSARY.md | 109 +++++ .../docs/src/app/components/demo-registry.tsx | 368 +++++++++++++++++ .../packages/shaders/src/components/index.ts | 79 ++++ .../packages/shaders/src/engine.ts | 124 ++++++ .../scripts/build-posters.sh | 174 ++++++++ .../.changeset/fluted-glass.md | 16 + .../GLOSSARY.md | 127 ++++++ .../docs-tests/visual/fluted-glass.spec.ts | 254 ++++++++++++ .../fluted-glass-default-chromium-darwin.png | Bin 0 -> 128114 bytes .../fluted-glass-default-chromium-linux.png | Bin 0 -> 128123 bytes .../apps/docs/public/posters/fluted-glass.jpg | Bin 0 -> 88179 bytes .../public/posters/fluted-glass.thumb.webp | Bin 0 -> 1044 bytes .../docs/src/app/components/demo-registry.tsx | 386 ++++++++++++++++++ .../src/app/components/fluted-glass/demo.tsx | 90 ++++ .../src/app/components/fluted-glass/params.ts | 21 + .../src/app/components/fluted-glass/scene.tsx | 32 ++ .../benchmark-scene.tsx | 75 ++++ .../dev/fluted-glass-benchmark/page.dev.tsx | 12 + .../app/dev/fluted-glass-probe/page.dev.tsx | 11 + .../dev/fluted-glass-probe/probe-scene.tsx | 76 ++++ ...ffects-resample-by-wrapping-the-sampler.md | 16 + .../fluted-glass/fluted-glass.test.tsx | 274 +++++++++++++ .../components/fluted-glass/fluted-glass.tsx | 120 ++++++ .../src/components/fluted-glass/shader.tsx | 326 +++++++++++++++ .../packages/shaders/src/components/index.ts | 82 ++++ .../packages/shaders/src/engine.ts | 130 ++++++ .../flute-surface/flute-surface.test.ts | 53 +++ .../primitives/flute-surface/flute-surface.ts | 272 ++++++++++++ .../scripts/build-posters.sh | 182 +++++++++ 31 files changed, 3663 insertions(+) create mode 100644 tests/fixtures/guides/shaders/264.md create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/compare/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa...84cc41703117a566f6e181dd8aec5ec7ce889521.json create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/GLOSSARY.md create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/apps/docs/src/app/components/demo-registry.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/packages/shaders/src/components/index.ts create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/packages/shaders/src/engine.ts create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/scripts/build-posters.sh create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/.changeset/fluted-glass.md create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/GLOSSARY.md create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts-snapshots/fluted-glass-default-chromium-darwin.png create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts-snapshots/fluted-glass-default-chromium-linux.png create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/public/posters/fluted-glass.jpg create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/public/posters/fluted-glass.thumb.webp create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/demo-registry.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/demo.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/params.ts create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/scene.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-benchmark/benchmark-scene.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-benchmark/page.dev.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-probe/page.dev.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-probe/probe-scene.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/docs/adr/0004-effects-resample-by-wrapping-the-sampler.md create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/fluted-glass.test.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/fluted-glass.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/shader.tsx create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/index.ts create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/engine.ts create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/primitives/flute-surface/flute-surface.test.ts create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/primitives/flute-surface/flute-surface.ts create mode 100644 tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/scripts/build-posters.sh diff --git a/tests/fixtures/guides/shaders/264.md b/tests/fixtures/guides/shaders/264.md new file mode 100644 index 0000000..60e468b --- /dev/null +++ b/tests/fixtures/guides/shaders/264.md @@ -0,0 +1,48 @@ +--- +repo: campdotdev/shaders +pr: 264 +title: "feat(shaders): add FlutedGlass" +scope: shaders +baseSha: 60b67b513ab1a21d2e33abc9dbf7b7b342c775aa +headSha: 84cc41703117a566f6e181dd8aec5ec7ce889521 +afterTheFact: true +--- + +## Overview + +### Why + +The homepage's Composable card stacks a Source, FlutedGlass, and Grain. Shaders had no reeded-glass Effect to put in the middle. + +### Behavior change + +`` is a new Effect. Mount it after a Source inside a ``, and it shows that Source through strips of glass. Each flute bends the image beneath it and catches the light along one wall. Mode 2 users get the same math as the `fluteSurface` primitive. + +### Checks + +- Unit tests for the component and for `fluteSurface`. +- A visual-regression baseline, `fluted-glass-default`, in `apps/docs-tests`. +- Five pixel checks against the stripe probe at `/dev/fluted-glass-probe`. +- A GPU budget run on `/dev/fluted-glass-benchmark`. FlutedGlass over MeshGradient adds 0.04 ms against a 0.2 ms budget. + +### Not covered + +A UV warp mounted before FlutedGlass, such as Dither, hands the glass a coordinate it already moved. SHA-267 changes how UV warps stack. + +## Mount FlutedGlass after a Source + +`FlutedGlass` is an Effect. It bends the image that the Sources before it drew, so it goes after them in a ``. The component owns the props and their defaults, then hands them to `FlutedGlassShader`. + +[](packages/shaders/src/components/fluted-glass/fluted-glass.tsx#L91-L100) + +The package index exports the component and its prop types. + +[](packages/shaders/src/components/index.ts#L37-L39) + +## Shape each flute in one primitive + +`fluteSurface` works out how steep the glass is and how far it moves the image, in one call. The light reads the same slope, so it lands where the image bends. The engine exports the primitive for Mode 2. + +[](packages/shaders/src/primitives/flute-surface/flute-surface.ts#L108-L115) + +[](packages/shaders/src/engine.ts#L70-L75) diff --git a/tests/fixtures/repo-files/campdotdev/shaders/compare/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa...84cc41703117a566f6e181dd8aec5ec7ce889521.json b/tests/fixtures/repo-files/campdotdev/shaders/compare/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa...84cc41703117a566f6e181dd8aec5ec7ce889521.json new file mode 100644 index 0000000..231d409 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/compare/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa...84cc41703117a566f6e181dd8aec5ec7ce889521.json @@ -0,0 +1,206 @@ +[ + { + "path": ".changeset/fluted-glass.md", + "status": "added", + "blobSha": "e884e63a84cfff2a059e70f674e1da5720d01852", + "additions": 16, + "deletions": 0, + "patch": "@@ -0,0 +1,16 @@\n+---\n+'@camp-dev/shaders': minor\n+---\n+\n+Add `FlutedGlass`, an Effect that shows the image beneath it through repeating flutes of glass. Mount it after a Source inside a `ShaderScene`. Each flute bends the image under it into a squeezed copy, catches a highlight along the wall that faces the light, and darkens near its grooves where the glass turns steep. Its props:\n+\n+- `fluteWidth` sets each flute's width in CSS pixels.\n+- `angle` sets the direction the flutes run, in the degrees `LinearGradient` uses, so the default of 90 gives vertical flutes.\n+- `refraction` sets how far the glass bends the image, and `refraction={0}` leaves it unchanged.\n+- `crossSection` sets each flute's shape, from a pointed `'prism'` through a `'round'` lens to a `'flat'` top with steep walls, or any number from 0 to 1 between them.\n+- `lightAngle` sets where the light comes from, in degrees that turn the same way as `angle`: 0 from the right and 90 from above. It defaults to 135, the upper left. The light stays put while `angle` turns the glass.\n+- `highlight` and `shadow` set how strong the highlight and the shadow are, from 0 to 1. `highlightColor` tints the highlight.\n+\n+Every prop except `highlightColor` takes a static value or an animation signal. Near the canvas edge, the glass mirrors the image back onto the canvas. `FlutedGlass` bends only what Sources drew, never the output of an Effect such as `Grain`, so mount such Effects after it. It works in Mode 1 only.\n+\n+Add the `fluteSurface` primitive for Mode 2. For a position, it returns the bend to add before you sample your own texture, the slope and steepness of the glass, and how far the position sits from its flute's center." + }, + { + "path": "GLOSSARY.md", + "status": "modified", + "blobSha": "2f579a5449f1a4ed1cd95dcf14221a5e4a665274", + "additions": 18, + "deletions": 0, + "patch": "@@ -18,6 +18,10 @@ _Avoid_: Overlay, post-process pass, post-process layer, filter\n An Effect's change to where the image beneath it is sampled, such as Dither's pixel snap or CursorRipple's refraction. A UV warp bends only what Sources drew.\n _Avoid_: Warp on its own, base UV transform\n \n+**Resample**:\n+An Effect's rule for building each pixel from samples of what the Sources beneath it drew, such as FlutedGlass's color split. A UV warp is a resample that takes one sample.\n+_Avoid_: Gather, sampler transform, tap stage\n+\n **Input**:\n A value that changes over time and is not drawn, such as the cursor, scroll, or canvas size. An Input is fed to a component as an animation signal.\n _Avoid_: Cursor hook, entry point, scene-level field\n@@ -74,6 +78,20 @@ _Avoid_: Preset shape, default shape\n A mark the caller supplies as inline SVG markup. Only its coverage is read, so it takes the field's color like any other mark.\n _Avoid_: Custom shape, user shape, icon\n \n+### Fluted glass\n+\n+**Flute**:\n+One ridge of FlutedGlass: a strip of glass with a fixed width in CSS pixels that shows a squeezed copy of the image beneath it. Flutes repeat side by side across the glass.\n+_Avoid_: Reed, rib, bar, stripe, cell\n+\n+**Cross-section**:\n+The shape of one flute seen from its end, on a scale from a pointed prism through a round lens to a flat top with steep walls.\n+_Avoid_: Profile, distortion shape, preset\n+\n+**Groove**:\n+The line where two neighboring flutes meet, where the glass is steepest.\n+_Avoid_: Seam, edge, crease\n+\n ### Docs site\n \n **Search trigger**:" + }, + { + "path": "apps/docs-tests/visual/fluted-glass.spec.ts", + "status": "added", + "blobSha": "3a3aa7bb318720c796953fb2c39365442d820f1f", + "additions": 254, + "deletions": 0, + "patch": "@@ -0,0 +1,254 @@\n+import type { Page } from '@playwright/test';\n+\n+import { expect, test } from './fixtures';\n+import { waitForShader } from './helpers';\n+\n+test('FlutedGlass โ€” default story', async ({ page }) => {\n+ await page.goto('/components/fluted-glass?visualTest=1');\n+ await waitForShader(page);\n+ await expect(page.locator('canvas').first()).toHaveScreenshot('fluted-glass-default.png');\n+});\n+\n+// The probe draws hard diagonal stripes under FlutedGlass at its defaults.\n+// Query params set the glass's dials, drop the glass with glass=none, or drop\n+// the stripes with source=none.\n+const PROBE = '/dev/fluted-glass-probe?visualTest=1';\n+// FlutedGlass's default flute width in CSS pixels, which is device pixels at\n+// the config's deviceScaleFactor of 1. A flute's center sits on the canvas\n+// center, so the grooves fall half a flute either side of it and every flute\n+// width beyond.\n+const FLUTE_WIDTH = 40;\n+// How far either side of a groove each sample column sits, in pixels. Wide\n+// enough to step clear of the groove's own pixel, narrow enough that\n+// unbroken stripes rarely change between the two columns.\n+const COLUMN_OFFSET = 3;\n+// The least the highlight must brighten the walls that face the light, in\n+// brightness levels from 0 to 255, averaged over those walls. The defaults\n+// measured about 18 on macOS, so this leaves room for another GPU.\n+const GAIN_ON_LIT_WALLS = 6;\n+\n+/** The probe's canvas as each pixel's brightness, the mean of its channels. */\n+interface Brightness {\n+ width: number;\n+ /** Brightness from 0 to 255 at a pixel. */\n+ at: (x: number, y: number) => number;\n+ /** The middle band of rows, clear of the site header drawn over the canvas. */\n+ rows: number[];\n+}\n+\n+/**\n+ * Loads the probe with `query` appended and reads its canvas. The screenshot\n+ * decodes in the page, which has the browser's PNG decoder, and comes back\n+ * one byte of brightness per pixel.\n+ */\n+async function probeBrightness(page: Page, query: string): Promise {\n+ await page.goto(`${PROBE}${query}`);\n+ await page.locator('canvas').first().waitFor();\n+ await waitForShader(page);\n+\n+ const shot = await page.locator('canvas').first().screenshot();\n+ const { width, height, bytes } = await page.evaluate(async (pngBase64) => {\n+ const response = await fetch(`data:image/png;base64,${pngBase64}`);\n+ const bitmap = await createImageBitmap(await response.blob());\n+ const canvas = document.createElement('canvas');\n+\n+ canvas.width = bitmap.width;\n+ canvas.height = bitmap.height;\n+ const context = canvas.getContext('2d')!;\n+\n+ context.drawImage(bitmap, 0, 0);\n+ const rgba = context.getImageData(0, 0, bitmap.width, bitmap.height).data;\n+ let binary = '';\n+\n+ for (let index = 0; index < rgba.length; index += 4) {\n+ const sum = (rgba[index] ?? 0) + (rgba[index + 1] ?? 0) + (rgba[index + 2] ?? 0);\n+\n+ binary += String.fromCharCode(Math.round(sum / 3));\n+ }\n+\n+ return { width: bitmap.width, height: bitmap.height, bytes: btoa(binary) };\n+ }, shot.toString('base64'));\n+ const pixels = Buffer.from(bytes, 'base64');\n+ const rows: number[] = [];\n+\n+ for (let y = Math.floor(height * 0.2); y < Math.floor(height * 0.8); y += 1) rows.push(y);\n+\n+ return { width, at: (x, y) => pixels[y * width + x] ?? 0, rows };\n+}\n+\n+/** The x of every groove on a canvas `width` pixels wide, left to right. */\n+function grooves(width: number): number[] {\n+ const center = width / 2;\n+ const first =\n+ center + FLUTE_WIDTH / 2 - Math.floor((center + FLUTE_WIDTH / 2) / FLUTE_WIDTH) * FLUTE_WIDTH;\n+ const positions: number[] = [];\n+\n+ for (let groove = first; groove < width; groove += FLUTE_WIDTH) {\n+ positions.push(Math.round(groove));\n+ }\n+\n+ return positions;\n+}\n+\n+/**\n+ * The share of rows where the pixels just left and just right of a groove\n+ * fall on different sides of mid-gray, over every groove on the canvas. On\n+ * unbroken stripes the two columns differ only where a stripe edge passes\n+ * between them. Through the glass they read the image from far apart, so\n+ * they differ far more often.\n+ */\n+function grooveBreaks(image: Brightness): number {\n+ let pairs = 0;\n+ let breaks = 0;\n+\n+ for (const groove of grooves(image.width)) {\n+ const left = groove - COLUMN_OFFSET;\n+ const right = groove + COLUMN_OFFSET;\n+\n+ if (left < 0 || right >= image.width) continue;\n+ for (const y of image.rows) {\n+ pairs += 1;\n+ if (image.at(left, y) > 128 !== image.at(right, y) > 128) breaks += 1;\n+ }\n+ }\n+\n+ return breaks / pairs;\n+}\n+\n+/**\n+ * How much brighter `lit` is than `unlit` on average, over the left half and\n+ * the right half of every whole flute, two pixels clear of the grooves and\n+ * the center.\n+ */\n+function wallGain(lit: Brightness, unlit: Brightness): { left: number; right: number } {\n+ let left = 0;\n+ let right = 0;\n+ let count = 0;\n+\n+ for (const groove of grooves(lit.width)) {\n+ if (groove + FLUTE_WIDTH >= lit.width) continue;\n+ for (const y of lit.rows) {\n+ for (let step = 2; step <= FLUTE_WIDTH / 2 - 2; step += 1) {\n+ const leftX = groove + step;\n+ const rightX = groove + FLUTE_WIDTH - step;\n+\n+ left += lit.at(leftX, y) - unlit.at(leftX, y);\n+ right += lit.at(rightX, y) - unlit.at(rightX, y);\n+ count += 1;\n+ }\n+ }\n+ }\n+\n+ return { left: left / count, right: right / count };\n+}\n+\n+/**\n+ * The x of every column `near` to `far` pixels out from a groove, on both\n+ * sides of every groove on a canvas `width` pixels wide. Column 0 touches\n+ * the groove.\n+ */\n+function columnsNearGrooves(width: number, near: number, far: number): number[] {\n+ const columns: number[] = [];\n+\n+ for (const groove of grooves(width)) {\n+ for (let step = near; step <= far; step += 1) {\n+ columns.push(groove - 1 - step, groove + step);\n+ }\n+ }\n+\n+ return columns.filter((x) => x >= 0 && x < width);\n+}\n+\n+/**\n+ * The share of `unshaded`'s brightness that `shaded` loses, over the columns\n+ * `near` to `far` pixels out from every groove.\n+ */\n+function shadowShare(shaded: Brightness, unshaded: Brightness, near: number, far: number): number {\n+ let lost = 0;\n+ let total = 0;\n+\n+ for (const x of columnsNearGrooves(shaded.width, near, far)) {\n+ for (const y of shaded.rows) {\n+ lost += unshaded.at(x, y) - shaded.at(x, y);\n+ total += unshaded.at(x, y);\n+ }\n+ }\n+\n+ return lost / total;\n+}\n+\n+/** The largest brightness difference between two loads, pixel for pixel. */\n+function maxDifference(first: Brightness, second: Brightness): number {\n+ let largest = 0;\n+\n+ for (const y of first.rows) {\n+ for (let x = 0; x < first.width; x += 1) {\n+ largest = Math.max(largest, Math.abs(first.at(x, y) - second.at(x, y)));\n+ }\n+ }\n+\n+ return largest;\n+}\n+\n+// A regenerated baseline of inert glass would pass its own screenshot, so\n+// this checks that the glass breaks the stripes at all. Both loads turn the\n+// light off, since the shadow and highlight also change the columns either\n+// side of a groove, and this measures the bend alone. The same probe with\n+// refraction 0 is the inert reference. At 1280 px wide a stripe repeats every\n+// 113 px across a row, so the two columns differ on about 11% of rows without\n+// the glass. Through it, they read the image 28 px apart, and differ on\n+// about half.\n+test('FlutedGlass โ€” stripes break at the grooves', async ({ page }) => {\n+ const inert = grooveBreaks(await probeBrightness(page, '&highlight=0&shadow=0&refraction=0'));\n+ const glass = grooveBreaks(await probeBrightness(page, '&highlight=0&shadow=0'));\n+\n+ expect(glass, `groove breaks: glass ${glass}, inert ${inert}`).toBeGreaterThan(inert + 0.2);\n+});\n+\n+// The default light comes from the upper left, so over vertical flutes the\n+// left wall of each flute faces it and catches the highlight, and the right\n+// wall faces away. Both loads turn the shadow off, so the highlight is the\n+// only difference between them.\n+test('FlutedGlass โ€” the highlight lights the walls that face the light', async ({ page }) => {\n+ const unlit = await probeBrightness(page, '&highlight=0&shadow=0');\n+ const lit = await probeBrightness(page, '&shadow=0');\n+ const gain = wallGain(lit, unlit);\n+ const message = `brightness gain: left walls ${gain.left}, right walls ${gain.right}`;\n+\n+ expect(gain.left, message).toBeGreaterThan(GAIN_ON_LIT_WALLS);\n+ expect(gain.right, message).toBeLessThan(gain.left / 4);\n+});\n+\n+// A flat top is flat across its middle and steep only at its walls, so its\n+// shadow belongs on the walls. Both loads turn the highlight off, and only\n+// the shadow differs between them. Columns 0 and 1 out from a groove sit on\n+// the walls. Columns 4 to 9 sit a fifth to a half of the way to the center:\n+// inside the shadow's reach, but on glass that is flat.\n+test('FlutedGlass โ€” on a flat top the shadow stays on the walls', async ({ page }) => {\n+ const unshaded = await probeBrightness(page, '&crossSection=flat&highlight=0&shadow=0');\n+ const shaded = await probeBrightness(page, '&crossSection=flat&highlight=0&shadow=1');\n+ const walls = shadowShare(shaded, unshaded, 0, 1);\n+ const flatTop = shadowShare(shaded, unshaded, 4, 9);\n+ const message = `share of brightness the shadow takes: walls ${walls}, flat top ${flatTop}`;\n+\n+ expect(walls, message).toBeGreaterThan(0.1);\n+ expect(flatTop, message).toBeLessThan(0.02);\n+});\n+\n+// With nothing to bend and no light, the glass draws the Source exactly as\n+// the Source alone does.\n+test('FlutedGlass โ€” inert glass leaves the Source unchanged', async ({ page }) => {\n+ const source = await probeBrightness(page, '&glass=none');\n+ const inert = await probeBrightness(page, '&refraction=0&highlight=0&shadow=0');\n+\n+ expect(maxDifference(source, inert)).toBeLessThanOrEqual(1);\n+});\n+\n+// Over a transparent scene the canvas shows the probe page's black\n+// background. Light added to a pixel with no coverage would show on it.\n+test('FlutedGlass โ€” a transparent pixel gets no highlight', async ({ page }) => {\n+ const black = await probeBrightness(page, '&glass=none&source=none');\n+ const glass = await probeBrightness(page, '&source=none&highlight=1&shadow=1');\n+\n+ expect(maxDifference(black, glass)).toBeLessThanOrEqual(1);\n+});" + }, + { + "path": "apps/docs-tests/visual/fluted-glass.spec.ts-snapshots/fluted-glass-default-chromium-darwin.png", + "status": "added", + "blobSha": "92c353529320beb06ed76383e9475b1039319b8f", + "additions": 0, + "deletions": 0 + }, + { + "path": "apps/docs-tests/visual/fluted-glass.spec.ts-snapshots/fluted-glass-default-chromium-linux.png", + "status": "added", + "blobSha": "b1cac8f14fe2435667dc96659ef99174b50cb1de", + "additions": 0, + "deletions": 0 + }, + { + "path": "apps/docs/public/posters/fluted-glass.jpg", + "status": "added", + "blobSha": "023db30456924b3415a6611e7d4276d7421ff5da", + "additions": 0, + "deletions": 0 + }, + { + "path": "apps/docs/public/posters/fluted-glass.thumb.webp", + "status": "added", + "blobSha": "20629d8933fc39d0bbc02e76e075bceb8007139e", + "additions": 0, + "deletions": 0 + }, + { + "path": "apps/docs/src/app/components/demo-registry.tsx", + "status": "modified", + "blobSha": "f315a6db10fee6b2db0104c57d51f35bbfce5b7e", + "additions": 18, + "deletions": 0, + "patch": "@@ -17,6 +17,7 @@ import { CursorSpotlightIsland } from './cursor-spotlight/demo';\n import { DissolveIsland } from './dissolve/demo';\n import { DitherIsland } from './dither/demo';\n import { DotFieldIsland } from './dot-field/demo';\n+import { FlutedGlassIsland } from './fluted-glass/demo';\n import { FractalNoiseIsland } from './fractal-noise/demo';\n import { GodRaysIsland } from './god-rays/demo';\n import { GrainIsland } from './grain/demo';\n@@ -217,6 +218,23 @@ export const COMPONENT_PAGES: Record = {\n \n `,\n },\n+ 'fluted-glass': {\n+ Island: FlutedGlassIsland,\n+ copySiblings: [''],\n+ usageSnippet: `\n+ \n+ \n+ \n+`,\n+ usageNotes: (\n+ <>\n+ FlutedGlass bends only what Sources drew, never the output of an Effect such as Grain, so\n+ mount Grain after the glass, where the order you write matches what you see. Mode 1 only: it\n+ needs the scene's output stage. In your own r3f <Canvas>, the{' '}\n+ fluteSurface primitive gives you the same flute math.\n+ \n+ ),\n+ },\n 'fractal-noise': {\n Island: FractalNoiseIsland,\n usageSnippet: `" + }, + { + "path": "apps/docs/src/app/components/fluted-glass/demo.tsx", + "status": "added", + "blobSha": "b6eea96f79b7847068988bbdbfa20f10373c6238", + "additions": 90, + "deletions": 0, + "patch": "@@ -0,0 +1,90 @@\n+'use client';\n+\n+/**\n+ * Fluted Glass demo island: the interactive slice of the Fluted Glass page,\n+ * holding the control store, the shader preview, and the control panel. The\n+ * shared components/[slug] template renders this between its static header\n+ * and prose sections.\n+ */\n+import dynamic from 'next/dynamic';\n+import { useMemo } from 'react';\n+\n+import {\n+ ColorInput,\n+ ControlPanel,\n+ ControlsProvider,\n+ createControlStore,\n+ DemoLayout,\n+ Section,\n+ SliderInput,\n+ useSnapshot,\n+} from '@/components/controls';\n+import { DemoPoster } from '@/components/DemoPoster';\n+import { VisualTestPause } from '@/lib/visualTestHooks';\n+\n+import { type FlutedGlassParams, INITIAL } from './params';\n+\n+const FlutedGlassScene = dynamic(() => import('./scene'), { ssr: false });\n+\n+/**\n+ * The CSS size the poster was captured at in build-posters.sh, the widest\n+ * demo box. The flutes are sized in CSS pixels, so DemoPoster's pixel-locked\n+ * mode shows the image at this size and lets the box crop it. Cover-scaling\n+ * would change the flutes' spacing, and the grooves would jump when the live\n+ * scene replaced the poster.\n+ */\n+const POSTER_SIZE: readonly [number, number] = [896, 598];\n+\n+/**\n+ * Reads the live params and renders the scene. Split out from the island so\n+ * it subscribes to the store on its own: the island never re-renders during\n+ * a drag, only this and the moved control do.\n+ */\n+function FlutedGlassDemo() {\n+ const params = useSnapshot();\n+\n+ return (\n+ \n+ \n+ \n+ \n+ \n+ );\n+}\n+\n+function FlutedGlassControls() {\n+ return (\n+ \n+
\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+ \n+ \n+ \n+
\n+
\n+ );\n+}\n+\n+export function FlutedGlassIsland() {\n+ const store = useMemo(() => createControlStore(INITIAL), []);\n+\n+ return (\n+ \n+ }>\n+
\n+ \n+
\n+
\n+
\n+ );\n+}" + }, + { + "path": "apps/docs/src/app/components/fluted-glass/params.ts", + "status": "added", + "blobSha": "5fe1544d3173b4969852926f4e0fb17fc91ba955", + "additions": 21, + "deletions": 0, + "patch": "@@ -0,0 +1,21 @@\n+export interface FlutedGlassParams {\n+ fluteWidth: number;\n+ angle: number;\n+ refraction: number;\n+ crossSection: number;\n+ lightAngle: number;\n+ highlight: number;\n+ highlightColor: string;\n+ shadow: number;\n+}\n+\n+export const INITIAL: FlutedGlassParams = {\n+ fluteWidth: 40,\n+ angle: 90,\n+ refraction: 1,\n+ crossSection: 0.5,\n+ lightAngle: 135,\n+ highlight: 0.45,\n+ highlightColor: 'oklch(1 0 0)',\n+ shadow: 0.2,\n+};" + }, + { + "path": "apps/docs/src/app/components/fluted-glass/scene.tsx", + "status": "added", + "blobSha": "740dfd9915b544117330dd8484d4a79fa6e04301", + "additions": 32, + "deletions": 0, + "patch": "@@ -0,0 +1,32 @@\n+'use client';\n+\n+import type { ReactNode } from 'react';\n+\n+import { FlutedGlass, MeshGradient, ShaderScene } from '@camp-dev/shaders';\n+\n+import { type FlutedGlassParams, INITIAL } from './params';\n+\n+export default function FlutedGlassScene({\n+ params = INITIAL,\n+ children,\n+}: {\n+ params?: FlutedGlassParams;\n+ children?: ReactNode;\n+} = {}) {\n+ return (\n+ \n+ \n+ \n+ {children}\n+ \n+ );\n+}" + }, + { + "path": "apps/docs/src/app/dev/fluted-glass-benchmark/benchmark-scene.tsx", + "status": "added", + "blobSha": "d96d83b983cbec022a5b6d08ad896e372ade26ab", + "additions": 75, + "deletions": 0, + "patch": "@@ -0,0 +1,75 @@\n+'use client';\n+\n+// Benchmark for FlutedGlass with ShaderMonitor's GPU readout (SHA-274):\n+// MeshGradient under FlutedGlass in the Composable card's 485 by 308\n+// CSS-pixel window, where the glass must add under 0.2 ms at 2x.\n+// `?glass=none` drops the glass, for the reading to subtract, and `?dpr=2`\n+// caps the canvas at two pixels per CSS pixel, anything else at one.\n+import { useState } from 'react';\n+\n+import {\n+ FlutedGlass,\n+ MeshGradient,\n+ ShaderMonitor,\n+ ShaderScene,\n+ useStaticSceneHint,\n+} from '@camp-dev/shaders';\n+\n+const WIDTH = 485;\n+const HEIGHT = 308;\n+\n+interface Settings {\n+ pixelRatio: number;\n+ glass: boolean;\n+}\n+\n+const readSettings = (): Settings => {\n+ const search = new URLSearchParams(window.location.search);\n+\n+ return {\n+ pixelRatio: search.get('dpr') === '2' ? 2 : 1,\n+ glass: search.get('glass') !== 'none',\n+ };\n+};\n+\n+/**\n+ * Votes the scene animated, so a still scene keeps drawing frames and the GPU\n+ * readout averages many frames instead of settling on one.\n+ */\n+function KeepDrawing() {\n+ useStaticSceneHint(false);\n+\n+ return null;\n+}\n+\n+// Time each reading with no other scene drawing, since a second scene\n+// inflates it (the skewed-readout gotcha in docs/agents/tsl.md). The links\n+// are full page loads, so each measurement starts from a fresh renderer.\n+export default function BenchmarkScene() {\n+ const [{ pixelRatio, glass }] = useState(readSettings);\n+ /** A link to this page with some settings changed. */\n+ const href = (changes: Partial) => {\n+ const next = { pixelRatio, glass, ...changes };\n+\n+ return `?dpr=${next.pixelRatio}${next.glass ? '' : '&glass=none'}`;\n+ };\n+\n+ return (\n+
\n+

\n+ MeshGradient {glass ? 'under FlutedGlass' : 'alone'} at {WIDTH} ร— {HEIGHT} CSS px, pixel\n+ ratio capped at {pixelRatio}x. 1x ยท{' '}\n+ 2x ยท glass ยท{' '}\n+ no glass\n+

\n+
\n+ \n+ \n+ {glass && }\n+ \n+ \n+ \n+
\n+
\n+ );\n+}" + }, + { + "path": "apps/docs/src/app/dev/fluted-glass-benchmark/page.dev.tsx", + "status": "added", + "blobSha": "eb5892eac6af5310c6fa7323d3670c13563e1c33", + "additions": 12, + "deletions": 0, + "patch": "@@ -0,0 +1,12 @@\n+'use client';\n+\n+import dynamic from 'next/dynamic';\n+\n+// three/webgpu references `self` at module load and cannot SSR, so the\n+// benchmark scene is loaded client-only. Dev route: invisible to production\n+// builds.\n+const BenchmarkScene = dynamic(() => import('./benchmark-scene'), { ssr: false });\n+\n+export default function FlutedGlassBenchmarkPage() {\n+ return ;\n+}" + }, + { + "path": "apps/docs/src/app/dev/fluted-glass-probe/page.dev.tsx", + "status": "added", + "blobSha": "f3a0cee7f742029cf374ffdce3f918571a1d2037", + "additions": 11, + "deletions": 0, + "patch": "@@ -0,0 +1,11 @@\n+'use client';\n+\n+import dynamic from 'next/dynamic';\n+\n+// three/webgpu references `self` at module load and cannot SSR, so the probe\n+// scene is loaded client-only. Dev route: invisible to production builds.\n+const ProbeScene = dynamic(() => import('./probe-scene'), { ssr: false });\n+\n+export default function FlutedGlassProbePage() {\n+ return ;\n+}" + }, + { + "path": "apps/docs/src/app/dev/fluted-glass-probe/probe-scene.tsx", + "status": "added", + "blobSha": "9ad5a0392177b20bf1937662e18ffd4a49d20ba5", + "additions": 76, + "deletions": 0, + "patch": "@@ -0,0 +1,76 @@\n+'use client';\n+\n+// Probe for FlutedGlass: hard diagonal stripes seen through the glass at its\n+// defaults. A smooth Source hides what the glass does, but stripes show it:\n+// each flute holds a squeezed copy of the stripes under it, and the stripes\n+// jump sideways at every groove. The visual spec's pixel checks load it.\n+import { type CrossSectionName, FlutedGlass, LinearGradient, ShaderScene } from '@camp-dev/shaders';\n+\n+import { VisualTestPause } from '@/lib/visualTestHooks';\n+\n+// Each pass of the gradient is half dark and half light, with a switch at\n+// its middle a fraction of a pixel wide. colorRamp skips a segment of zero\n+// width, so the two middle stops sit 0.002 of a pass apart rather than on\n+// one position.\n+const STRIPE_STOPS = [\n+ { color: 'oklch(0.2 0 0)', position: 0 },\n+ { color: 'oklch(0.2 0 0)', position: 0.5 },\n+ { color: 'oklch(0.95 0 0)', position: 0.502 },\n+ { color: 'oklch(0.95 0 0)', position: 1 },\n+];\n+\n+// Query params override the glass's dials, such as ?refraction=0&angle=30 or\n+// ?crossSection=prism, for checking one by eye. ?highlight=0&shadow=0 turns\n+// the light off, to see the bend alone. ?glass=none drops the glass, to\n+// compare against the Source alone, and ?source=none drops the stripes,\n+// leaving the glass over a transparent scene.\n+\n+/** A value from the page's query string, or null when it is absent. */\n+function query(name: string): string | null {\n+ return new URLSearchParams(window.location.search).get(name);\n+}\n+\n+/** A number from the page's query string, or undefined when it is absent. */\n+function queryNumber(name: string): number | undefined {\n+ const raw = query(name);\n+\n+ return raw === null ? undefined : Number(raw);\n+}\n+\n+/** Narrows a query string value to one of the cross-section's names. */\n+function isCrossSectionName(value: string): value is CrossSectionName {\n+ return value === 'prism' || value === 'round' || value === 'flat';\n+}\n+\n+/** The cross-section from the query string, as a name or a number. */\n+function queryCrossSection(): CrossSectionName | number | undefined {\n+ const raw = query('crossSection');\n+\n+ if (raw === null) return undefined;\n+\n+ return isCrossSectionName(raw) ? raw : Number(raw);\n+}\n+\n+export default function ProbeScene() {\n+ return (\n+
\n+ \n+ {query('source') !== 'none' && (\n+ \n+ )}\n+ {query('glass') !== 'none' && (\n+ \n+ )}\n+ \n+ \n+
\n+ );\n+}" + }, + { + "path": "apps/docs/src/content/components.ts", + "status": "modified", + "blobSha": "3c32dc9da610930d344c5309f619d40184eecf56", + "additions": 5, + "deletions": 0, + "patch": "@@ -63,6 +63,11 @@ export const COMPONENTS = {\n 'Tiled dot field with center-anchored ripple displacement. The architecturally-validating Shaders component.',\n category: 'patterns',\n },\n+ 'fluted-glass': {\n+ description:\n+ 'A pane of fluted glass over the scene. Each flute bends the image beneath it into a squeezed copy and catches the light along one wall. Stacks on top of any Source inside .',\n+ category: 'warp',\n+ },\n 'fractal-noise': {\n description:\n 'Layered fractal noise. Multi-octave texture with a style dial from soft clouds to crisp vein networks.'," + }, + { + "path": "apps/docs/src/content/taxonomy.ts", + "status": "modified", + "blobSha": "9d0c09e0a202e47dd8112e5302eba7eb1edb0db5", + "additions": 2, + "deletions": 0, + "patch": "@@ -28,6 +28,7 @@ const CATEGORY_SLUGS = [\n 'noise',\n 'patterns',\n 'scenes',\n+ 'warp',\n 'lens-film',\n 'retro-glitch',\n 'transitions',\n@@ -41,6 +42,7 @@ export const CATEGORIES: Record\n noise: { label: 'Noise', tier: 'sources' },\n patterns: { label: 'Patterns', tier: 'sources' },\n scenes: { label: 'Scenes', tier: 'sources' },\n+ warp: { label: 'Warp', tier: 'effects' },\n 'lens-film': { label: 'Lens & Film', tier: 'effects' },\n 'retro-glitch': { label: 'Retro & Glitch', tier: 'effects' },\n transitions: { label: 'Transitions', tier: 'effects' }," + }, + { + "path": "docs/adr/0004-effects-resample-by-wrapping-the-sampler.md", + "status": "added", + "blobSha": "d58e13096d0befa5526816e37697201934b6e042", + "additions": 16, + "deletions": 0, + "patch": "@@ -0,0 +1,16 @@\n+# Effects resample the image beneath them by wrapping a sampler, not through render targets\n+\n+Status: Accepted. The output-stage change lands in its own sub-issue of SHA-266, before FlutedGlass's color split and blur.\n+\n+FlutedGlass needs several samples of the image beneath it: three for its color split and twelve for its blur. The output stage gave an Effect one sample. `createOutputStage` passed the screen coordinate through every registered UV warp in turn and read the scene texture once, at the coordinate that came out. We chose to hand each Effect a function that samples everything beneath it, and the Effect returns a new function in its place. The output stage calls the last function once per pixel, at the screen coordinate. The glossary calls the Effect's rule a resample. A UV warp is the resample that calls the function beneath it once, at a moved coordinate, so Dither, CursorRipple, and CursorSmudge keep their code.\n+\n+## Considered options\n+\n+- **A render target per Effect.** Each Effect draws into its own texture, and the next Effect reads it. An Effect could then bend the color transforms mounted before it, such as Grain, which lifts the limit ADR 0002 accepted. The cost is a rewrite of the output stage, plus one full-screen pass and one canvas-sized texture per Effect. It stays the later option ADR 0002 names.\n+- **FlutedGlass reads the scene texture itself.** Rejected. That read skips every UV warp beneath it, so a CursorRipple under the glass would vanish.\n+\n+## Consequences\n+\n+- The stacking order of UV warps flips, and the new order is the right one. The old chain moved the screen coordinate through the first-mounted warp first, so that warp acted as the topmost layer, and `` pixelated the water along with the image beneath it. With wrapped samplers, the last-mounted Effect is the outermost function, so its warp moves the screen coordinate first. No docs page stacks two UV warps, so no visual baseline changes. A unit test pins the order.\n+- Samples multiply. Each sample an Effect takes reruns every warp beneath it. Twelve blur samples times three color-split samples is 36 scene reads per pixel, and a CursorRipple beneath the glass reads its height field four times for each of them. Time a deep stack on WebGPU before you raise a sample count.\n+- A resample still reads only what Sources drew. Color transforms such as Grain and Vignette run after every resample, so glass mounted after Grain leaves the grain unbent. ADR 0002's limit stands." + }, + { + "path": "packages/shaders/src/components/fluted-glass/fluted-glass.test.tsx", + "status": "added", + "blobSha": "4e4d2cd1d7b0d1126b168fa0c8d6742b1acffb1b", + "additions": 274, + "deletions": 0, + "patch": "@@ -0,0 +1,274 @@\n+import { type ReactNode, StrictMode } from 'react';\n+\n+import { render } from '@testing-library/react';\n+import type { WebGPURenderer } from 'three/webgpu';\n+import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest';\n+\n+import { type CrossSectionName, FlutedGlass, type FlutedGlassProps } from '../../index.js';\n+import {\n+ type PostProcessTransform,\n+ ShaderContext,\n+ type ShaderContextValue,\n+ type UvTransform,\n+} from '../../react/context/shader-context.js';\n+import type { AnimatableProp } from '../../react/hooks/animatable-signal/animatable-signal.js';\n+import { createSignal } from '../../react/internal/create-signal.js';\n+import type { SchedulerClient } from '../../runtime/frame-scheduler/frame-scheduler.js';\n+\n+// What the Effect asks of the scene, never what the glass looks like. The\n+// look is the demo page and its visual spec. A fake context counts what the\n+// glass registers on the output stage and how it votes.\n+beforeEach(() => {\n+ vi.stubGlobal('requestAnimationFrame', () => 0);\n+ vi.stubGlobal('cancelAnimationFrame', () => {});\n+});\n+afterEach(() => {\n+ vi.unstubAllGlobals();\n+ vi.restoreAllMocks();\n+});\n+\n+function makeScene() {\n+ const clients: SchedulerClient[] = [];\n+ const uvTransforms: UvTransform[] = [];\n+ const overlays: PostProcessTransform[] = [];\n+ const unregisters = { uv: 0, overlay: 0 };\n+ const renderer = {\n+ domElement: document.createElement('canvas'),\n+ getPixelRatio: () => 2,\n+ } as unknown as WebGPURenderer;\n+ const scheduler = {\n+ idle: true,\n+ add: (client: SchedulerClient) => clients.push(client),\n+ remove: (client: SchedulerClient) => {\n+ const index = clients.indexOf(client);\n+\n+ if (index >= 0) clients.splice(index, 1);\n+ },\n+ requestRender: vi.fn(() => true),\n+ setIdle: vi.fn(() => () => undefined),\n+ };\n+ const shaderContext = {\n+ renderer: { three: renderer },\n+ scene: {},\n+ camera: {},\n+ scheduler,\n+ registerOverlay: (transform: PostProcessTransform) => {\n+ overlays.push(transform);\n+\n+ return () => {\n+ unregisters.overlay += 1;\n+ };\n+ },\n+ registerBaseUvTransform: (transform: UvTransform) => {\n+ uvTransforms.push(transform);\n+\n+ return () => {\n+ unregisters.uv += 1;\n+ };\n+ },\n+ } as unknown as ShaderContextValue;\n+\n+ function Wrapper({ children }: { children: ReactNode }) {\n+ return {children};\n+ }\n+\n+ return { Wrapper, scheduler, uvTransforms, overlays, unregisters };\n+}\n+\n+/** One UV warp and one color transform, neither removed or registered again. */\n+function expectOneOfEach(scene: ReturnType) {\n+ expect(scene.uvTransforms).toHaveLength(1);\n+ expect(scene.overlays).toHaveLength(1);\n+ expect(scene.unregisters).toEqual({ uv: 0, overlay: 0 });\n+}\n+\n+/** A hand-driven animation signal, the shape a MotionValue has. */\n+function makeSignal(initial: number) {\n+ let value = initial;\n+ const channel = createSignal(() => value);\n+\n+ return {\n+ signal: channel.signal,\n+ set(next: number) {\n+ value = next;\n+ for (const listener of channel.listeners) listener(next);\n+ },\n+ };\n+}\n+\n+describe('FlutedGlass', () => {\n+ // The public shape: seven optional dials, each a number or an animation\n+ // signal, where the cross-section also takes a name for a point on its\n+ // dial, plus a highlight color. pnpm typecheck runs this file too, so a\n+ // missing export or a narrowed prop fails there as well as here.\n+ it('takes seven animatable dials and a highlight color from the package root', () => {\n+ expectTypeOf().toEqualTypeOf<'prism' | 'round' | 'flat'>();\n+ expectTypeOf().toEqualTypeOf<{\n+ fluteWidth?: AnimatableProp;\n+ angle?: AnimatableProp;\n+ refraction?: AnimatableProp;\n+ crossSection?: AnimatableProp | CrossSectionName;\n+ lightAngle?: AnimatableProp;\n+ highlight?: AnimatableProp;\n+ highlightColor?: string;\n+ shadow?: AnimatableProp;\n+ }>();\n+ });\n+\n+ // The UV warp bends the image, and the color transform lights the glass.\n+ it('registers one UV warp and one color transform, and removes both on unmount', () => {\n+ const scene = makeScene();\n+ const { unmount } = render(, { wrapper: scene.Wrapper });\n+\n+ expectOneOfEach(scene);\n+\n+ unmount();\n+ expect(scene.unregisters).toEqual({ uv: 1, overlay: 1 });\n+ });\n+\n+ it('survives Strict Mode with one UV warp and one color transform', () => {\n+ const scene = makeScene();\n+\n+ render(\n+ \n+ \n+ ,\n+ { wrapper: scene.Wrapper },\n+ );\n+\n+ expect(scene.uvTransforms.length - scene.unregisters.uv).toBe(1);\n+ expect(scene.overlays.length - scene.unregisters.overlay).toBe(1);\n+ });\n+\n+ // Every dial rides a stable uniform, so a new value is a uniform write\n+ // and a repaint, never a recompile of the output chain.\n+ it('changes its dials without re-registering either pass', () => {\n+ const scene = makeScene();\n+ const { rerender } = render(\n+ ,\n+ { wrapper: scene.Wrapper },\n+ );\n+\n+ scene.scheduler.requestRender.mockClear();\n+ rerender(\n+ ,\n+ );\n+\n+ expectOneOfEach(scene);\n+ expect(scene.scheduler.requestRender).toHaveBeenCalled();\n+ });\n+\n+ it('follows its dial signals without re-registering either pass', () => {\n+ const scene = makeScene();\n+ const fluteWidth = makeSignal(40);\n+ const angle = makeSignal(90);\n+ const refraction = makeSignal(1);\n+ const crossSection = makeSignal(0.5);\n+ const lightAngle = makeSignal(135);\n+ const highlight = makeSignal(0.5);\n+ const shadow = makeSignal(0.3);\n+\n+ render(\n+ ,\n+ { wrapper: scene.Wrapper },\n+ );\n+ scene.scheduler.requestRender.mockClear();\n+\n+ fluteWidth.set(24);\n+ expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(1);\n+ angle.set(30);\n+ expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(2);\n+ refraction.set(0);\n+ expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(3);\n+ crossSection.set(0.2);\n+ expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(4);\n+ lightAngle.set(45);\n+ expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(5);\n+ highlight.set(1);\n+ expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(6);\n+ shadow.set(0);\n+ expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(7);\n+\n+ expectOneOfEach(scene);\n+ });\n+\n+ // The color decodes into a uniform, so a new color repaints the glass\n+ // without rebuilding the color transform.\n+ it('changes its highlight color without re-registering either pass', () => {\n+ const scene = makeScene();\n+ const { rerender } = render(, {\n+ wrapper: scene.Wrapper,\n+ });\n+\n+ for (const highlightColor of ['#ffd27f', 'oklch(0.8 0.1 250)']) {\n+ scene.scheduler.requestRender.mockClear();\n+ rerender();\n+ expect(scene.scheduler.requestRender).toHaveBeenCalled();\n+ }\n+\n+ expectOneOfEach(scene);\n+ });\n+\n+ // A name is a point on the cross-section dial, not a formula of its own,\n+ // so switching names writes the same uniform a number would.\n+ it('switches cross-section names without re-registering either pass', () => {\n+ const scene = makeScene();\n+ const { rerender } = render(, {\n+ wrapper: scene.Wrapper,\n+ });\n+\n+ for (const name of ['round', 'flat', 'prism'] as const) {\n+ scene.scheduler.requestRender.mockClear();\n+ rerender();\n+ expect(scene.scheduler.requestRender).toHaveBeenCalled();\n+ }\n+\n+ expectOneOfEach(scene);\n+ });\n+\n+ // Still glass draws nothing that changes over time, so a scene of still\n+ // Sources under it can park.\n+ it('votes idle and never animated', () => {\n+ const scene = makeScene();\n+\n+ render(, { wrapper: scene.Wrapper });\n+\n+ expect(scene.scheduler.setIdle).toHaveBeenCalledWith(true);\n+ expect(scene.scheduler.setIdle).not.toHaveBeenCalledWith(false);\n+ });\n+\n+ // Mode 1 only: outside a scene there is no output stage to bend, so the\n+ // glass follows its signals without a scheduler to wake and listens to\n+ // nothing on the page.\n+ it('does nothing outside a ShaderScene', () => {\n+ const addListener = vi.spyOn(window, 'addEventListener');\n+ const refraction = makeSignal(1);\n+\n+ render();\n+\n+ expect(() => refraction.set(0.5)).not.toThrow();\n+ expect(addListener).not.toHaveBeenCalled();\n+ });\n+});" + }, + { + "path": "packages/shaders/src/components/fluted-glass/fluted-glass.tsx", + "status": "added", + "blobSha": "d4c4fecdb56b84cca238cf29095f3a1b3fa4c804", + "additions": 120, + "deletions": 0, + "patch": "@@ -0,0 +1,120 @@\n+'use client';\n+\n+// Public face of the fluted glass: owns the props, their JSDoc, and their\n+// defaults, then delegates to FlutedGlassShader (./shader.tsx). FlutedGlass\n+// is an Effect: stack it after a Source inside a and it shows\n+// that Source through a pane of fluted glass, each flute bending the image\n+// beneath it into a squeezed copy and catching the light along one wall.\n+import type { AnimatableProp } from '../../react/hooks/animatable-signal/animatable-signal.js';\n+import { useStaticSceneHint } from '../../react/hooks/use-static-hint/use-static-hint.js';\n+import { FlutedGlassShader } from './shader.js';\n+\n+/** The names for three points on the `crossSection` dial. */\n+export type CrossSectionName = 'prism' | 'round' | 'flat';\n+\n+// Where each name sits on the dial. A name is only a point on the dial, so\n+// it rides the same uniform as a number and animates like one.\n+const CROSS_SECTION_DIAL: Record = {\n+ prism: 0,\n+ round: 0.5,\n+ flat: 1,\n+};\n+\n+export interface FlutedGlassProps {\n+ /**\n+ * The width of one flute, in CSS pixels. Defaults to 40. Accepts a static\n+ * value or an animation signal.\n+ */\n+ fluteWidth?: AnimatableProp;\n+ /**\n+ * The direction the flutes run, in degrees on screen: 0 runs them left to\n+ * right and 90 runs them bottom to top. These are the degrees\n+ * LinearGradient's `angle` uses, though a gradient's bands run across its\n+ * angle and the flutes run along theirs. Defaults to 90, vertical flutes.\n+ * Accepts a static value or an animation signal.\n+ */\n+ angle?: AnimatableProp;\n+ /**\n+ * How far each flute bends the image beneath it, in half flute widths at\n+ * the steepest part of the glass. 0 leaves the image unchanged. At 1, each\n+ * flute shows a squeezed copy of twice its width of image. Defaults to 1.\n+ * Accepts a static value or an animation signal.\n+ */\n+ refraction?: AnimatableProp;\n+ /**\n+ * The shape of each flute seen from its end, from 0 to 1. `'prism'` (0) is\n+ * two flat faces meeting at a ridge, `'round'` (0.5) is a lens, and\n+ * `'flat'` (1) is a flat top with steep walls. Numbers between give the\n+ * shapes between, so a signal can morph one shape into another. Defaults\n+ * to `'round'`. Accepts a static value or an animation signal.\n+ */\n+ crossSection?: AnimatableProp | CrossSectionName;\n+ /**\n+ * Where the light comes from, in degrees counterclockwise on screen, the\n+ * way `angle` turns: 0 from the right, 90 from above, 180 from the left,\n+ * and 270 from below.\n+ * The light stays put on screen while `angle` turns the glass beneath it,\n+ * and the flute wall that faces it catches the highlight. A light that\n+ * runs along the flutes catches neither wall. Defaults to 135, the upper\n+ * left. Accepts a static value or an animation signal.\n+ */\n+ lightAngle?: AnimatableProp;\n+ /**\n+ * How bright the highlight along each lit wall is, from 0 to 1, where 1\n+ * adds the full `highlightColor` at the brightest point. 0 turns it off.\n+ * Defaults to 0.45. Accepts a static value or an animation signal.\n+ */\n+ highlight?: AnimatableProp;\n+ /**\n+ * The highlight's color: hex, `oklch()`, or `oklab()`. Defaults to white,\n+ * `'oklch(1 0 0)'`.\n+ */\n+ highlightColor?: string;\n+ /**\n+ * How dark the glass gets at each groove, from 0 to 1, where 1 is black\n+ * right on the groove. The shadow falls where the glass turns steep, so a\n+ * `'flat'` cross-section darkens only along its walls. 0 turns it off.\n+ * Defaults to 0.2. Accepts a static value or an animation signal.\n+ */\n+ shadow?: AnimatableProp;\n+}\n+\n+/**\n+ * Shows the image beneath it through repeating flutes of glass, lit from one\n+ * side with a shadow at each groove. Mount it inside a `` after\n+ * the Sources it should bend. It bends only what Sources drew, never the\n+ * output of an Effect such as Grain, so mount such Effects after it, where\n+ * the order you write matches what you see. Mode 1 only, because it needs\n+ * the scene's output stage; Mode 2 users get the flute math as the\n+ * `fluteSurface` primitive.\n+ */\n+export function FlutedGlass({\n+ fluteWidth = 40,\n+ angle = 90,\n+ refraction = 1,\n+ crossSection = 'round',\n+ lightAngle = 135,\n+ highlight = 0.45,\n+ highlightColor = 'oklch(1 0 0)',\n+ shadow = 0.2,\n+}: FlutedGlassProps) {\n+ // The idle vote. Still glass draws nothing that changes over time, and\n+ // every dial repaints through its uniform when it moves, so the glass\n+ // votes idle and a scene of still Sources under the glass parks.\n+ useStaticSceneHint(true);\n+\n+ return (\n+ \n+ );\n+}" + }, + { + "path": "packages/shaders/src/components/fluted-glass/shader.tsx", + "status": "added", + "blobSha": "6124dce06fdde6e09f46094cdfcee47ee42ad338", + "additions": 326, + "deletions": 0, + "patch": "@@ -0,0 +1,326 @@\n+'use client';\n+\n+// The fluted glass's GPU half: a UV warp that bends the image beneath the\n+// glass, and a color transform that lights it, both reading one surface from\n+// the engine's fluteSurface primitive. This file converts between the pass's\n+// coordinate and the primitive's pixels. The wrapper (./fluted-glass.tsx)\n+// supplies the props.\n+import { useEffect, useMemo } from 'react';\n+\n+import type { ShaderNodeObject } from 'three/tsl';\n+import {\n+ abs,\n+ cos,\n+ dot,\n+ fract,\n+ max,\n+ normalize,\n+ pow,\n+ screenSize,\n+ sin,\n+ smoothstep,\n+ uniform,\n+ uv,\n+ vec2,\n+ vec3,\n+ vec4,\n+} from 'three/tsl';\n+import { type Node, Vector3 } from 'three/webgpu';\n+\n+import { fluteSurface } from '../../engine.js';\n+import type { AnimatableProp } from '../../react/hooks/animatable-signal/animatable-signal.js';\n+import { useAnimatableUniform } from '../../react/hooks/use-animatable-uniform/use-animatable-uniform.js';\n+import { useBasePassUv } from '../../react/hooks/use-base-pass-uv/use-base-pass-uv.js';\n+import { usePostProcessPass } from '../../react/hooks/use-overlay-pass/use-overlay-pass.js';\n+import { useResize } from '../../react/hooks/use-resize/use-resize.js';\n+import { useShaderContext } from '../../react/hooks/use-shader-context/use-shader-context.js';\n+import { parseColor } from '../shared/color.js';\n+\n+export interface FlutedGlassShaderProps {\n+ /**\n+ * The width of one flute, in CSS pixels. Accepts a static value or an\n+ * animation signal.\n+ */\n+ fluteWidth: AnimatableProp;\n+ /**\n+ * The direction the flutes run, in degrees on screen: 0 runs them left to\n+ * right and 90 runs them bottom to top. Accepts a static value or an\n+ * animation signal.\n+ */\n+ angle: AnimatableProp;\n+ /**\n+ * How far each flute bends the image beneath it, in half flute widths at\n+ * the steepest part of the glass. 0 leaves the image unchanged. Accepts a\n+ * static value or an animation signal.\n+ */\n+ refraction: AnimatableProp;\n+ /**\n+ * The shape of each flute seen from its end, from 0 to 1: 0 is a prism,\n+ * 0.5 is round, and 1 is a flat top with steep walls. The wrapper turns a\n+ * name into its point on this dial. Accepts a static value or an animation\n+ * signal.\n+ */\n+ crossSection: AnimatableProp;\n+ /**\n+ * Where the light comes from, in degrees counterclockwise on screen, the\n+ * way `angle` turns: 0 from the right, 90 from above, 180 from the left,\n+ * and 270 from below. Accepts a static value or an animation signal.\n+ */\n+ lightAngle: AnimatableProp;\n+ /**\n+ * How bright the highlight is, from 0 to 1, where 1 adds the full\n+ * `highlightColor` at the brightest point. Accepts a static value or an\n+ * animation signal.\n+ */\n+ highlight: AnimatableProp;\n+ /** The highlight's color: hex, `oklch()`, or `oklab()`. */\n+ highlightColor: string;\n+ /**\n+ * How dark the glass gets at each groove, from 0 to 1, where 1 is black\n+ * right on the groove. Accepts a static value or an animation signal.\n+ */\n+ shadow: AnimatableProp;\n+}\n+\n+// ---------------------------------------------\n+// Feel constants (tuned by eye at the build's visual gates)\n+// ---------------------------------------------\n+// How high the light sits above the glass, in degrees: 0 skims across it\n+// and 90 shines straight down. A lower light catches walls that lean\n+// further over, so it moves the highlight out toward the grooves.\n+const LIGHT_HEIGHT = 30;\n+const LIGHT_HEIGHT_RADIANS = (LIGHT_HEIGHT * Math.PI) / 180;\n+// The specular exponent. Higher draws a thinner, harder highlight, and\n+// lower spreads it into a soft sheen across the wall.\n+const HIGHLIGHT_SHARPNESS = 32;\n+// How far the shadow can reach in from each groove, as a share of the way to\n+// the flute's center. Within that reach it falls only where the glass is\n+// steep. 1 lets it reach the center.\n+const SHADOW_WIDTH = 0.5;\n+\n+// The pass's frame has y down and the primitive's has y up, so a vector\n+// that crosses between them flips its y.\n+function flipY(vector: ShaderNodeObject): ShaderNodeObject {\n+ return vector.mul(vec2(1, -1));\n+}\n+\n+// ---------------------------------------------\n+// Mirroring at the canvas edge\n+// ---------------------------------------------\n+// Near the canvas edge the bend can point off the canvas, where the scene\n+// texture has nothing. Left alone, the sampler, the GPU unit that reads a\n+// texture, clamps to the edge texel, the texture's outermost pixel, and\n+// repeats it, which draws a smeared strip. This folds the coordinate back\n+// instead, as if the image were mirrored past each edge.\n+//\n+// fract(x / 2) * 2 counts x along a 0-to-2 cycle, and subtracting 1 makes it\n+// run -1 to 1. Its absolute value then falls from 1 to 0 and climbs back,\n+// and 1 minus that rises from 0 to 1 and falls back: a triangle wave. On\n+// 0..1 it returns x unchanged, past 1 it walks back down, so 1.1 reads 0.9,\n+// and below 0 it walks back up, so -0.1 reads 0.1. It works per component.\n+function mirrorOntoCanvas(coordinate: ShaderNodeObject): ShaderNodeObject {\n+ return abs(fract(coordinate.div(2)).mul(2).sub(1)).oneMinus();\n+}\n+\n+export function FlutedGlassShader({\n+ fluteWidth,\n+ angle,\n+ refraction,\n+ crossSection,\n+ lightAngle,\n+ highlight,\n+ highlightColor,\n+ shadow,\n+}: FlutedGlassShaderProps) {\n+ const shaderContext = useShaderContext();\n+\n+ // The dials live in uniforms, values the CPU writes and the GPU reads at\n+ // each draw, so changing them never rebuilds the output chain.\n+ const fluteWidthUniform = useAnimatableUniform(fluteWidth);\n+ const angleUniform = useAnimatableUniform(angle);\n+ const refractionUniform = useAnimatableUniform(refraction);\n+ const crossSectionUniform = useAnimatableUniform(crossSection);\n+ const lightAngleUniform = useAnimatableUniform(lightAngle);\n+ const highlightUniform = useAnimatableUniform(highlight);\n+ const shadowUniform = useAnimatableUniform(shadow);\n+\n+ // The highlight color decodes once into linear rgb (parseColor reads hex\n+ // and the wide-gamut oklch() and oklab() strings). The Vector3 and its\n+ // uniform are created once, and the effect below writes each new color\n+ // into the Vector3, so a new color never rebuilds the color transform.\n+ const highlightColorVector = useMemo(() => new Vector3(1, 1, 1), []);\n+ const highlightColorUniform = useMemo(\n+ () => uniform(highlightColorVector),\n+ [highlightColorVector],\n+ );\n+\n+ useEffect(() => {\n+ highlightColorVector.set(...parseColor(highlightColor));\n+ // The bare-uniform-write gotcha in docs/agents/tsl.md: a write into the\n+ // Vector3 repaints nothing on a static scene by itself.\n+ shaderContext?.scheduler.requestRender();\n+ }, [highlightColor, highlightColorVector, shaderContext]);\n+\n+ // ---------------------------------------------\n+ // CSS pixels -> device pixels\n+ // ---------------------------------------------\n+ // fluteWidth is in CSS pixels so a flute keeps its size on a 1x and a 2x\n+ // display, but the warp below counts device pixels. The renderer's pixel\n+ // ratio, which respects maxDPR clamping, converts between them; useResize\n+ // fires again on monitor moves and browser zoom, when that ratio changes.\n+ // Dither builds its ratio the same way.\n+ const resize = useResize();\n+ // Created once and never replaced: the effect below writes into it, and\n+ // the warp depends on the stable wrapper.\n+ const pixelRatioUniform = useMemo(\n+ () => uniform(resize.get()[2] || 1),\n+ // eslint-disable-next-line react-hooks/exhaustive-deps\n+ [],\n+ );\n+\n+ useEffect(() => {\n+ const apply = () => {\n+ const rendererRatio = shaderContext?.renderer.three.getPixelRatio();\n+\n+ pixelRatioUniform.value =\n+ rendererRatio !== undefined && rendererRatio > 0 ? rendererRatio : resize.get()[2] || 1;\n+ // A bare uniform write repaints nothing on a static scene, so ask for\n+ // a frame (the bare-uniform-write gotcha in docs/agents/tsl.md).\n+ shaderContext?.scheduler.requestRender();\n+ };\n+\n+ apply();\n+\n+ return resize.on('change', apply);\n+ }, [resize, pixelRatioUniform, shaderContext]);\n+\n+ // ---------------------------------------------\n+ // The glass at a pixel\n+ // ---------------------------------------------\n+ // Both passes below start here, so the light lands where the image bends.\n+ // `coordinate` is a pass coordinate, [0, 0] at the top-left with y growing\n+ // downward. Into the primitive's frame: subtract 0.5 to measure from the\n+ // canvas center, so the middle flute sits in the middle of the canvas and\n+ // the flutes turn around it as `angle` changes. Times screenSize, the\n+ // drawing buffer in device pixels, turns 0..1 into pixels, so a flute is as\n+ // wide across a wide canvas as across a tall one. The y flip points y up,\n+ // the frame the primitive's angle is measured in.\n+ const surfaceAt = (coordinate: ShaderNodeObject) =>\n+ fluteSurface(flipY(coordinate.sub(0.5).mul(screenSize)), {\n+ fluteWidth: fluteWidthUniform.mul(pixelRatioUniform),\n+ angle: angleUniform,\n+ refraction: refractionUniform,\n+ crossSection: crossSectionUniform,\n+ });\n+\n+ // ---------------------------------------------\n+ // Refraction: bend the scene through the flutes\n+ // ---------------------------------------------\n+ // A base-pass UV warp: the callback receives the 0..1 coordinate the\n+ // rendered scene is about to be sampled at and returns a replacement.\n+ useBasePassUv(\n+ (coordinate) => {\n+ const { bend } = surfaceAt(coordinate);\n+\n+ // Back out: the bend comes back in device pixels with y up. Flip y\n+ // again and divide by the buffer size to get a 0..1 offset in the\n+ // pass's frame, then fold anything off the canvas back onto it.\n+ const bent = coordinate.add(flipY(bend).div(screenSize));\n+\n+ return mirrorOntoCanvas(bent);\n+ },\n+ [fluteWidthUniform, angleUniform, refractionUniform, crossSectionUniform, pixelRatioUniform],\n+ );\n+\n+ // ---------------------------------------------\n+ // Lighting: a shadow at the grooves, a highlight on the walls\n+ // ---------------------------------------------\n+ // A post-process pass: the callback receives each pixel the scene drew,\n+ // already bent by the warp above (`input`, rgba), and returns a\n+ // replacement. The warp and this pass draw in the same output quad, and\n+ // uv() is the pixel's own pass coordinate, so surfaceAt(uv()) is the glass\n+ // at this pixel. The warp starts from uv() too while it runs first. A UV\n+ // warp mounted before FlutedGlass, such as Dither's, hands the glass its\n+ // moved coordinate instead, so there the bend and the light read the\n+ // surface at slightly different points. SHA-267 reworks how warps stack.\n+ usePostProcessPass(\n+ (input) => {\n+ const { slope, fromCenter, steepness } = surfaceAt(uv());\n+\n+ // The shadow. smoothstep() climbs from 0 to 1 over the outer\n+ // SHADOW_WIDTH of the way from the center to the groove, so the shade\n+ // is 0 across the middle of the flute and full right on the groove.\n+ // Times the steepness, it falls only where the glass is steep: a flat\n+ // top darkens on its walls and not across its flat middle, and a\n+ // prism, steep from ridge to groove, keeps the whole band. Both read\n+ // the glass without the groove smoothing, which pulls the slope to 0\n+ // on the groove's own pixel and would leave a bright line down the\n+ // middle of the dark one. Multiplying darkens without moving alpha,\n+ // and a transparent pixel, which holds no color here, stays\n+ // transparent.\n+ const shade = smoothstep(1 - SHADOW_WIDTH, 1, fromCenter)\n+ .mul(steepness)\n+ .mul(shadowUniform);\n+ const shaded = input.rgb.mul(shade.oneMinus());\n+\n+ // The light works in the primitive's frame, y up, where the slope\n+ // already is and where `angle` turns. Only directions meet in the math\n+ // below, so the frame changes nothing but which way the angles run.\n+ //\n+ // The surface normal: the direction the glass faces, one unit long.\n+ // Flat glass faces straight out of the screen, (0, 0, 1). Tilt it by\n+ // the slope, and a wall's normal leans the way the wall faces. At a\n+ // slope of 1, at a groove, it leans 45 degrees. normalize() scales the\n+ // vector back to one unit long.\n+ const normal = normalize(vec3(slope, 1));\n+\n+ // The direction toward the light, one unit long. lightAngle turns it\n+ // counterclockwise around the screen, the way `angle` turns the\n+ // flutes, and LIGHT_HEIGHT lifts it off the glass: cos(height) of it\n+ // lies along the screen and sin(height) points out of it. The height\n+ // is fixed, so its cosine and sine are worked out once here rather\n+ // than per pixel.\n+ const lightRadians = lightAngleUniform.mul(Math.PI / 180);\n+ const toLight = vec3(\n+ cos(lightRadians).mul(Math.cos(LIGHT_HEIGHT_RADIANS)),\n+ sin(lightRadians).mul(Math.cos(LIGHT_HEIGHT_RADIANS)),\n+ Math.sin(LIGHT_HEIGHT_RADIANS),\n+ );\n+\n+ // Specular light, the Blinn-Phong way. A mirror sends light from the\n+ // light to the eye where its normal points halfway between the two.\n+ // The eye looks straight at the screen, from (0, 0, 1), so the\n+ // halfway vector is the normalized sum of that and toLight. dot()\n+ // of two unit vectors is the cosine of the angle between them: 1\n+ // where the normal points exactly halfway, and lower as it turns\n+ // away. pow() raises that to HIGHLIGHT_SHARPNESS, which keeps values\n+ // near 1 and crushes the rest, so the highlight narrows to a bright\n+ // band on the part of each wall that leans toward the light by the\n+ // right amount. Flat glass and walls that face away stay dark. max()\n+ // keeps pow()'s base from going below 0.\n+ const halfway = normalize(toLight.add(vec3(0, 0, 1)));\n+ const specular = pow(max(dot(normal, halfway), 0), HIGHLIGHT_SHARPNESS);\n+\n+ // Added light, scaled by the dial and by the pixel's alpha, as\n+ // CursorRipple's shine is: a transparent pixel has nothing for light\n+ // to land on. The color multiplies last, as an argument (the\n+ // vec-uniform gotcha in docs/agents/tsl.md). Alpha passes through.\n+ const light = specular.mul(highlightUniform).mul(input.a).mul(highlightColorUniform);\n+\n+ return vec4(shaded.add(light), input.a);\n+ },\n+ [\n+ fluteWidthUniform,\n+ angleUniform,\n+ refractionUniform,\n+ crossSectionUniform,\n+ pixelRatioUniform,\n+ lightAngleUniform,\n+ highlightUniform,\n+ highlightColorUniform,\n+ shadowUniform,\n+ ],\n+ );\n+\n+ return null;\n+}" + }, + { + "path": "packages/shaders/src/components/index.ts", + "status": "modified", + "blobSha": "918e6039b4b0d8ac9d66cd29fe58512dfc0e63bd", + "additions": 3, + "deletions": 0, + "patch": "@@ -34,6 +34,9 @@ export type { DitherProps } from './dither/dither.js';\n export { DotField } from './dot-field/dot-field.js';\n export type { DotFieldProps, DotShape, SvgMarkup } from './dot-field/dot-field.js';\n \n+export { FlutedGlass } from './fluted-glass/fluted-glass.js';\n+export type { CrossSectionName, FlutedGlassProps } from './fluted-glass/fluted-glass.js';\n+\n export { FractalNoise } from './fractal-noise/fractal-noise.js';\n export type { FractalNoiseProps } from './fractal-noise/fractal-noise.js';\n export type { FractalNoiseStyle } from './fractal-noise/shader.js';" + }, + { + "path": "packages/shaders/src/engine.ts", + "status": "modified", + "blobSha": "a8c0237331621668bfd0be4b06d5f10a487b846c", + "additions": 6, + "deletions": 0, + "patch": "@@ -67,6 +67,12 @@ export { signedDistanceFieldCross } from './primitives/sdf-cross/sdf-cross.js';\n \n export { displace } from './primitives/displace/displace.js';\n \n+export { fluteSurface } from './primitives/flute-surface/flute-surface.js';\n+export type {\n+ FluteSurfaceOptions,\n+ FluteSurfaceResult,\n+} from './primitives/flute-surface/flute-surface.js';\n+\n export { cursorRipple } from './primitives/cursor-ripple/cursor-ripple.js';\n export type { CursorRippleOptions } from './primitives/cursor-ripple/cursor-ripple.js';\n " + }, + { + "path": "packages/shaders/src/primitives/flute-surface/flute-surface.test.ts", + "status": "added", + "blobSha": "86b1478b552e19d2c731032dbbbcdbeec28b7f20", + "additions": 53, + "deletions": 0, + "patch": "@@ -0,0 +1,53 @@\n+import { uniform, uv } from 'three/tsl';\n+import { describe, expect, it } from 'vitest';\n+\n+import { fluteSurface } from './flute-surface.js';\n+\n+describe('fluteSurface', () => {\n+ it('returns bend, slope, fromCenter, and steepness nodes with default options', () => {\n+ const surface = fluteSurface(uv().mul(400));\n+\n+ expect(surface.bend).toBeDefined();\n+ expect(surface.slope).toBeDefined();\n+ expect(surface.fromCenter).toBeDefined();\n+ expect(surface.steepness).toBeDefined();\n+ });\n+\n+ it('accepts numeric options', () => {\n+ const surface = fluteSurface(uv().mul(400), { fluteWidth: 24, angle: 30, refraction: 0.5 });\n+\n+ expect(surface.bend).toBeDefined();\n+ });\n+\n+ it('accepts node options (uniform-driven dials)', () => {\n+ const surface = fluteSurface(uv().mul(400), {\n+ fluteWidth: uniform(40),\n+ angle: uniform(90),\n+ refraction: uniform(1),\n+ });\n+\n+ expect(surface.bend).toBeDefined();\n+ expect(surface.slope).toBeDefined();\n+ });\n+\n+ it('accepts a cross-section at either end of the dial and between', () => {\n+ for (const crossSection of [0, 0.25, 0.5, 1]) {\n+ expect(fluteSurface(uv().mul(400), { crossSection }).slope).toBeDefined();\n+ }\n+ });\n+\n+ it('accepts a node cross-section', () => {\n+ const surface = fluteSurface(uv().mul(400), { crossSection: uniform(0.5) });\n+\n+ expect(surface.bend).toBeDefined();\n+ });\n+\n+ it('accepts a cross-section outside 0 to 1 without throwing', () => {\n+ expect(() => fluteSurface(uv(), { crossSection: -1 })).not.toThrow();\n+ expect(() => fluteSurface(uv(), { crossSection: 2 })).not.toThrow();\n+ });\n+\n+ it('accepts a flute width of 0 without throwing', () => {\n+ expect(() => fluteSurface(uv(), { fluteWidth: 0 })).not.toThrow();\n+ });\n+});" + }, + { + "path": "packages/shaders/src/primitives/flute-surface/flute-surface.ts", + "status": "added", + "blobSha": "a19bd76c7da6ccba33e8d210180e910d1ecf26b7", + "additions": 272, + "deletions": 0, + "patch": "@@ -0,0 +1,272 @@\n+// The surface of fluted glass: identical flutes side by side, each a strip\n+// of glass that bends what is under it. For a point, this works out how\n+// steep the glass is and how far it moves the image, in one call, so light\n+// read from the slope lands where the image bends. FlutedGlass bends and\n+// lights the scene with it, and Mode 2 users call it on their own coordinates.\n+import type { ShaderNodeObject } from 'three/tsl';\n+import {\n+ abs,\n+ clamp,\n+ cos,\n+ dot,\n+ float,\n+ fract,\n+ fwidth,\n+ max,\n+ mix,\n+ pow,\n+ select,\n+ sin,\n+ smoothstep,\n+ vec2,\n+} from 'three/tsl';\n+import type { Node } from 'three/webgpu';\n+\n+import type { TSLNode } from '../color-ramp/color-ramp.js';\n+\n+export interface FluteSurfaceOptions {\n+ /**\n+ * The width of one flute, in the units of `position`. Values near 0 clamp\n+ * to a tiny width rather than dividing by zero. Default 40.\n+ */\n+ fluteWidth?: TSLNode | number;\n+ /**\n+ * The direction the flutes run, in degrees counterclockwise from the\n+ * position's +x axis with y up: 0 runs them left to right and 90 runs them\n+ * bottom to top, the degrees LinearGradient's angle uses. Default 90,\n+ * vertical flutes.\n+ */\n+ angle?: TSLNode | number;\n+ /**\n+ * How far the glass moves the image, in half flute widths per unit of\n+ * slope. 0 leaves it in place. At 1, a groove shows the image half a flute\n+ * width away, so each flute shows a copy of twice its own width, squeezed.\n+ * Default 1.\n+ */\n+ refraction?: TSLNode | number;\n+ /**\n+ * The shape of each flute seen from its end, from 0 to 1. 0 is a pointed\n+ * prism, two flat faces meeting at a ridge. 0.5 is a round lens. 1 is a\n+ * flat top with steep walls. Values between give the shapes between, and\n+ * values outside 0 to 1 clamp. Default 0.5.\n+ */\n+ crossSection?: TSLNode | number;\n+}\n+\n+export interface FluteSurfaceResult {\n+ /**\n+ * How far to move the point before you sample the image under the glass,\n+ * as a vector in the units and frame of `position`. Add it to `position`.\n+ */\n+ bend: ShaderNodeObject;\n+ /**\n+ * How steep the glass is, as a vector in the frame of `position`. It\n+ * points across the flute, away from the flute's center, and its length\n+ * runs from 0 at the center to 1 at a groove.\n+ */\n+ slope: ShaderNodeObject;\n+ /**\n+ * How far the point sits from its flute's center, from 0 at the center to\n+ * 1 at a groove, whatever the cross-section. Unlike the slope it skips the\n+ * groove smoothing, so it reaches 1 at a groove from both sides with no\n+ * dip. Shading meant to be darkest on the groove reads this.\n+ */\n+ fromCenter: ShaderNodeObject;\n+ /**\n+ * How steep the glass is, from 0 where it is flat to 1 at a groove. It is\n+ * the slope's length without the groove smoothing, so like `fromCenter` it\n+ * reaches 1 at a groove from both sides, where the slope dips to 0.\n+ * Shading meant to follow the shape of the glass reads this.\n+ */\n+ steepness: ShaderNodeObject;\n+}\n+\n+// ---------------------------------------------\n+// Feel constants (tuned by eye at the build's visual gates)\n+// ---------------------------------------------\n+// The slope's exponent at each end of the cross-section dial. A cross-section\n+// of 0.5 always has an exponent of 1. Lower PRISM_EXPONENT sharpens the\n+// prism's faces toward perfectly flat, and higher FLAT_EXPONENT flattens the\n+// top and steepens the walls.\n+const PRISM_EXPONENT = 1 / 16;\n+const FLAT_EXPONENT = 16;\n+// How many screen pixels each groove and the prism's ridge smooth over on\n+// either side. 0 brings back the stair steps; above about 2 the grooves read\n+// as soft blurs rather than sharp breaks.\n+const GROOVE_SMOOTHING = 1;\n+\n+/**\n+ * The bend and slope of fluted glass at `position`. A flute's center sits on\n+ * the position's origin, so center the position where the middle flute\n+ * should go. Call it from a fragment shader: it smooths each groove over the\n+ * screen-space rate of change of `position`, which only a fragment shader\n+ * can measure.\n+ *\n+ * @param position โ€” Vec2 TSL node in any units with y up, such as `uv()`\n+ * scaled to pixels. `fluteWidth` and the returned bend share its units.\n+ */\n+export function fluteSurface(\n+ position: TSLNode,\n+ options: FluteSurfaceOptions = {},\n+): FluteSurfaceResult {\n+ const fluteWidth = max(float(options.fluteWidth ?? 40), 1e-4);\n+ const angle = float(options.angle ?? 90);\n+ const refraction = float(options.refraction ?? 1);\n+ const crossSection = clamp(float(options.crossSection ?? 0.5), 0, 1);\n+\n+ // ---------------------------------------------\n+ // The direction across the flutes\n+ // ---------------------------------------------\n+ // The flutes run along (cos, sin) of the angle. Across is that direction\n+ // turned a quarter turn clockwise, (sin, -cos), so at the default 90\n+ // degrees it is (1, 0): across runs left to right over vertical flutes.\n+ const angleRadians = angle.mul(Math.PI / 180);\n+ const across = vec2(sin(angleRadians), cos(angleRadians).negate());\n+\n+ // ---------------------------------------------\n+ // Repeating space: which part of which flute\n+ // ---------------------------------------------\n+ // dot() projects the position onto the across direction: its signed\n+ // distance across the flutes, in the position's units. Dividing by the\n+ // width counts that distance in flutes. The +0.5 below centers a flute on\n+ // every whole number, so 2.25 lies a quarter flute right of the center of\n+ // flute 2, halfway to its groove.\n+ //\n+ // fract() keeps only the part after the decimal point, which is how a\n+ // shader repeats space: every flute gets the same 0-to-1 ramp, whatever\n+ // its number. The +0.5 shifts the ramp so its middle, not its start, lands\n+ // on the origin, which puts a flute's center there. At each groove the\n+ // ramp jumps from 1 back to 0, and that jump is what breaks the image into\n+ // strips.\n+ const flutePhase = dot(position, across).div(fluteWidth);\n+ const ramp = fract(flutePhase.add(0.5));\n+\n+ // The distance from the center, before any smoothing. ramp*2-1 runs from\n+ // -1 at one groove to 1 at the other, and abs() folds it to run 0 to 1\n+ // from the center out on both sides. Where the ramp jumps from 1 to 0 at a\n+ // groove, this goes from 1 to 1, so it has no jump to smooth.\n+ const fromCenter = abs(ramp.mul(2).sub(1));\n+\n+ // ---------------------------------------------\n+ // Clean grooves: antialiasing from the screen-space rate of change\n+ // ---------------------------------------------\n+ // The scene has no MSAA, the GPU's own edge smoothing, so a jump in the\n+ // ramp lands on whole pixels and a slanted groove draws a staircase. The\n+ // fix is to replace the jump with a slope exactly one pixel wide, and to\n+ // know how wide a pixel is in flutes, the shader asks the GPU.\n+ //\n+ // The GPU shades pixels in 2x2 blocks, so each pixel can read how much a\n+ // value changes to its neighbor. fwidth() adds that change across and\n+ // down, so fwidth(flutePhase) is how many flutes one pixel spans: 0.025 for\n+ // a 40-pixel flute, and 0.125 for an 8-pixel one. It is taken of the phase\n+ // before fract(), which has no jump in it to throw the measure off. The max\n+ // keeps the band from shrinking to nothing, which would divide by zero in\n+ // smoothstep() when a flute is very wide.\n+ const rampPerPixel = max(fwidth(flutePhase).mul(GROOVE_SMOOTHING), 1e-5);\n+\n+ // Paper's smoothFract. distanceToGroove is 0 at a groove and falls to -0.5\n+ // at the flute's center. smoothstep() turns the last pixel before a groove\n+ // into a blend that climbs from 0 to 0.5, so mix() eases the ramp toward\n+ // its mirror image, 1 - ramp, and the two meet at 0.5. Both sides of a\n+ // groove now arrive at 0.5, the flute's center, where the glass is flat, so\n+ // the groove pixel shows the image right at the groove instead of either\n+ // flute's bent copy. Away from the grooves the blend is 0 and the ramp is\n+ // unchanged.\n+ const distanceToGroove = abs(ramp.sub(0.5)).sub(0.5);\n+ const grooveBlend = smoothstep(rampPerPixel.negate(), rampPerPixel, distanceToGroove);\n+ const smoothRamp = mix(ramp, ramp.oneMinus(), grooveBlend);\n+\n+ // *2-1 stretches the ramp to run from -1 at one groove, through 0 at the\n+ // center, to 1 at the other groove.\n+ const fluteOffset = smoothRamp.mul(2).sub(1);\n+\n+ // ---------------------------------------------\n+ // The cross-section: picking an exponent from the dial\n+ // ---------------------------------------------\n+ // The dial picks the exponent that shapes the slope below, on a doubling\n+ // scale. dialFromRound runs from -1 at a prism, through 0 at round, to 1 at\n+ // flat. The prism half raises PRISM_EXPONENT to its distance from round,\n+ // and the flat half raises FLAT_EXPONENT the same way. At round both give\n+ // 1, since anything to the power 0 is 1, so 0.5 is exactly round whatever\n+ // the ends are. Each equal step of the dial multiplies the exponent by the\n+ // same factor, so a sweep morphs the shape evenly. With ends of 1/16 and\n+ // 16, the exponent doubles every eighth of the dial.\n+ const dialFromRound = crossSection.mul(2).sub(1);\n+ const exponentEnd = select(\n+ dialFromRound.lessThan(0),\n+ float(PRISM_EXPONENT),\n+ float(FLAT_EXPONENT),\n+ );\n+ const exponent = pow(exponentEnd, abs(dialFromRound));\n+\n+ // ---------------------------------------------\n+ // The slope: shaping a curve with an exponent\n+ // ---------------------------------------------\n+ // The slope's size is the distance from the flute's center, |offset|,\n+ // raised to the exponent. Because that distance runs from 0 to 1, a power\n+ // bends the curve between without moving its ends: the center stays flat\n+ // and the groove stays steepest.\n+ //\n+ // - An exponent of 1 leaves a straight line, so the slope grows in step\n+ // with the distance: a round lens. Strictly that draws a parabola, a\n+ // shallow stand-in for a circular arc, whose slope would shoot up near\n+ // the grooves.\n+ // - Below 1 the power lifts small values toward 1. x^(1/16) is already\n+ // 0.65 at x = 0.001 and 0.96 at 0.5, so the slope jumps to nearly full\n+ // just off the center and stays there: two flat faces meeting at a\n+ // ridge, a prism.\n+ // - Above 1 the power presses small values toward 0. x^16 is 0.00002 at\n+ // 0.5 and reaches 0.5 only at 0.96, so the glass stays flat across the\n+ // top and turns steep right at each groove.\n+ //\n+ // pow() needs a base of 0 or more, so the power is taken of fromCenter,\n+ // the distance without the groove smoothing, and the offset carries the\n+ // sign: offset * distance^(exponent - 1). Away from the grooves the offset\n+ // is ยฑdistance, so that works out to ยฑdistance^exponent, the curve above.\n+ // On a groove's own pixel the distance is about 1, and 1 to any power is\n+ // 1, so the slope follows the smoothed offset through 0 over the same\n+ // pixel whatever the exponent. Raising the smoothed offset itself would\n+ // undo the smoothing on a prism: x^(1/16) is still 0.82 at x = 0.045, so\n+ // the slope would stay near full until a sliver from the groove, and the\n+ // grooves would staircase again. The max() keeps the base off 0 at the\n+ // center, where a prism's negative power, exponent - 1, would shoot to\n+ // infinity. Pointing the result along `across` turns it into a vector, so\n+ // on either side of the center the slope points toward the nearer groove.\n+ const shapedSlope = fluteOffset.mul(pow(max(fromCenter, 1e-5), exponent.sub(1)));\n+\n+ // A prism's ridge jumps too. Just off the center x^(1/16) leaps from 0 to\n+ // about 0.65 inside a sliver much thinner than a pixel, so the two faces\n+ // meet in a staircase of their own. The same pixel width, doubled because\n+ // the offset runs twice as fast as the ramp, fades the slope in over the\n+ // first pixel either side of the center. On a round or flat flute the\n+ // slope there is already close to 0, so the fade changes nothing you can\n+ // see. It also softens the groove pixel, where the smoothed offset passes\n+ // through 0 on its way between flutes.\n+ const ridgeBlend = smoothstep(0, rampPerPixel.mul(2), abs(fluteOffset));\n+ const slopeAmount = shapedSlope.mul(ridgeBlend);\n+ const slope = across.mul(slopeAmount);\n+\n+ // The steepness takes the same power of fromCenter rather than of the\n+ // smoothed offset. Away from the grooves the two are equal, so it is the\n+ // slope's length there. On a groove's own pixel, where the smoothing pulls\n+ // the slope through 0, it keeps climbing toward 1. The ridge fade reads\n+ // fromCenter too, so it fades only at the center and never at a groove.\n+ const steepness = pow(fromCenter, exponent).mul(smoothstep(0, rampPerPixel.mul(2), fromCenter));\n+\n+ // ---------------------------------------------\n+ // The bend: refraction as a sample offset\n+ // ---------------------------------------------\n+ // Glass doesn't move pixels. It changes which point of the image behind it\n+ // reaches the eye, so a shader bends an image by sampling it somewhere\n+ // else. The bend is the slope times the dial, in half flute widths. On a\n+ // round flute at a refraction of 1, a point at offset x from the center\n+ // samples at 2x, so the flute's own width shows twice that width of image:\n+ // a squeezed copy of what lies right under it. The bend points away from\n+ // the center on both sides, which is what squeezes rather than stretches.\n+ // Other cross-sections spread the same span unevenly. A prism shifts each\n+ // face's half of the copy outward whole, and a flat top barely moves its\n+ // middle and crams the rest against the walls.\n+ const bend = slope.mul(refraction).mul(fluteWidth.mul(0.5));\n+\n+ return { bend, slope, fromCenter, steepness };\n+}" + }, + { + "path": "scripts/build-posters.sh", + "status": "modified", + "blobSha": "5a385543d385046191d5b18fce5935b399923008", + "additions": 10, + "deletions": 2, + "patch": "@@ -38,8 +38,15 @@ fi\n # transparent base layer, e.g. aurora, so they flatten onto a sensible color;\n # WxH overrides the default capture size and is only needed for pixel-sized\n # shaders like dot-field, whose posters render pixel-locked in the demo โ€” see\n-# DemoPoster's pixelSize prop โ€” and so must be captured larger than any demo\n-# box they'll be cropped into)\n+# DemoPoster's pixelSize prop โ€” and so must be captured at least as large as\n+# any demo box they'll be cropped into)\n+#\n+# fluted-glass is captured at the widest demo box: the 4xl column's 896px at\n+# 3:2, rounded up to 598. Its flutes are sized in CSS pixels, so its poster\n+# renders pixel-locked too. The MeshGradient under the glass stretches to the\n+# canvas, so a larger capture would zoom the gradient in every box. At this\n+# size the poster matches the live scene in the widest box. A narrower box\n+# keeps the flutes' spacing but shows only the middle of the gradient.\n #\n # crop is the side, in poster pixels, of the center square the card thumbnail\n # is cut from. Without it the thumbnail keeps the poster's full height. Only a\n@@ -74,6 +81,7 @@ for pair in \\\n \"cursor-ripple:jpg\" \\\n \"cursor-smudge:jpg\" \\\n \"cursor-spotlight:jpg\" \\\n+ \"fluted-glass:jpg::896x598\" \\\n \"radial-wipe:jpg:#0b0f0d\" \\\n \"dissolve:jpg:#0b0f0d\"; do\n IFS=':' read -r name format background size crop <<< \"$pair\"" + } +] diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/GLOSSARY.md b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/GLOSSARY.md new file mode 100644 index 0000000..267b932 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/GLOSSARY.md @@ -0,0 +1,109 @@ +# Shaders + +A React shader component library on WebGPU and Three.js TSL. This glossary holds the terms the project uses for its own concepts, so code, docs, and issues say the same thing. + +## Language + +### Scene model + +**Source**: +A component that draws its own image into a scene, such as MeshGradient or DotField. It is the thing an Effect acts on. +_Avoid_: Base component, generator, layer + +**Effect**: +A component that acts on the image beneath it in a scene rather than drawing its own, such as Grain, Dither, or LedWall. An Effect composes over any Source or Effect mounted before it. +_Avoid_: Overlay, post-process pass, post-process layer, filter + +**UV warp**: +An Effect's change to where the image beneath it is sampled, such as Dither's pixel snap or CursorRipple's refraction. A UV warp bends only what Sources drew. +_Avoid_: Warp on its own, base UV transform + +**Input**: +A value that changes over time and is not drawn, such as the cursor, scroll, or canvas size. An Input is fed to a component as an animation signal. +_Avoid_: Cursor hook, entry point, scene-level field + +**Position prop**: +A component prop that names a point on the canvas. A component's own center is `center`; a point that belongs to one reaction among the component's other dials is named after the reaction, such as `swellCenter`. It is a position, not a cursor: the cursor is one Input you can feed it. The `'cursor'` shorthand expresses that mapping without a hook, as in `swellCenter="cursor"`. +_Avoid_: Cursor prop, interactive prop, entry point, focus + +**Reaction**: +A component's response that grows toward a point and needs the component's own internals, such as LedWall's swell. A reaction exposes three props: the amount named for the reaction, where 0 turns it off, a `Radius` in canvas units, and a `Center` position prop. +_Avoid_: Interaction, cursor ability, hover effect + +**Cursor effect**: +An Effect driven by the cursor Input that works over any scene with no help from the components beneath it. CursorSpotlight and CursorRipple are two. +_Avoid_: Interactive shader, cursor component, drop-in + +**Scroll push**: +The push a scene's cursor effects feel from a touch that scrolls the page over the canvas, at the finger and on through the fling. A scene opts in, and the page scrolls exactly as it would without it. +_Avoid_: Hold to interact, touch drag, scroll drag + +**Scripted stroke**: +A stroke code draws across a scene's cursor effects by moving a point. While code strokes, the effects ignore the mouse and scroll push. +_Avoid_: Virtual pointer, fake cursor, brush + +### Rendering + +**Render on demand**: +A scene draws every frame until its components' idle votes let it park. A parked scene does no GPU work. An Input or a prop change asks for frames only until its value settles, and the scene stays parked. An animated vote restarts the frame loop. +_Avoid_: Idle mode, frameloop demand, static scene + +**Idle vote**: +A component's say in whether its scene may park. A component votes idle while nothing it draws can change, votes animated while anything can, or casts no vote; a scene parks only when at least one component votes idle and none votes animated. +_Avoid_: Static hint, render-on-demand vote, static vote + +**Frame cap**: +The most frames per second a scene draws, set by its app. With no frame cap, a scene draws at the display's refresh rate, so a 120 Hz display gets 120 frames a second. Frames a prop change asks for count against the frame cap. A frame cap never wakes a parked scene or resumes a paused one. +_Avoid_: Frame budget, target frame rate, fps limit + +**Paused scene**: +A scene frozen on its current frame, by its app through `paused`, or because the tab is hidden or the canvas is off screen. Time stops while a scene is paused, so it resumes on the frame it stopped on. A parked scene stops drawing because nothing it draws can change. A paused scene stops whatever its components vote. +_Avoid_: Frozen scene, stopped scene + +### Pattern components + +**Mark**: +The single figure drawn at each grid point of a pattern component such as DotField. Every mark is a coverage at a cell-local point, computed from a signed distance for a built-in mark or read from a decoded tile for a custom mark, so sizing and ripple displacement work the same for every mark. +_Avoid_: Glyph, symbol, icon, dot (except inside shipped identifiers such as `DotField` and `dotSize`) + +**Built-in mark**: +A mark the library ships by name, such as the circle and the cross. Its edge is an exact distance, so it stays crisp at any size. +_Avoid_: Preset shape, default shape + +**Custom mark**: +A mark the caller supplies as inline SVG markup. Only its coverage is read, so it takes the field's color like any other mark. +_Avoid_: Custom shape, user shape, icon + +### Docs site + +**Search trigger**: +The button in the site header that opens the search panel and focuses its input. It is not an input itself. +_Avoid_: Search bar, search box, search input (for the header control) + +**Search panel**: +The dialog the search trigger opens, holding the query input and the result list over the blurred page. +_Avoid_: Search modal, search dialog, command palette + +**Demo**: +The live scene and its control panel at the top of a component page. The homepage hero turns into Aurora's demo as the page scrolls. +_Avoid_: Editor, playground + +**Favorite**: +A component hand-picked for the homepage's favorites grid. It links to its component page. +_Avoid_: Featured component, highlight + +**Card tab**: +The strip that cuts into a favorite's edge to show the favorite's short name, such as "Simplex" for Simplex Noise, when the card is hovered or focused. +_Avoid_: Tab, name tab, label tab + +**Feature card**: +A card in the homepage's features section that illustrates one quality of the library, such as "Composable" or "Reactive". Unlike a favorite, a feature card shows no component. +_Avoid_: Benefit card, value prop, pillar, featured card + +**Story**: +The animation a feature card plays when the pointer moves over it. Once started, a story always finishes, and the card returns to its still frame. +_Avoid_: Intro, entrance, demo, hover effect + +**Still frame**: +The one frame a feature card shows whenever its story is not playing. It need not be the story's first or last frame. +_Avoid_: End frame, rest state, poster diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/apps/docs/src/app/components/demo-registry.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/apps/docs/src/app/components/demo-registry.tsx new file mode 100644 index 0000000..24df87b --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/apps/docs/src/app/components/demo-registry.tsx @@ -0,0 +1,368 @@ +/** + * Maps a component slug to the pieces the shared components/[slug] template + * cannot derive from the catalog (content/components.ts): the client demo island and + * the Usage content. The template renders usageNotes above the snippet, so + * notes read as guidance introducing the code. Paragraphs that merely + * restated the catalog description were dropped when pages converted โ€” the + * template's header already shows it. + */ +import type { ComponentType, ReactNode } from 'react'; + +import { AuroraIsland } from './aurora/demo'; +import { BlobsIsland } from './blobs/demo'; +import { ConicGradientIsland } from './conic-gradient/demo'; +import { CursorRippleIsland } from './cursor-ripple/demo'; +import { CursorSmudgeIsland } from './cursor-smudge/demo'; +import { CursorSpotlightIsland } from './cursor-spotlight/demo'; +import { DissolveIsland } from './dissolve/demo'; +import { DitherIsland } from './dither/demo'; +import { DotFieldIsland } from './dot-field/demo'; +import { FractalNoiseIsland } from './fractal-noise/demo'; +import { GodRaysIsland } from './god-rays/demo'; +import { GrainIsland } from './grain/demo'; +import { LedWallIsland } from './led-wall/demo'; +import { LinearGradientIsland } from './linear-gradient/demo'; +import { LiquidGradientIsland } from './liquid-gradient/demo'; +import { MeshGradientIsland } from './mesh-gradient/demo'; +import { RadialGradientIsland } from './radial-gradient/demo'; +import { RadialWipeIsland } from './radial-wipe/demo'; +import { SimplexNoiseIsland } from './simplex-noise/demo'; +import { VignetteIsland } from './vignette/demo'; +import { VoronoiIsland } from './voronoi/demo'; +import { WaveLinesIsland } from './wave-lines/demo'; + +export interface ComponentPageEntry { + /** Client island rendering the control store, shader demo, and control panel. */ + Island: ComponentType; + /** + * Code snippet for the Usage section, rendered through the shiki CodeBlock. + * The import line shown above it is derived from this snippet's JSX tags + * (lib/usage-import), so don't write import statements here โ€” every + * component tag used in the snippet lands in the import automatically. + */ + usageSnippet: string; + /** + * Layers the demo scene renders under the component, as JSX, for pages + * whose scene composes a background. Copy React (page-actions/) emits them + * ahead of the component so the copied snippet reproduces the demo. Absent + * when the component fills the scene by itself. + */ + copySiblings?: readonly string[]; + /** + * Optional prose above the snippet. Single-paragraph notes are bare + * content; multi-paragraph notes bring their own

tags. + */ + usageNotes?: ReactNode; +} + +export const COMPONENT_PAGES: Record = { + aurora: { + Island: AuroraIsland, + usageSnippet: ` + +`, + usageNotes: ( + <> + The aurora fills its scene; compose with the container. For a curtain band hanging in a wide + dark sky, place a short ShaderScene near the top of a taller dark section.{' '} + coverage reveals the curtain from the bottom up โ€” 1 covers the canvas, 0 hides + it. + + ), + }, + blobs: { + Island: BlobsIsland, + copySiblings: [ + "", + ], + usageSnippet: ` + + +`, + usageNotes: ( + <> + The space between blobs is transparent โ€” stack Blobs over a gradient or any other layer and + it shows through. + + ), + }, + 'conic-gradient': { + Island: ConicGradientIsland, + usageSnippet: ` + +`, + }, + 'cursor-ripple': { + Island: CursorRippleIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> + Cursor Ripple is a cursor effect: stack it after any components inside a{' '} + <ShaderScene> and dragging the pointer across the canvas leaves a wake + that spreads and settles. Its refraction bends the image the Sources beneath it drew, and + not the output of an Effect mounted before it. Mode 1 only: it needs the scene's output + stage. + + ), + }, + 'cursor-smudge': { + Island: CursorSmudgeIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Move the pointer over the canvas without pressing. Higher decay values + recover faster. A still pointer adds no distortion, and the scene can idle after recovery + when its other components are still. Set strength to 0 to disable the effect. +

+

+ Set decay to 0 to keep the distortion after the pointer stops. New strokes + keep changing it. Raise decay to resume recovery, or set strength to 0 to clear it. + Resizing or remounting the scene also clears the distortion. An animated Source keeps + moving beneath the held distortion. +

+

+ Place Cursor Smudge after a Source inside <ShaderScene>. It bends the + image drawn by Sources; other Effects keep their own contributions unbent. Sampling past + the image extends its edge pixels. Cursor Smudge requires Mode 1. +

+

+ Enable scrollPush on the scene to smudge while a finger scrolls across it, + including the fling after release. The page keeps scrolling. Reduced motion slows recovery + to 0.3ร— speed; the explicit paused motion policy clears the effect. An + ordinary scene pause preserves the smudge until the scene resumes. +

+ + ), + }, + 'cursor-spotlight': { + Island: CursorSpotlightIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> + Cursor Spotlight is a cursor effect: stack it after any components inside a{' '} + <ShaderScene> and the area around the pointer brightens over whatever + they drew. It stays invisible until the pointer first moves over the canvas, fades out when + the pointer leaves, and lets the scene idle while the pointer is still. Mode 1 only: it + needs the scene's output stage. + + ), + }, + dissolve: { + Island: DissolveIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Dissolve is a post-process layer: stack it after any components inside a{' '} + <ShaderScene> and drive progress from 0 to 1 to show them + block by block. Hidden blocks are transparent, so the page shows through. +

+

+ It reads the alpha beneath it. Stacked after a feathered Radial Wipe, at{' '} + progress 1, it turns the wipe's soft edge into a ragged one, with the + wipe's feather setting how wide the ragged band is and{' '} + pixelSize its grain. A soft-edged source such as Blobs gets the same + treatment, which is the point of stacking a Dissolve over it. +

+ + ), + }, + dither: { + Island: DitherIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Dither is a post-process layer: stack it after any components inside a{' '} + <ShaderScene> and it pixelates the composed scene into chunky cells, + posterizing each channel to a few levels โ€” the scene keeps its own colors. +

+

+ pattern picks the threshold map that decides how in-between colors resolve: + the Bayer matrices trade smoothness (8x8) for crunch (2x2). pixelSize sets + the cell size in CSS pixels, and levels is how many steps each color channel + is allowed โ€” 2 is the harshest look, 6 and up reads as subtle + banding. spread dials the pattern's strength from clean posterize bands + (0) to gritty overshoot (2), and threshold gates + the effect by brightness โ€” slide it down to release the highlights until the effect is + gone. +

+ + ), + }, + 'dot-field': { + Island: DotFieldIsland, + usageSnippet: ` + +`, + }, + 'fractal-noise': { + Island: FractalNoiseIsland, + usageSnippet: ` + +`, + }, + 'god-rays': { + Island: GodRaysIsland, + usageSnippet: ` + +`, + usageNotes: ( + <> + Light rays radiate from center โ€” park it off-canvas for a top-of-page sun. The + rays emit over a transparent background; stack them above a dark layer. + + ), + }, + grain: { + Island: GrainIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Grain stacks inside any <ShaderScene> on top of whatever base component + you want โ€” gradients, noise fields, mesh gradients โ€” and applies a layer of animated grain + via the post-processing pipeline. +

+

+ Additive (default) adds signed grain so half the pixels brighten and half + darken, preserving average exposure โ€” pure texture, no exposure shift.{' '} + Subtractive takes the absolute value of the grain and subtracts it, so + the image only darkens. Subtractive simulates silver-halide film stock physics, where + exposed grain blocks light. +

+

+ speed controls the shutter cadence: 1 โ‰ˆ 60Hz (continuous shimmer + at 60fps), 0.4 โ‰ˆ 24Hz (chunky film cadence), 0 freezes the grain + pattern. +

+ + ), + }, + 'led-wall': { + Island: LedWallIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> + LED Wall is a post-process layer: stack it after any components inside a{' '} + <ShaderScene> and it screens the composed scene into a grid of square + dots, each lit with the scene color at its cell. The gaps are transparent unless{' '} + bleed lets the scene through. + + ), + }, + 'linear-gradient': { + Island: LinearGradientIsland, + usageSnippet: ` + +`, + }, + 'liquid-gradient': { + Island: LiquidGradientIsland, + usageSnippet: ` + +`, + }, + 'mesh-gradient': { + Island: MeshGradientIsland, + usageSnippet: ` + +`, + }, + 'radial-gradient': { + Island: RadialGradientIsland, + usageSnippet: ` + +`, + }, + 'radial-wipe': { + Island: RadialWipeIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> + Radial Wipe is a post-process layer: stack it after any components inside a{' '} + <ShaderScene> and drive progress from 0 to 1 to reveal them + from center. feather softens the front. Hidden areas are + transparent, so the page shows through. Stack a Dissolve after it to grain the soft edge. + + ), + }, + 'simplex-noise': { + Island: SimplexNoiseIsland, + usageSnippet: ` + +`, + }, + vignette: { + Island: VignetteIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Vignette stacks inside any <ShaderScene> on top of whatever base + component you want and fades the upstream pixels toward an edge color along a soft radial + ring. Unlike <Grain />, which generates new noise from uv, + Vignette reads the upstream pixel and mixes it toward color โ€” the{' '} + {`"read-upstream"`} half of the post-processing pipeline. +

+

+ feather controls how gradual the blend is. At 0 the ring is a + hard cutoff; at 1 the entire canvas is in the blend (a smooth radial gradient + from center to edge). radius is the outer edge of the ring;{' '} + center is the bright spot in normalized UV space. +

+ + ), + }, + voronoi: { + Island: VoronoiIsland, + usageSnippet: ` + +`, + }, + 'wave-lines': { + Island: WaveLinesIsland, + // The legacy page showed a live snippet mirroring the control panel; see + // the note in wave-lines/demo.tsx for why it is static here for now. + usageSnippet: ` + +`, + }, +}; diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/packages/shaders/src/components/index.ts b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/packages/shaders/src/components/index.ts new file mode 100644 index 0000000..1f69400 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/packages/shaders/src/components/index.ts @@ -0,0 +1,79 @@ +// The Tier 1 components and the prop shapes they share. Each component folder +// holds a wrapper (props, uniforms, mesh lifecycle) and a shader.tsx (the TSL). +// Only the wrappers and their public types leave this barrel; the *Shader +// functions and DEFAULT_* constants are implementation detail. +export { Aurora } from './aurora/aurora.js'; +export type { AuroraProps } from './aurora/aurora.js'; + +export { Blobs } from './blobs/blobs.js'; +export type { BlobsProps } from './blobs/blobs.js'; + +export { ConicGradient } from './conic-gradient/conic-gradient.js'; +export type { ConicGradientProps } from './conic-gradient/conic-gradient.js'; + +export { CursorRipple } from './cursor-ripple/cursor-ripple.js'; +export type { CursorRippleProps } from './cursor-ripple/cursor-ripple.js'; +// The shader half is public so a caller can mount it over a wave field it +// drives itself. The docs' dev probe route does that with a hand-seeded +// field, for a deterministic capture. +export { CursorRippleShader } from './cursor-ripple/shader.js'; +export type { CursorRippleShaderProps } from './cursor-ripple/shader.js'; + +export { CursorSmudge } from './cursor-smudge/cursor-smudge.js'; +export type { CursorSmudgeProps } from './cursor-smudge/cursor-smudge.js'; + +export { CursorSpotlight } from './cursor-spotlight/cursor-spotlight.js'; +export type { CursorSpotlightProps } from './cursor-spotlight/cursor-spotlight.js'; + +export { Dissolve } from './dissolve/dissolve.js'; +export type { DissolveProps } from './dissolve/dissolve.js'; + +export { Dither } from './dither/dither.js'; +export type { DitherProps } from './dither/dither.js'; + +export { DotField } from './dot-field/dot-field.js'; +export type { DotFieldProps, DotShape, SvgMarkup } from './dot-field/dot-field.js'; + +export { FractalNoise } from './fractal-noise/fractal-noise.js'; +export type { FractalNoiseProps } from './fractal-noise/fractal-noise.js'; +export type { FractalNoiseStyle } from './fractal-noise/shader.js'; +export { STYLE_DIAL_DEFAULTS } from './fractal-noise/style-dial-defaults.js'; + +export { GodRays } from './god-rays/god-rays.js'; +export type { GodRaysProps } from './god-rays/god-rays.js'; + +export { Grain } from './grain/grain.js'; +export type { GrainProps } from './grain/grain.js'; +export type { GrainBlend } from './grain/shader.js'; + +export { LedWall } from './led-wall/led-wall.js'; +export type { LedWallProps } from './led-wall/led-wall.js'; + +export { LinearGradient } from './linear-gradient/linear-gradient.js'; +export type { LinearGradientProps } from './linear-gradient/linear-gradient.js'; + +export { LiquidGradient } from './liquid-gradient/liquid-gradient.js'; +export type { LiquidGradientProps } from './liquid-gradient/liquid-gradient.js'; + +export { MeshGradient } from './mesh-gradient/mesh-gradient.js'; +export type { MeshGradientProps } from './mesh-gradient/mesh-gradient.js'; + +export { RadialGradient } from './radial-gradient/radial-gradient.js'; +export type { RadialGradientProps } from './radial-gradient/radial-gradient.js'; + +export { RadialWipe } from './radial-wipe/radial-wipe.js'; +export type { RadialWipeProps } from './radial-wipe/radial-wipe.js'; + +export { SimplexNoise } from './simplex-noise/simplex-noise.js'; +export type { SimplexNoiseProps } from './simplex-noise/simplex-noise.js'; + +export { Vignette } from './vignette/vignette.js'; +export type { VignetteProps } from './vignette/vignette.js'; + +export { Voronoi } from './voronoi/voronoi.js'; +export type { VoronoiProps } from './voronoi/voronoi.js'; + +export { WaveLines } from './wave-lines/wave-lines.js'; +export type { WaveLine, WaveLinesProps } from './wave-lines/wave-lines.js'; + +export type { ColorStop, Palette } from './shared/color.js'; diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/packages/shaders/src/engine.ts b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/packages/shaders/src/engine.ts new file mode 100644 index 0000000..9f46b20 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/packages/shaders/src/engine.ts @@ -0,0 +1,124 @@ +// The framework-free half of @camp-dev/shaders: TSL primitives, the renderer, +// the scheduler, and CPU color math. src/react and src/components import this +// barrel rather than the root index, which keeps the dependency direction one +// way (root -> react -> engine, root -> components -> react -> engine) and +// leaves this half extractable into its own package if a second framework +// binding ever becomes real. + +export { createRenderer } from './runtime/create-renderer/create-renderer.js'; +export type { + GpuRenderer, + GpuBackend, + CreateRendererOptions, +} from './runtime/create-renderer/create-renderer.js'; + +export { CursorInput } from './inputs/cursor-input/cursor-input.js'; +export type { + CursorInputOptions, + StrokePoint, + Vector2, +} from './inputs/cursor-input/cursor-input.js'; +export type { PushStroke } from './inputs/scroll-push/scroll-push.js'; + +export { colorRamp } from './primitives/color-ramp/color-ramp.js'; +export type { ColorRampStop, TSLNode } from './primitives/color-ramp/color-ramp.js'; + +export { mixColor } from './primitives/color-space/index.js'; +export type { ColorSpace, HueInterpolation } from './primitives/color-space/index.js'; + +// Re-exported from the ./color subpath, which is where these actually live. +// Keeping them here means no existing import breaks; new call sites in the docs +// site are pushed to the subpath by a lint rule in the root eslint config, +// because reaching color math through this barrel drags in three/webgpu. +export { + linearChannelToSrgb, + linearSrgbToLinearDisplayP3, + linearSrgbToOklch, + oklabToLinearSrgb, + oklchInGamut, + oklchToGamut, + oklchToLinearSrgb, + parseColorString, + parseOklchString, + srgbChannelToLinear, +} from './color.js'; +export type { OutputGamut } from './color.js'; + +export { simplexNoise } from './primitives/noise/noise.js'; + +export { fractalNoise } from './primitives/fbm/fbm.js'; +export type { FractalFold, FractalNoiseOptions } from './primitives/fbm/fbm.js'; + +export { voronoi } from './primitives/voronoi/voronoi.js'; + +export { voronoiCells } from './primitives/voronoi/voronoi-cells.js'; +export type { + VoronoiCellsOptions, + VoronoiCellsResult, +} from './primitives/voronoi/voronoi-cells.js'; + +export { MAX_BLOBS, metaballs } from './primitives/metaballs/metaballs.js'; +export type { MetaballsOptions, MetaballsResult } from './primitives/metaballs/metaballs.js'; + +export { quantize } from './primitives/quantize/quantize.js'; + +export { signedDistanceFieldCircle } from './primitives/sdf-circle/sdf-circle.js'; +export { signedDistanceFieldCross } from './primitives/sdf-cross/sdf-cross.js'; + +export { displace } from './primitives/displace/displace.js'; + +export { cursorRipple } from './primitives/cursor-ripple/cursor-ripple.js'; +export type { CursorRippleOptions } from './primitives/cursor-ripple/cursor-ripple.js'; + +export { elapsedTime } from './primitives/time/time.js'; + +export { holdRendererClock, resetRendererClock } from './runtime/clock/reset-clock.js'; + +export { grain } from './primitives/grain/grain.js'; +export { stableHash, stableHashUint } from './primitives/stable-hash/stable-hash.js'; + +export { dither } from './primitives/dither/dither.js'; +export { ditherThreshold } from './primitives/dither-pattern/dither-pattern.js'; +export type { DitherPattern } from './primitives/dither-pattern/dither-pattern.js'; + +export { + setReducedMotionPolicy, + getReducedMotionPolicy, + getReducedMotionTimeScale, + createReducedMotionWatcher, +} from './runtime/reduced-motion/reduced-motion.js'; +export type { + ReducedMotionPolicy, + ReducedMotionWatcher, +} from './runtime/reduced-motion/reduced-motion.js'; + +export { createVisibilityWatcher } from './runtime/visibility/visibility.js'; +export type { VisibilityWatcher } from './runtime/visibility/visibility.js'; + +export { createIntersectionWatcher } from './runtime/intersection/intersection.js'; +export type { IntersectionWatcher } from './runtime/intersection/intersection.js'; + +export { FrameScheduler } from './runtime/frame-scheduler/frame-scheduler.js'; + +export { createOutputStage } from './runtime/output-stage/output-stage.js'; +export type { TimeGpu } from './runtime/gpu-timer/gpu-timer.js'; +export type { + OutputStage, + PostProcessTransform, + PrePass, + UvTransform, +} from './runtime/output-stage/output-stage.js'; + +export { createWaveField, dampingForLifetime } from './runtime/wave-field/wave-field.js'; +export type { + WaveField, + WaveFieldStroke, + WaveFieldTuning, +} from './runtime/wave-field/wave-field.js'; + +export { createTexturePass } from './runtime/texture-pass/texture-pass.js'; +export type { TexturePass, TexturePassOptions } from './runtime/texture-pass/texture-pass.js'; + +export { createPauseWatcher } from './runtime/pause-watcher/pause-watcher.js'; +export type { PauseWatcher } from './runtime/pause-watcher/pause-watcher.js'; +export type { SchedulerTick, SchedulerClient } from './runtime/frame-scheduler/frame-scheduler.js'; diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/scripts/build-posters.sh b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/scripts/build-posters.sh new file mode 100644 index 0000000..a57ffc8 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/60b67b513ab1a21d2e33abc9dbf7b7b342c775aa/scripts/build-posters.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# Regenerate every committed poster image from its docs scene module. +# Each poster's --source is the SAME scene the live docs page renders, so the +# poster cannot drift from the demo. Runs one at a time: the poster dev server +# binds a fixed port and concurrent runs collide. +# +# After each capture, build-thumbnail.mjs cuts the component's card thumbnail +# for the components index from the new poster. Pass --thumbnails-only to +# re-cut every thumbnail from the committed posters without capturing any. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +CLI="node packages/shaders-cli/dist/index.js poster" +COMPONENTS_DIR="apps/docs/src/app/components" +OUT_DIR="apps/docs/public/posters" +WIDTH=1080 +HEIGHT=720 + +THUMBNAILS_ONLY=0 +case "${1:-}" in + "") ;; + --thumbnails-only) THUMBNAILS_ONLY=1 ;; + *) + echo "error: unknown argument $1. The only option is --thumbnails-only." >&2 + exit 1 + ;; +esac + +if [ "$THUMBNAILS_ONLY" = 0 ] && [ ! -f packages/shaders-cli/dist/index.js ]; then + echo "error: CLI not built. Run: pnpm --filter @camp-dev/shaders-cli build" >&2 + exit 1 +fi + +# name:format[:background][:WxH][:crop] entries (png for the flat shaders, jpg +# for the busy ones; background is optional and only needed for shaders with a +# transparent base layer, e.g. aurora, so they flatten onto a sensible color; +# WxH overrides the default capture size and is only needed for pixel-sized +# shaders like dot-field, whose posters render pixel-locked in the demo โ€” see +# DemoPoster's pixelSize prop โ€” and so must be captured larger than any demo +# box they'll be cropped into) +# +# crop is the side, in poster pixels, of the center square the card thumbnail +# is cut from. Without it the thumbnail keeps the poster's full height. Only a +# pixel-sized shader needs one, because its poster keeps the pattern at its +# real on-screen size and a full-height cut shrinks the pattern out of sight +# (build-thumbnail.mjs works through DotField's numbers). DotField's 360 is +# six of its 60-pixel cells across. +# +# radial-gradient is the exception to "flat means png": PNG's row filters +# predict each pixel from its left and upper neighbours, which crushes a linear +# gradient (every row identical, 35 KB) but does almost nothing for a radial one +# (every row different, 1.3 MB). JPEG lands at 36 KB with no banding visible at +# poster size. +for pair in \ + "linear-gradient:png" \ + "simplex-noise:png" \ + "aurora:jpg:#0b0f1a" \ + "grain:jpg" \ + "mesh-gradient:jpg" \ + "wave-lines:jpg" \ + "vignette:jpg" \ + "dot-field:png:#0a0a14:2048x1280:360" \ + "radial-gradient:jpg" \ + "god-rays:jpg:#0b0f1a" \ + "conic-gradient:jpg" \ + "liquid-gradient:jpg" \ + "dither:jpg" \ + "voronoi:jpg" \ + "fractal-noise:jpg" \ + "blobs:jpg" \ + "led-wall:jpg:#0b0f0d" \ + "cursor-ripple:jpg" \ + "cursor-smudge:jpg" \ + "cursor-spotlight:jpg" \ + "radial-wipe:jpg:#0b0f0d" \ + "dissolve:jpg:#0b0f0d"; do + IFS=':' read -r name format background size crop <<< "$pair" + width="$WIDTH" + height="$HEIGHT" + if [ -n "${size:-}" ]; then + width="${size%x*}" + height="${size#*x}" + fi + poster="${OUT_DIR}/${name}.${format}" + if [ "$THUMBNAILS_ONLY" = 0 ]; then + echo "==> $name ($format, ${width}x${height})" + args=( + --source "${COMPONENTS_DIR}/${name}/scene.tsx" + --output "$poster" + --format "${format}" + --width "$width" + --height "$height" + ) + if [ -n "$background" ]; then + args+=(--background "$background") + fi + $CLI "${args[@]}" + fi + thumbnail_args=("$poster") + if [ -n "${crop:-}" ]; then + thumbnail_args+=(--crop "$crop") + fi + node scripts/build-thumbnail.mjs "${thumbnail_args[@]}" +done + +if [ "$THUMBNAILS_ONLY" = 1 ]; then + echo "All thumbnails regenerated." + exit 0 +fi + +# The homepage hero gets its own Aurora poster at the hero's 1636 by 696. +# Aurora always fits the sky band to the canvas height and widens it at the +# sides, so the 3:2 poster, cover-cropped into the wider hero, showed a +# zoomed-in slice that jumped when the live scene replaced it. A poster at +# least as wide as the box crops only its sides, the way the shader does. +echo "==> aurora-hero (jpg, 1636x696)" +$CLI \ + --source "${COMPONENTS_DIR}/aurora/scene.tsx" \ + --output "${OUT_DIR}/aurora-hero.jpg" \ + --format jpg \ + --width 1636 \ + --height 696 \ + --background "#0b0f1a" + +# The homepage favorites render every live scene at 320 by 233, the card +# window's size at the grid's full 1440px (SCENE_SIZE in +# apps/docs/src/components/favorites/favorites-list.tsx), and scale it with +# the card. Each favorite's card poster is captured at that size, over the +# ground its scene sits on there, so the poster frames and crops the scene +# the way the live canvas does. A 3:2 poster cropped into the card drifted +# from shaders that stretch to the canvas, such as Simplex Noise, and from +# ones that size their cells in CSS pixels, such as Dither and LED Wall. +# Entries are name:format[:background]; the backgrounds are the demos' +# backdrops, and the page color for LED Wall's gaps. +for pair in \ + "simplex-noise:png" \ + "mesh-gradient:jpg" \ + "wave-lines:jpg:#0a0a14" \ + "voronoi:jpg" \ + "dither:jpg" \ + "god-rays:jpg:linear-gradient(to top, #131b31, #0b0f1a)" \ + "led-wall:jpg:#0b0f0d"; do + IFS=':' read -r name format background <<< "$pair" + echo "==> ${name}-card ($format, 320x233)" + card_args=( + --source "${COMPONENTS_DIR}/${name}/scene.tsx" + --output "${OUT_DIR}/${name}-card.${format}" + --format "$format" + --width 320 + --height 233 + ) + if [ -n "$background" ]; then + card_args+=(--background "$background") + fi + $CLI "${card_args[@]}" +done + +# The Components banner is captured from its own scene at the mock's width, +# 1728 by the 200px header block, and shown at that size, center-cropped, by +# banner-shader.tsx. Past 864px either side of center the glow has already +# reached page black, so a wider viewport shows the page past the poster's +# edges with no seam. +echo "==> banner (jpg, 1728x200)" +$CLI \ + --source apps/docs/src/components/section-banner/banner-scene.tsx \ + --output "${OUT_DIR}/banner.jpg" \ + --format jpg \ + --width 1728 \ + --height 200 \ + --background "#0b0f0d" + +echo "All posters regenerated." diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/.changeset/fluted-glass.md b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/.changeset/fluted-glass.md new file mode 100644 index 0000000..e884e63 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/.changeset/fluted-glass.md @@ -0,0 +1,16 @@ +--- +'@camp-dev/shaders': minor +--- + +Add `FlutedGlass`, an Effect that shows the image beneath it through repeating flutes of glass. Mount it after a Source inside a `ShaderScene`. Each flute bends the image under it into a squeezed copy, catches a highlight along the wall that faces the light, and darkens near its grooves where the glass turns steep. Its props: + +- `fluteWidth` sets each flute's width in CSS pixels. +- `angle` sets the direction the flutes run, in the degrees `LinearGradient` uses, so the default of 90 gives vertical flutes. +- `refraction` sets how far the glass bends the image, and `refraction={0}` leaves it unchanged. +- `crossSection` sets each flute's shape, from a pointed `'prism'` through a `'round'` lens to a `'flat'` top with steep walls, or any number from 0 to 1 between them. +- `lightAngle` sets where the light comes from, in degrees that turn the same way as `angle`: 0 from the right and 90 from above. It defaults to 135, the upper left. The light stays put while `angle` turns the glass. +- `highlight` and `shadow` set how strong the highlight and the shadow are, from 0 to 1. `highlightColor` tints the highlight. + +Every prop except `highlightColor` takes a static value or an animation signal. Near the canvas edge, the glass mirrors the image back onto the canvas. `FlutedGlass` bends only what Sources drew, never the output of an Effect such as `Grain`, so mount such Effects after it. It works in Mode 1 only. + +Add the `fluteSurface` primitive for Mode 2. For a position, it returns the bend to add before you sample your own texture, the slope and steepness of the glass, and how far the position sits from its flute's center. diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/GLOSSARY.md b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/GLOSSARY.md new file mode 100644 index 0000000..2f579a5 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/GLOSSARY.md @@ -0,0 +1,127 @@ +# Shaders + +A React shader component library on WebGPU and Three.js TSL. This glossary holds the terms the project uses for its own concepts, so code, docs, and issues say the same thing. + +## Language + +### Scene model + +**Source**: +A component that draws its own image into a scene, such as MeshGradient or DotField. It is the thing an Effect acts on. +_Avoid_: Base component, generator, layer + +**Effect**: +A component that acts on the image beneath it in a scene rather than drawing its own, such as Grain, Dither, or LedWall. An Effect composes over any Source or Effect mounted before it. +_Avoid_: Overlay, post-process pass, post-process layer, filter + +**UV warp**: +An Effect's change to where the image beneath it is sampled, such as Dither's pixel snap or CursorRipple's refraction. A UV warp bends only what Sources drew. +_Avoid_: Warp on its own, base UV transform + +**Resample**: +An Effect's rule for building each pixel from samples of what the Sources beneath it drew, such as FlutedGlass's color split. A UV warp is a resample that takes one sample. +_Avoid_: Gather, sampler transform, tap stage + +**Input**: +A value that changes over time and is not drawn, such as the cursor, scroll, or canvas size. An Input is fed to a component as an animation signal. +_Avoid_: Cursor hook, entry point, scene-level field + +**Position prop**: +A component prop that names a point on the canvas. A component's own center is `center`; a point that belongs to one reaction among the component's other dials is named after the reaction, such as `swellCenter`. It is a position, not a cursor: the cursor is one Input you can feed it. The `'cursor'` shorthand expresses that mapping without a hook, as in `swellCenter="cursor"`. +_Avoid_: Cursor prop, interactive prop, entry point, focus + +**Reaction**: +A component's response that grows toward a point and needs the component's own internals, such as LedWall's swell. A reaction exposes three props: the amount named for the reaction, where 0 turns it off, a `Radius` in canvas units, and a `Center` position prop. +_Avoid_: Interaction, cursor ability, hover effect + +**Cursor effect**: +An Effect driven by the cursor Input that works over any scene with no help from the components beneath it. CursorSpotlight and CursorRipple are two. +_Avoid_: Interactive shader, cursor component, drop-in + +**Scroll push**: +The push a scene's cursor effects feel from a touch that scrolls the page over the canvas, at the finger and on through the fling. A scene opts in, and the page scrolls exactly as it would without it. +_Avoid_: Hold to interact, touch drag, scroll drag + +**Scripted stroke**: +A stroke code draws across a scene's cursor effects by moving a point. While code strokes, the effects ignore the mouse and scroll push. +_Avoid_: Virtual pointer, fake cursor, brush + +### Rendering + +**Render on demand**: +A scene draws every frame until its components' idle votes let it park. A parked scene does no GPU work. An Input or a prop change asks for frames only until its value settles, and the scene stays parked. An animated vote restarts the frame loop. +_Avoid_: Idle mode, frameloop demand, static scene + +**Idle vote**: +A component's say in whether its scene may park. A component votes idle while nothing it draws can change, votes animated while anything can, or casts no vote; a scene parks only when at least one component votes idle and none votes animated. +_Avoid_: Static hint, render-on-demand vote, static vote + +**Frame cap**: +The most frames per second a scene draws, set by its app. With no frame cap, a scene draws at the display's refresh rate, so a 120 Hz display gets 120 frames a second. Frames a prop change asks for count against the frame cap. A frame cap never wakes a parked scene or resumes a paused one. +_Avoid_: Frame budget, target frame rate, fps limit + +**Paused scene**: +A scene frozen on its current frame, by its app through `paused`, or because the tab is hidden or the canvas is off screen. Time stops while a scene is paused, so it resumes on the frame it stopped on. A parked scene stops drawing because nothing it draws can change. A paused scene stops whatever its components vote. +_Avoid_: Frozen scene, stopped scene + +### Pattern components + +**Mark**: +The single figure drawn at each grid point of a pattern component such as DotField. Every mark is a coverage at a cell-local point, computed from a signed distance for a built-in mark or read from a decoded tile for a custom mark, so sizing and ripple displacement work the same for every mark. +_Avoid_: Glyph, symbol, icon, dot (except inside shipped identifiers such as `DotField` and `dotSize`) + +**Built-in mark**: +A mark the library ships by name, such as the circle and the cross. Its edge is an exact distance, so it stays crisp at any size. +_Avoid_: Preset shape, default shape + +**Custom mark**: +A mark the caller supplies as inline SVG markup. Only its coverage is read, so it takes the field's color like any other mark. +_Avoid_: Custom shape, user shape, icon + +### Fluted glass + +**Flute**: +One ridge of FlutedGlass: a strip of glass with a fixed width in CSS pixels that shows a squeezed copy of the image beneath it. Flutes repeat side by side across the glass. +_Avoid_: Reed, rib, bar, stripe, cell + +**Cross-section**: +The shape of one flute seen from its end, on a scale from a pointed prism through a round lens to a flat top with steep walls. +_Avoid_: Profile, distortion shape, preset + +**Groove**: +The line where two neighboring flutes meet, where the glass is steepest. +_Avoid_: Seam, edge, crease + +### Docs site + +**Search trigger**: +The button in the site header that opens the search panel and focuses its input. It is not an input itself. +_Avoid_: Search bar, search box, search input (for the header control) + +**Search panel**: +The dialog the search trigger opens, holding the query input and the result list over the blurred page. +_Avoid_: Search modal, search dialog, command palette + +**Demo**: +The live scene and its control panel at the top of a component page. The homepage hero turns into Aurora's demo as the page scrolls. +_Avoid_: Editor, playground + +**Favorite**: +A component hand-picked for the homepage's favorites grid. It links to its component page. +_Avoid_: Featured component, highlight + +**Card tab**: +The strip that cuts into a favorite's edge to show the favorite's short name, such as "Simplex" for Simplex Noise, when the card is hovered or focused. +_Avoid_: Tab, name tab, label tab + +**Feature card**: +A card in the homepage's features section that illustrates one quality of the library, such as "Composable" or "Reactive". Unlike a favorite, a feature card shows no component. +_Avoid_: Benefit card, value prop, pillar, featured card + +**Story**: +The animation a feature card plays when the pointer moves over it. Once started, a story always finishes, and the card returns to its still frame. +_Avoid_: Intro, entrance, demo, hover effect + +**Still frame**: +The one frame a feature card shows whenever its story is not playing. It need not be the story's first or last frame. +_Avoid_: End frame, rest state, poster diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts new file mode 100644 index 0000000..3a3aa7b --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts @@ -0,0 +1,254 @@ +import type { Page } from '@playwright/test'; + +import { expect, test } from './fixtures'; +import { waitForShader } from './helpers'; + +test('FlutedGlass โ€” default story', async ({ page }) => { + await page.goto('/components/fluted-glass?visualTest=1'); + await waitForShader(page); + await expect(page.locator('canvas').first()).toHaveScreenshot('fluted-glass-default.png'); +}); + +// The probe draws hard diagonal stripes under FlutedGlass at its defaults. +// Query params set the glass's dials, drop the glass with glass=none, or drop +// the stripes with source=none. +const PROBE = '/dev/fluted-glass-probe?visualTest=1'; +// FlutedGlass's default flute width in CSS pixels, which is device pixels at +// the config's deviceScaleFactor of 1. A flute's center sits on the canvas +// center, so the grooves fall half a flute either side of it and every flute +// width beyond. +const FLUTE_WIDTH = 40; +// How far either side of a groove each sample column sits, in pixels. Wide +// enough to step clear of the groove's own pixel, narrow enough that +// unbroken stripes rarely change between the two columns. +const COLUMN_OFFSET = 3; +// The least the highlight must brighten the walls that face the light, in +// brightness levels from 0 to 255, averaged over those walls. The defaults +// measured about 18 on macOS, so this leaves room for another GPU. +const GAIN_ON_LIT_WALLS = 6; + +/** The probe's canvas as each pixel's brightness, the mean of its channels. */ +interface Brightness { + width: number; + /** Brightness from 0 to 255 at a pixel. */ + at: (x: number, y: number) => number; + /** The middle band of rows, clear of the site header drawn over the canvas. */ + rows: number[]; +} + +/** + * Loads the probe with `query` appended and reads its canvas. The screenshot + * decodes in the page, which has the browser's PNG decoder, and comes back + * one byte of brightness per pixel. + */ +async function probeBrightness(page: Page, query: string): Promise { + await page.goto(`${PROBE}${query}`); + await page.locator('canvas').first().waitFor(); + await waitForShader(page); + + const shot = await page.locator('canvas').first().screenshot(); + const { width, height, bytes } = await page.evaluate(async (pngBase64) => { + const response = await fetch(`data:image/png;base64,${pngBase64}`); + const bitmap = await createImageBitmap(await response.blob()); + const canvas = document.createElement('canvas'); + + canvas.width = bitmap.width; + canvas.height = bitmap.height; + const context = canvas.getContext('2d')!; + + context.drawImage(bitmap, 0, 0); + const rgba = context.getImageData(0, 0, bitmap.width, bitmap.height).data; + let binary = ''; + + for (let index = 0; index < rgba.length; index += 4) { + const sum = (rgba[index] ?? 0) + (rgba[index + 1] ?? 0) + (rgba[index + 2] ?? 0); + + binary += String.fromCharCode(Math.round(sum / 3)); + } + + return { width: bitmap.width, height: bitmap.height, bytes: btoa(binary) }; + }, shot.toString('base64')); + const pixels = Buffer.from(bytes, 'base64'); + const rows: number[] = []; + + for (let y = Math.floor(height * 0.2); y < Math.floor(height * 0.8); y += 1) rows.push(y); + + return { width, at: (x, y) => pixels[y * width + x] ?? 0, rows }; +} + +/** The x of every groove on a canvas `width` pixels wide, left to right. */ +function grooves(width: number): number[] { + const center = width / 2; + const first = + center + FLUTE_WIDTH / 2 - Math.floor((center + FLUTE_WIDTH / 2) / FLUTE_WIDTH) * FLUTE_WIDTH; + const positions: number[] = []; + + for (let groove = first; groove < width; groove += FLUTE_WIDTH) { + positions.push(Math.round(groove)); + } + + return positions; +} + +/** + * The share of rows where the pixels just left and just right of a groove + * fall on different sides of mid-gray, over every groove on the canvas. On + * unbroken stripes the two columns differ only where a stripe edge passes + * between them. Through the glass they read the image from far apart, so + * they differ far more often. + */ +function grooveBreaks(image: Brightness): number { + let pairs = 0; + let breaks = 0; + + for (const groove of grooves(image.width)) { + const left = groove - COLUMN_OFFSET; + const right = groove + COLUMN_OFFSET; + + if (left < 0 || right >= image.width) continue; + for (const y of image.rows) { + pairs += 1; + if (image.at(left, y) > 128 !== image.at(right, y) > 128) breaks += 1; + } + } + + return breaks / pairs; +} + +/** + * How much brighter `lit` is than `unlit` on average, over the left half and + * the right half of every whole flute, two pixels clear of the grooves and + * the center. + */ +function wallGain(lit: Brightness, unlit: Brightness): { left: number; right: number } { + let left = 0; + let right = 0; + let count = 0; + + for (const groove of grooves(lit.width)) { + if (groove + FLUTE_WIDTH >= lit.width) continue; + for (const y of lit.rows) { + for (let step = 2; step <= FLUTE_WIDTH / 2 - 2; step += 1) { + const leftX = groove + step; + const rightX = groove + FLUTE_WIDTH - step; + + left += lit.at(leftX, y) - unlit.at(leftX, y); + right += lit.at(rightX, y) - unlit.at(rightX, y); + count += 1; + } + } + } + + return { left: left / count, right: right / count }; +} + +/** + * The x of every column `near` to `far` pixels out from a groove, on both + * sides of every groove on a canvas `width` pixels wide. Column 0 touches + * the groove. + */ +function columnsNearGrooves(width: number, near: number, far: number): number[] { + const columns: number[] = []; + + for (const groove of grooves(width)) { + for (let step = near; step <= far; step += 1) { + columns.push(groove - 1 - step, groove + step); + } + } + + return columns.filter((x) => x >= 0 && x < width); +} + +/** + * The share of `unshaded`'s brightness that `shaded` loses, over the columns + * `near` to `far` pixels out from every groove. + */ +function shadowShare(shaded: Brightness, unshaded: Brightness, near: number, far: number): number { + let lost = 0; + let total = 0; + + for (const x of columnsNearGrooves(shaded.width, near, far)) { + for (const y of shaded.rows) { + lost += unshaded.at(x, y) - shaded.at(x, y); + total += unshaded.at(x, y); + } + } + + return lost / total; +} + +/** The largest brightness difference between two loads, pixel for pixel. */ +function maxDifference(first: Brightness, second: Brightness): number { + let largest = 0; + + for (const y of first.rows) { + for (let x = 0; x < first.width; x += 1) { + largest = Math.max(largest, Math.abs(first.at(x, y) - second.at(x, y))); + } + } + + return largest; +} + +// A regenerated baseline of inert glass would pass its own screenshot, so +// this checks that the glass breaks the stripes at all. Both loads turn the +// light off, since the shadow and highlight also change the columns either +// side of a groove, and this measures the bend alone. The same probe with +// refraction 0 is the inert reference. At 1280 px wide a stripe repeats every +// 113 px across a row, so the two columns differ on about 11% of rows without +// the glass. Through it, they read the image 28 px apart, and differ on +// about half. +test('FlutedGlass โ€” stripes break at the grooves', async ({ page }) => { + const inert = grooveBreaks(await probeBrightness(page, '&highlight=0&shadow=0&refraction=0')); + const glass = grooveBreaks(await probeBrightness(page, '&highlight=0&shadow=0')); + + expect(glass, `groove breaks: glass ${glass}, inert ${inert}`).toBeGreaterThan(inert + 0.2); +}); + +// The default light comes from the upper left, so over vertical flutes the +// left wall of each flute faces it and catches the highlight, and the right +// wall faces away. Both loads turn the shadow off, so the highlight is the +// only difference between them. +test('FlutedGlass โ€” the highlight lights the walls that face the light', async ({ page }) => { + const unlit = await probeBrightness(page, '&highlight=0&shadow=0'); + const lit = await probeBrightness(page, '&shadow=0'); + const gain = wallGain(lit, unlit); + const message = `brightness gain: left walls ${gain.left}, right walls ${gain.right}`; + + expect(gain.left, message).toBeGreaterThan(GAIN_ON_LIT_WALLS); + expect(gain.right, message).toBeLessThan(gain.left / 4); +}); + +// A flat top is flat across its middle and steep only at its walls, so its +// shadow belongs on the walls. Both loads turn the highlight off, and only +// the shadow differs between them. Columns 0 and 1 out from a groove sit on +// the walls. Columns 4 to 9 sit a fifth to a half of the way to the center: +// inside the shadow's reach, but on glass that is flat. +test('FlutedGlass โ€” on a flat top the shadow stays on the walls', async ({ page }) => { + const unshaded = await probeBrightness(page, '&crossSection=flat&highlight=0&shadow=0'); + const shaded = await probeBrightness(page, '&crossSection=flat&highlight=0&shadow=1'); + const walls = shadowShare(shaded, unshaded, 0, 1); + const flatTop = shadowShare(shaded, unshaded, 4, 9); + const message = `share of brightness the shadow takes: walls ${walls}, flat top ${flatTop}`; + + expect(walls, message).toBeGreaterThan(0.1); + expect(flatTop, message).toBeLessThan(0.02); +}); + +// With nothing to bend and no light, the glass draws the Source exactly as +// the Source alone does. +test('FlutedGlass โ€” inert glass leaves the Source unchanged', async ({ page }) => { + const source = await probeBrightness(page, '&glass=none'); + const inert = await probeBrightness(page, '&refraction=0&highlight=0&shadow=0'); + + expect(maxDifference(source, inert)).toBeLessThanOrEqual(1); +}); + +// Over a transparent scene the canvas shows the probe page's black +// background. Light added to a pixel with no coverage would show on it. +test('FlutedGlass โ€” a transparent pixel gets no highlight', async ({ page }) => { + const black = await probeBrightness(page, '&glass=none&source=none'); + const glass = await probeBrightness(page, '&source=none&highlight=1&shadow=1'); + + expect(maxDifference(black, glass)).toBeLessThanOrEqual(1); +}); diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts-snapshots/fluted-glass-default-chromium-darwin.png b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts-snapshots/fluted-glass-default-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..92c353529320beb06ed76383e9475b1039319b8f GIT binary patch literal 128114 zcmV)UK(N1wP){yBVK`Tq6$aDQLCzrK(6_n+^}|BwIY|M-vp{r45u zuV23+uD^c$bzR@bU)TB*>(3{L%&)Ke>nnf%{{H9tz`EwU?H*uHx{r{Ci zqRRT@`&~tRO8oS6R3*Ouf3N@FtGB|Uf3aa+<%C0m2d{t||@~`{9|Hps)RvZ8NZ*1@Cf$tl?%ij0yvRLjH zyuH^a1C9>tY`Gs*Z=t6|M3r&`>R^b_I_9XyUFX`Pe1Jv@AahWseQ`W zy}E`J>b!dCGd6DQ5&gOUYt{bq?{|Cu@%#7n>v+E3qdL|IU*)cLt#{#G<<|I2_GFdi z<4XP9f9};)tHEFS>r?J-FGuoj?^W$xIY9mVxd*GgpFD3o`*xnMhr#CFyS1M@;vfIp zKfZoKX+b?Z?zKh_N78zt-y^ZkKN4sCa;?#}uI%&c*Z2QX z_df2@amg(3JTK4Z*C+kO^QbH9{95IHAAfy{+rNIF?{Ys^zVEtLZtFZQ=EFL#d=Qme zt~99QDrMC_s}+8}>s)mJs`RWL_+_dRp7D z`-HuET?qDb_v+PGqMqPt-FP}#*XL7?k|wxe5f^`g#GQ{!g*D-}#WKvh}5vo>?Z$whm*TGY#vr>d!P_S*4kGx#QO@3zFA^Mhk*A zC_JCOi|2P)uDpVoW*zTJ8TIohca`Y1+POisjidC+hgY(D+jEPZKYOYAYB!X5259zL zDd=A@OHT@6K9^G)aTMkIC@Q6$8Yn*WXsbc?6QA}%M%CW#leRh7!|EBzT-5a?BQcU2dj~0#@*3-B%;IbPWC+P72zMnNtFqjoQ z&*R+3)#p0;*BW~z_0IGd(n_MYG|m1UQPkdbu8P*1laO5ASD>u<>&{_H=N8*>SFyaF z%CiJ#nC$-Kxo_ANzdN7O3R^ngWxxaA_1xDN3)=BkxlC_!A-8LYocFe-QF_-*sIqQL zv(7*0+c`ipF?Y?8tmh}4v~tL-n%sAnUiqva7MMo2v0JgPjDCo&Vda^fr}AHAdE50I zOH*oGMNjG2agRgq&){8XO$^t;W&ya4@da;3Yj^ zv;@6re5K4zr~RHbbI4cI-pQ2`efTiKA1V7yyhtLqfWZJ9wSi?N`$gC)k_mmB_TwP=p~|9s^d zA!&vkbN>B88e!yVcViQYS*dZ@w(IpyxE(!?ahzBFzmP8}#Ufk(A&n2xN=ZDJ>5*H$ z)!~|DRehHJ82PYWlOx%0A=ZgvG7L7tgLSTx2$g^JxnM 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts-snapshots/fluted-glass-default-chromium-linux.png b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs-tests/visual/fluted-glass.spec.ts-snapshots/fluted-glass-default-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..b1cac8f14fe2435667dc96659ef99174b50cb1de GIT binary patch literal 128123 zcmV)UK(N1wP){yBVK`Tq6$aDQLCzrK(6_n+^}|BwIY|M-vp{r45u zuV23+uD^c$bzR@bU)TB*>(3{L%&)Ke>nnf%{{H9tz`EwU?H*uHx{r{Ci zqRRT@`&~tRO8oS6R3*Ouf3N@FtGB|Uf3aa+<%C0m2d{t||@~`{9|Hps)RvZ8NZ*1@Cf$tl?%ij0yvRLjH zyuH^a1C9>tY`Gs*Z=t6|M3r&`>R^b_I_9XyUFX`Pe1Jv@AahWseQ`W zy}E`J>b!dCGd6DQ5&gOUYt{bq?{|Cu@%#7n>v+E3qdL|IU*)cLt#{#G<<|I2_GFdi z<4XP9f9};)tHEFS>r?J-FGuoj?^W$xIY9mVxd*GgpFD3o`*xnMhr#CFyS1M@;vfIp zKfZoKX+b?Z?zKh_N78zt-y^ZkKN4sCa;?#}uI%&c*Z2QX z_df2@amg(3JTK4Z*C+kO^QbH9{95IHAAfy{+rNIF?{Ys^zVEtLZtFZQ=EFL#d=Qme zt~99QDrMC_s}+8}>s)mJs`RWL_+_dRp7D z`-HuET?qDb_v+PGqMqPt-FP}#*XL7?k|wxe5f^`g#GQ{!g*D-}#WKvh}5vo>?Z$whm*TGY#vr>d!P_S*4kGx#QO@3zFA^Mhk*A zC_JCOi|2P)uDpVoW*zTJ8TIohca`Y1+POisjidC+hgY(D+jEPZKYOYAYB!X5259zL zDd=A@OHT@6K9^G)aTMkIC@Q6$8Yn*WXsbc?6QA}%M%CW#leRh7!|EBzT-5a?BQcU2dj~0#@*3-B%;IbPWC+P72zMnNtFqjoQ z&*R+3)#p0;*BW~z_0IGd(n_MYG|m1UQPkdbu8P*1laO5ASD>u<>&{_H=N8*>SFyaF z%CiJ#nC$-Kxo_ANzdN7O3R^ngWxxaA_1xDN3)=BkxlC_!A-8LYocFe-QF_-*sIqQL zv(7*0+c`ipF?Y?8tmh}4v~tL-n%sAnUiqva7MMo2v0JgPjDCo&Vda^fr}AHAdE50I zOH*oGMNjG2agRgq&){8XO$^t;W&ya4@da;3Yj^ zv;@6re5K4zr~RHbbI4cI-pQ2`efTiKA1V7yyhtLqfWZJ9wSi?N`$gC)k_mmB_TwP=p~|9s^d zA!&vkbN>B88e!yVcViQYS*dZ@w(IpyxE(!?ahzBFzmP8}#Ufk(A&n2xN=ZDJ>5*H$ z)!~|DRehHJ82PYWlOx%0A=ZgvG7L7tgLSTx2$g^JxnM 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�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%0AtuiIlX=vhev<1AI8y6 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 tags. + */ + usageNotes?: ReactNode; +} + +export const COMPONENT_PAGES: Record = { + aurora: { + Island: AuroraIsland, + usageSnippet: ` + +`, + usageNotes: ( + <> + The aurora fills its scene; compose with the container. For a curtain band hanging in a wide + dark sky, place a short ShaderScene near the top of a taller dark section.{' '} + coverage reveals the curtain from the bottom up โ€” 1 covers the canvas, 0 hides + it. + + ), + }, + blobs: { + Island: BlobsIsland, + copySiblings: [ + "", + ], + usageSnippet: ` + + +`, + usageNotes: ( + <> + The space between blobs is transparent โ€” stack Blobs over a gradient or any other layer and + it shows through. + + ), + }, + 'conic-gradient': { + Island: ConicGradientIsland, + usageSnippet: ` + +`, + }, + 'cursor-ripple': { + Island: CursorRippleIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> + Cursor Ripple is a cursor effect: stack it after any components inside a{' '} + <ShaderScene> and dragging the pointer across the canvas leaves a wake + that spreads and settles. Its refraction bends the image the Sources beneath it drew, and + not the output of an Effect mounted before it. Mode 1 only: it needs the scene's output + stage. + + ), + }, + 'cursor-smudge': { + Island: CursorSmudgeIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Move the pointer over the canvas without pressing. Higher decay values + recover faster. A still pointer adds no distortion, and the scene can idle after recovery + when its other components are still. Set strength to 0 to disable the effect. +

+

+ Set decay to 0 to keep the distortion after the pointer stops. New strokes + keep changing it. Raise decay to resume recovery, or set strength to 0 to clear it. + Resizing or remounting the scene also clears the distortion. An animated Source keeps + moving beneath the held distortion. +

+

+ Place Cursor Smudge after a Source inside <ShaderScene>. It bends the + image drawn by Sources; other Effects keep their own contributions unbent. Sampling past + the image extends its edge pixels. Cursor Smudge requires Mode 1. +

+

+ Enable scrollPush on the scene to smudge while a finger scrolls across it, + including the fling after release. The page keeps scrolling. Reduced motion slows recovery + to 0.3ร— speed; the explicit paused motion policy clears the effect. An + ordinary scene pause preserves the smudge until the scene resumes. +

+ + ), + }, + 'cursor-spotlight': { + Island: CursorSpotlightIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> + Cursor Spotlight is a cursor effect: stack it after any components inside a{' '} + <ShaderScene> and the area around the pointer brightens over whatever + they drew. It stays invisible until the pointer first moves over the canvas, fades out when + the pointer leaves, and lets the scene idle while the pointer is still. Mode 1 only: it + needs the scene's output stage. + + ), + }, + dissolve: { + Island: DissolveIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Dissolve is a post-process layer: stack it after any components inside a{' '} + <ShaderScene> and drive progress from 0 to 1 to show them + block by block. Hidden blocks are transparent, so the page shows through. +

+

+ It reads the alpha beneath it. Stacked after a feathered Radial Wipe, at{' '} + progress 1, it turns the wipe's soft edge into a ragged one, with the + wipe's feather setting how wide the ragged band is and{' '} + pixelSize its grain. A soft-edged source such as Blobs gets the same + treatment, which is the point of stacking a Dissolve over it. +

+ + ), + }, + dither: { + Island: DitherIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Dither is a post-process layer: stack it after any components inside a{' '} + <ShaderScene> and it pixelates the composed scene into chunky cells, + posterizing each channel to a few levels โ€” the scene keeps its own colors. +

+

+ pattern picks the threshold map that decides how in-between colors resolve: + the Bayer matrices trade smoothness (8x8) for crunch (2x2). pixelSize sets + the cell size in CSS pixels, and levels is how many steps each color channel + is allowed โ€” 2 is the harshest look, 6 and up reads as subtle + banding. spread dials the pattern's strength from clean posterize bands + (0) to gritty overshoot (2), and threshold gates + the effect by brightness โ€” slide it down to release the highlights until the effect is + gone. +

+ + ), + }, + 'dot-field': { + Island: DotFieldIsland, + usageSnippet: ` + +`, + }, + 'fluted-glass': { + Island: FlutedGlassIsland, + copySiblings: [''], + usageSnippet: ` + + + +`, + usageNotes: ( + <> + FlutedGlass bends only what Sources drew, never the output of an Effect such as Grain, so + mount Grain after the glass, where the order you write matches what you see. Mode 1 only: it + needs the scene's output stage. In your own r3f <Canvas>, the{' '} + fluteSurface primitive gives you the same flute math. + + ), + }, + 'fractal-noise': { + Island: FractalNoiseIsland, + usageSnippet: ` + +`, + }, + 'god-rays': { + Island: GodRaysIsland, + usageSnippet: ` + +`, + usageNotes: ( + <> + Light rays radiate from center โ€” park it off-canvas for a top-of-page sun. The + rays emit over a transparent background; stack them above a dark layer. + + ), + }, + grain: { + Island: GrainIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Grain stacks inside any <ShaderScene> on top of whatever base component + you want โ€” gradients, noise fields, mesh gradients โ€” and applies a layer of animated grain + via the post-processing pipeline. +

+

+ Additive (default) adds signed grain so half the pixels brighten and half + darken, preserving average exposure โ€” pure texture, no exposure shift.{' '} + Subtractive takes the absolute value of the grain and subtracts it, so + the image only darkens. Subtractive simulates silver-halide film stock physics, where + exposed grain blocks light. +

+

+ speed controls the shutter cadence: 1 โ‰ˆ 60Hz (continuous shimmer + at 60fps), 0.4 โ‰ˆ 24Hz (chunky film cadence), 0 freezes the grain + pattern. +

+ + ), + }, + 'led-wall': { + Island: LedWallIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> + LED Wall is a post-process layer: stack it after any components inside a{' '} + <ShaderScene> and it screens the composed scene into a grid of square + dots, each lit with the scene color at its cell. The gaps are transparent unless{' '} + bleed lets the scene through. + + ), + }, + 'linear-gradient': { + Island: LinearGradientIsland, + usageSnippet: ` + +`, + }, + 'liquid-gradient': { + Island: LiquidGradientIsland, + usageSnippet: ` + +`, + }, + 'mesh-gradient': { + Island: MeshGradientIsland, + usageSnippet: ` + +`, + }, + 'radial-gradient': { + Island: RadialGradientIsland, + usageSnippet: ` + +`, + }, + 'radial-wipe': { + Island: RadialWipeIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> + Radial Wipe is a post-process layer: stack it after any components inside a{' '} + <ShaderScene> and drive progress from 0 to 1 to reveal them + from center. feather softens the front. Hidden areas are + transparent, so the page shows through. Stack a Dissolve after it to grain the soft edge. + + ), + }, + 'simplex-noise': { + Island: SimplexNoiseIsland, + usageSnippet: ` + +`, + }, + vignette: { + Island: VignetteIsland, + copySiblings: [''], + usageSnippet: ` + + +`, + usageNotes: ( + <> +

+ Vignette stacks inside any <ShaderScene> on top of whatever base + component you want and fades the upstream pixels toward an edge color along a soft radial + ring. Unlike <Grain />, which generates new noise from uv, + Vignette reads the upstream pixel and mixes it toward color โ€” the{' '} + {`"read-upstream"`} half of the post-processing pipeline. +

+

+ feather controls how gradual the blend is. At 0 the ring is a + hard cutoff; at 1 the entire canvas is in the blend (a smooth radial gradient + from center to edge). radius is the outer edge of the ring;{' '} + center is the bright spot in normalized UV space. +

+ + ), + }, + voronoi: { + Island: VoronoiIsland, + usageSnippet: ` + +`, + }, + 'wave-lines': { + Island: WaveLinesIsland, + // The legacy page showed a live snippet mirroring the control panel; see + // the note in wave-lines/demo.tsx for why it is static here for now. + usageSnippet: ` + +`, + }, +}; diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/demo.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/demo.tsx new file mode 100644 index 0000000..b6eea96 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/demo.tsx @@ -0,0 +1,90 @@ +'use client'; + +/** + * Fluted Glass demo island: the interactive slice of the Fluted Glass page, + * holding the control store, the shader preview, and the control panel. The + * shared components/[slug] template renders this between its static header + * and prose sections. + */ +import dynamic from 'next/dynamic'; +import { useMemo } from 'react'; + +import { + ColorInput, + ControlPanel, + ControlsProvider, + createControlStore, + DemoLayout, + Section, + SliderInput, + useSnapshot, +} from '@/components/controls'; +import { DemoPoster } from '@/components/DemoPoster'; +import { VisualTestPause } from '@/lib/visualTestHooks'; + +import { type FlutedGlassParams, INITIAL } from './params'; + +const FlutedGlassScene = dynamic(() => import('./scene'), { ssr: false }); + +/** + * The CSS size the poster was captured at in build-posters.sh, the widest + * demo box. The flutes are sized in CSS pixels, so DemoPoster's pixel-locked + * mode shows the image at this size and lets the box crop it. Cover-scaling + * would change the flutes' spacing, and the grooves would jump when the live + * scene replaced the poster. + */ +const POSTER_SIZE: readonly [number, number] = [896, 598]; + +/** + * Reads the live params and renders the scene. Split out from the island so + * it subscribes to the store on its own: the island never re-renders during + * a drag, only this and the moved control do. + */ +function FlutedGlassDemo() { + const params = useSnapshot(); + + return ( + + + + + + ); +} + +function FlutedGlassControls() { + return ( + +
+ + + + +
+
+ + + + +
+
+ ); +} + +export function FlutedGlassIsland() { + const store = useMemo(() => createControlStore(INITIAL), []); + + return ( + + }> +
+ +
+
+
+ ); +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/params.ts b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/params.ts new file mode 100644 index 0000000..5fe1544 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/params.ts @@ -0,0 +1,21 @@ +export interface FlutedGlassParams { + fluteWidth: number; + angle: number; + refraction: number; + crossSection: number; + lightAngle: number; + highlight: number; + highlightColor: string; + shadow: number; +} + +export const INITIAL: FlutedGlassParams = { + fluteWidth: 40, + angle: 90, + refraction: 1, + crossSection: 0.5, + lightAngle: 135, + highlight: 0.45, + highlightColor: 'oklch(1 0 0)', + shadow: 0.2, +}; diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/scene.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/scene.tsx new file mode 100644 index 0000000..740dfd9 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/components/fluted-glass/scene.tsx @@ -0,0 +1,32 @@ +'use client'; + +import type { ReactNode } from 'react'; + +import { FlutedGlass, MeshGradient, ShaderScene } from '@camp-dev/shaders'; + +import { type FlutedGlassParams, INITIAL } from './params'; + +export default function FlutedGlassScene({ + params = INITIAL, + children, +}: { + params?: FlutedGlassParams; + children?: ReactNode; +} = {}) { + return ( + + + + {children} + + ); +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-benchmark/benchmark-scene.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-benchmark/benchmark-scene.tsx new file mode 100644 index 0000000..d96d83b --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-benchmark/benchmark-scene.tsx @@ -0,0 +1,75 @@ +'use client'; + +// Benchmark for FlutedGlass with ShaderMonitor's GPU readout (SHA-274): +// MeshGradient under FlutedGlass in the Composable card's 485 by 308 +// CSS-pixel window, where the glass must add under 0.2 ms at 2x. +// `?glass=none` drops the glass, for the reading to subtract, and `?dpr=2` +// caps the canvas at two pixels per CSS pixel, anything else at one. +import { useState } from 'react'; + +import { + FlutedGlass, + MeshGradient, + ShaderMonitor, + ShaderScene, + useStaticSceneHint, +} from '@camp-dev/shaders'; + +const WIDTH = 485; +const HEIGHT = 308; + +interface Settings { + pixelRatio: number; + glass: boolean; +} + +const readSettings = (): Settings => { + const search = new URLSearchParams(window.location.search); + + return { + pixelRatio: search.get('dpr') === '2' ? 2 : 1, + glass: search.get('glass') !== 'none', + }; +}; + +/** + * Votes the scene animated, so a still scene keeps drawing frames and the GPU + * readout averages many frames instead of settling on one. + */ +function KeepDrawing() { + useStaticSceneHint(false); + + return null; +} + +// Time each reading with no other scene drawing, since a second scene +// inflates it (the skewed-readout gotcha in docs/agents/tsl.md). The links +// are full page loads, so each measurement starts from a fresh renderer. +export default function BenchmarkScene() { + const [{ pixelRatio, glass }] = useState(readSettings); + /** A link to this page with some settings changed. */ + const href = (changes: Partial) => { + const next = { pixelRatio, glass, ...changes }; + + return `?dpr=${next.pixelRatio}${next.glass ? '' : '&glass=none'}`; + }; + + return ( +
+

+ MeshGradient {glass ? 'under FlutedGlass' : 'alone'} at {WIDTH} ร— {HEIGHT} CSS px, pixel + ratio capped at {pixelRatio}x. 1x ยท{' '} + 2x ยท glass ยท{' '} + no glass +

+
+ + + {glass && } + + + +
+
+ ); +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-benchmark/page.dev.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-benchmark/page.dev.tsx new file mode 100644 index 0000000..eb5892e --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-benchmark/page.dev.tsx @@ -0,0 +1,12 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +// three/webgpu references `self` at module load and cannot SSR, so the +// benchmark scene is loaded client-only. Dev route: invisible to production +// builds. +const BenchmarkScene = dynamic(() => import('./benchmark-scene'), { ssr: false }); + +export default function FlutedGlassBenchmarkPage() { + return ; +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-probe/page.dev.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-probe/page.dev.tsx new file mode 100644 index 0000000..f3a0cee --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-probe/page.dev.tsx @@ -0,0 +1,11 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +// three/webgpu references `self` at module load and cannot SSR, so the probe +// scene is loaded client-only. Dev route: invisible to production builds. +const ProbeScene = dynamic(() => import('./probe-scene'), { ssr: false }); + +export default function FlutedGlassProbePage() { + return ; +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-probe/probe-scene.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-probe/probe-scene.tsx new file mode 100644 index 0000000..9ad5a03 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/apps/docs/src/app/dev/fluted-glass-probe/probe-scene.tsx @@ -0,0 +1,76 @@ +'use client'; + +// Probe for FlutedGlass: hard diagonal stripes seen through the glass at its +// defaults. A smooth Source hides what the glass does, but stripes show it: +// each flute holds a squeezed copy of the stripes under it, and the stripes +// jump sideways at every groove. The visual spec's pixel checks load it. +import { type CrossSectionName, FlutedGlass, LinearGradient, ShaderScene } from '@camp-dev/shaders'; + +import { VisualTestPause } from '@/lib/visualTestHooks'; + +// Each pass of the gradient is half dark and half light, with a switch at +// its middle a fraction of a pixel wide. colorRamp skips a segment of zero +// width, so the two middle stops sit 0.002 of a pass apart rather than on +// one position. +const STRIPE_STOPS = [ + { color: 'oklch(0.2 0 0)', position: 0 }, + { color: 'oklch(0.2 0 0)', position: 0.5 }, + { color: 'oklch(0.95 0 0)', position: 0.502 }, + { color: 'oklch(0.95 0 0)', position: 1 }, +]; + +// Query params override the glass's dials, such as ?refraction=0&angle=30 or +// ?crossSection=prism, for checking one by eye. ?highlight=0&shadow=0 turns +// the light off, to see the bend alone. ?glass=none drops the glass, to +// compare against the Source alone, and ?source=none drops the stripes, +// leaving the glass over a transparent scene. + +/** A value from the page's query string, or null when it is absent. */ +function query(name: string): string | null { + return new URLSearchParams(window.location.search).get(name); +} + +/** A number from the page's query string, or undefined when it is absent. */ +function queryNumber(name: string): number | undefined { + const raw = query(name); + + return raw === null ? undefined : Number(raw); +} + +/** Narrows a query string value to one of the cross-section's names. */ +function isCrossSectionName(value: string): value is CrossSectionName { + return value === 'prism' || value === 'round' || value === 'flat'; +} + +/** The cross-section from the query string, as a name or a number. */ +function queryCrossSection(): CrossSectionName | number | undefined { + const raw = query('crossSection'); + + if (raw === null) return undefined; + + return isCrossSectionName(raw) ? raw : Number(raw); +} + +export default function ProbeScene() { + return ( +
+ + {query('source') !== 'none' && ( + + )} + {query('glass') !== 'none' && ( + + )} + + +
+ ); +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/docs/adr/0004-effects-resample-by-wrapping-the-sampler.md b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/docs/adr/0004-effects-resample-by-wrapping-the-sampler.md new file mode 100644 index 0000000..d58e130 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/docs/adr/0004-effects-resample-by-wrapping-the-sampler.md @@ -0,0 +1,16 @@ +# Effects resample the image beneath them by wrapping a sampler, not through render targets + +Status: Accepted. The output-stage change lands in its own sub-issue of SHA-266, before FlutedGlass's color split and blur. + +FlutedGlass needs several samples of the image beneath it: three for its color split and twelve for its blur. The output stage gave an Effect one sample. `createOutputStage` passed the screen coordinate through every registered UV warp in turn and read the scene texture once, at the coordinate that came out. We chose to hand each Effect a function that samples everything beneath it, and the Effect returns a new function in its place. The output stage calls the last function once per pixel, at the screen coordinate. The glossary calls the Effect's rule a resample. A UV warp is the resample that calls the function beneath it once, at a moved coordinate, so Dither, CursorRipple, and CursorSmudge keep their code. + +## Considered options + +- **A render target per Effect.** Each Effect draws into its own texture, and the next Effect reads it. An Effect could then bend the color transforms mounted before it, such as Grain, which lifts the limit ADR 0002 accepted. The cost is a rewrite of the output stage, plus one full-screen pass and one canvas-sized texture per Effect. It stays the later option ADR 0002 names. +- **FlutedGlass reads the scene texture itself.** Rejected. That read skips every UV warp beneath it, so a CursorRipple under the glass would vanish. + +## Consequences + +- The stacking order of UV warps flips, and the new order is the right one. The old chain moved the screen coordinate through the first-mounted warp first, so that warp acted as the topmost layer, and `` pixelated the water along with the image beneath it. With wrapped samplers, the last-mounted Effect is the outermost function, so its warp moves the screen coordinate first. No docs page stacks two UV warps, so no visual baseline changes. A unit test pins the order. +- Samples multiply. Each sample an Effect takes reruns every warp beneath it. Twelve blur samples times three color-split samples is 36 scene reads per pixel, and a CursorRipple beneath the glass reads its height field four times for each of them. Time a deep stack on WebGPU before you raise a sample count. +- A resample still reads only what Sources drew. Color transforms such as Grain and Vignette run after every resample, so glass mounted after Grain leaves the grain unbent. ADR 0002's limit stands. diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/fluted-glass.test.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/fluted-glass.test.tsx new file mode 100644 index 0000000..4e4d2cd --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/fluted-glass.test.tsx @@ -0,0 +1,274 @@ +import { type ReactNode, StrictMode } from 'react'; + +import { render } from '@testing-library/react'; +import type { WebGPURenderer } from 'three/webgpu'; +import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'; + +import { type CrossSectionName, FlutedGlass, type FlutedGlassProps } from '../../index.js'; +import { + type PostProcessTransform, + ShaderContext, + type ShaderContextValue, + type UvTransform, +} from '../../react/context/shader-context.js'; +import type { AnimatableProp } from '../../react/hooks/animatable-signal/animatable-signal.js'; +import { createSignal } from '../../react/internal/create-signal.js'; +import type { SchedulerClient } from '../../runtime/frame-scheduler/frame-scheduler.js'; + +// What the Effect asks of the scene, never what the glass looks like. The +// look is the demo page and its visual spec. A fake context counts what the +// glass registers on the output stage and how it votes. +beforeEach(() => { + vi.stubGlobal('requestAnimationFrame', () => 0); + vi.stubGlobal('cancelAnimationFrame', () => {}); +}); +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +function makeScene() { + const clients: SchedulerClient[] = []; + const uvTransforms: UvTransform[] = []; + const overlays: PostProcessTransform[] = []; + const unregisters = { uv: 0, overlay: 0 }; + const renderer = { + domElement: document.createElement('canvas'), + getPixelRatio: () => 2, + } as unknown as WebGPURenderer; + const scheduler = { + idle: true, + add: (client: SchedulerClient) => clients.push(client), + remove: (client: SchedulerClient) => { + const index = clients.indexOf(client); + + if (index >= 0) clients.splice(index, 1); + }, + requestRender: vi.fn(() => true), + setIdle: vi.fn(() => () => undefined), + }; + const shaderContext = { + renderer: { three: renderer }, + scene: {}, + camera: {}, + scheduler, + registerOverlay: (transform: PostProcessTransform) => { + overlays.push(transform); + + return () => { + unregisters.overlay += 1; + }; + }, + registerBaseUvTransform: (transform: UvTransform) => { + uvTransforms.push(transform); + + return () => { + unregisters.uv += 1; + }; + }, + } as unknown as ShaderContextValue; + + function Wrapper({ children }: { children: ReactNode }) { + return {children}; + } + + return { Wrapper, scheduler, uvTransforms, overlays, unregisters }; +} + +/** One UV warp and one color transform, neither removed or registered again. */ +function expectOneOfEach(scene: ReturnType) { + expect(scene.uvTransforms).toHaveLength(1); + expect(scene.overlays).toHaveLength(1); + expect(scene.unregisters).toEqual({ uv: 0, overlay: 0 }); +} + +/** A hand-driven animation signal, the shape a MotionValue has. */ +function makeSignal(initial: number) { + let value = initial; + const channel = createSignal(() => value); + + return { + signal: channel.signal, + set(next: number) { + value = next; + for (const listener of channel.listeners) listener(next); + }, + }; +} + +describe('FlutedGlass', () => { + // The public shape: seven optional dials, each a number or an animation + // signal, where the cross-section also takes a name for a point on its + // dial, plus a highlight color. pnpm typecheck runs this file too, so a + // missing export or a narrowed prop fails there as well as here. + it('takes seven animatable dials and a highlight color from the package root', () => { + expectTypeOf().toEqualTypeOf<'prism' | 'round' | 'flat'>(); + expectTypeOf().toEqualTypeOf<{ + fluteWidth?: AnimatableProp; + angle?: AnimatableProp; + refraction?: AnimatableProp; + crossSection?: AnimatableProp | CrossSectionName; + lightAngle?: AnimatableProp; + highlight?: AnimatableProp; + highlightColor?: string; + shadow?: AnimatableProp; + }>(); + }); + + // The UV warp bends the image, and the color transform lights the glass. + it('registers one UV warp and one color transform, and removes both on unmount', () => { + const scene = makeScene(); + const { unmount } = render(, { wrapper: scene.Wrapper }); + + expectOneOfEach(scene); + + unmount(); + expect(scene.unregisters).toEqual({ uv: 1, overlay: 1 }); + }); + + it('survives Strict Mode with one UV warp and one color transform', () => { + const scene = makeScene(); + + render( + + + , + { wrapper: scene.Wrapper }, + ); + + expect(scene.uvTransforms.length - scene.unregisters.uv).toBe(1); + expect(scene.overlays.length - scene.unregisters.overlay).toBe(1); + }); + + // Every dial rides a stable uniform, so a new value is a uniform write + // and a repaint, never a recompile of the output chain. + it('changes its dials without re-registering either pass', () => { + const scene = makeScene(); + const { rerender } = render( + , + { wrapper: scene.Wrapper }, + ); + + scene.scheduler.requestRender.mockClear(); + rerender( + , + ); + + expectOneOfEach(scene); + expect(scene.scheduler.requestRender).toHaveBeenCalled(); + }); + + it('follows its dial signals without re-registering either pass', () => { + const scene = makeScene(); + const fluteWidth = makeSignal(40); + const angle = makeSignal(90); + const refraction = makeSignal(1); + const crossSection = makeSignal(0.5); + const lightAngle = makeSignal(135); + const highlight = makeSignal(0.5); + const shadow = makeSignal(0.3); + + render( + , + { wrapper: scene.Wrapper }, + ); + scene.scheduler.requestRender.mockClear(); + + fluteWidth.set(24); + expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(1); + angle.set(30); + expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(2); + refraction.set(0); + expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(3); + crossSection.set(0.2); + expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(4); + lightAngle.set(45); + expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(5); + highlight.set(1); + expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(6); + shadow.set(0); + expect(scene.scheduler.requestRender).toHaveBeenCalledTimes(7); + + expectOneOfEach(scene); + }); + + // The color decodes into a uniform, so a new color repaints the glass + // without rebuilding the color transform. + it('changes its highlight color without re-registering either pass', () => { + const scene = makeScene(); + const { rerender } = render(, { + wrapper: scene.Wrapper, + }); + + for (const highlightColor of ['#ffd27f', 'oklch(0.8 0.1 250)']) { + scene.scheduler.requestRender.mockClear(); + rerender(); + expect(scene.scheduler.requestRender).toHaveBeenCalled(); + } + + expectOneOfEach(scene); + }); + + // A name is a point on the cross-section dial, not a formula of its own, + // so switching names writes the same uniform a number would. + it('switches cross-section names without re-registering either pass', () => { + const scene = makeScene(); + const { rerender } = render(, { + wrapper: scene.Wrapper, + }); + + for (const name of ['round', 'flat', 'prism'] as const) { + scene.scheduler.requestRender.mockClear(); + rerender(); + expect(scene.scheduler.requestRender).toHaveBeenCalled(); + } + + expectOneOfEach(scene); + }); + + // Still glass draws nothing that changes over time, so a scene of still + // Sources under it can park. + it('votes idle and never animated', () => { + const scene = makeScene(); + + render(, { wrapper: scene.Wrapper }); + + expect(scene.scheduler.setIdle).toHaveBeenCalledWith(true); + expect(scene.scheduler.setIdle).not.toHaveBeenCalledWith(false); + }); + + // Mode 1 only: outside a scene there is no output stage to bend, so the + // glass follows its signals without a scheduler to wake and listens to + // nothing on the page. + it('does nothing outside a ShaderScene', () => { + const addListener = vi.spyOn(window, 'addEventListener'); + const refraction = makeSignal(1); + + render(); + + expect(() => refraction.set(0.5)).not.toThrow(); + expect(addListener).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/fluted-glass.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/fluted-glass.tsx new file mode 100644 index 0000000..d4c4fec --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/fluted-glass.tsx @@ -0,0 +1,120 @@ +'use client'; + +// Public face of the fluted glass: owns the props, their JSDoc, and their +// defaults, then delegates to FlutedGlassShader (./shader.tsx). FlutedGlass +// is an Effect: stack it after a Source inside a and it shows +// that Source through a pane of fluted glass, each flute bending the image +// beneath it into a squeezed copy and catching the light along one wall. +import type { AnimatableProp } from '../../react/hooks/animatable-signal/animatable-signal.js'; +import { useStaticSceneHint } from '../../react/hooks/use-static-hint/use-static-hint.js'; +import { FlutedGlassShader } from './shader.js'; + +/** The names for three points on the `crossSection` dial. */ +export type CrossSectionName = 'prism' | 'round' | 'flat'; + +// Where each name sits on the dial. A name is only a point on the dial, so +// it rides the same uniform as a number and animates like one. +const CROSS_SECTION_DIAL: Record = { + prism: 0, + round: 0.5, + flat: 1, +}; + +export interface FlutedGlassProps { + /** + * The width of one flute, in CSS pixels. Defaults to 40. Accepts a static + * value or an animation signal. + */ + fluteWidth?: AnimatableProp; + /** + * The direction the flutes run, in degrees on screen: 0 runs them left to + * right and 90 runs them bottom to top. These are the degrees + * LinearGradient's `angle` uses, though a gradient's bands run across its + * angle and the flutes run along theirs. Defaults to 90, vertical flutes. + * Accepts a static value or an animation signal. + */ + angle?: AnimatableProp; + /** + * How far each flute bends the image beneath it, in half flute widths at + * the steepest part of the glass. 0 leaves the image unchanged. At 1, each + * flute shows a squeezed copy of twice its width of image. Defaults to 1. + * Accepts a static value or an animation signal. + */ + refraction?: AnimatableProp; + /** + * The shape of each flute seen from its end, from 0 to 1. `'prism'` (0) is + * two flat faces meeting at a ridge, `'round'` (0.5) is a lens, and + * `'flat'` (1) is a flat top with steep walls. Numbers between give the + * shapes between, so a signal can morph one shape into another. Defaults + * to `'round'`. Accepts a static value or an animation signal. + */ + crossSection?: AnimatableProp | CrossSectionName; + /** + * Where the light comes from, in degrees counterclockwise on screen, the + * way `angle` turns: 0 from the right, 90 from above, 180 from the left, + * and 270 from below. + * The light stays put on screen while `angle` turns the glass beneath it, + * and the flute wall that faces it catches the highlight. A light that + * runs along the flutes catches neither wall. Defaults to 135, the upper + * left. Accepts a static value or an animation signal. + */ + lightAngle?: AnimatableProp; + /** + * How bright the highlight along each lit wall is, from 0 to 1, where 1 + * adds the full `highlightColor` at the brightest point. 0 turns it off. + * Defaults to 0.45. Accepts a static value or an animation signal. + */ + highlight?: AnimatableProp; + /** + * The highlight's color: hex, `oklch()`, or `oklab()`. Defaults to white, + * `'oklch(1 0 0)'`. + */ + highlightColor?: string; + /** + * How dark the glass gets at each groove, from 0 to 1, where 1 is black + * right on the groove. The shadow falls where the glass turns steep, so a + * `'flat'` cross-section darkens only along its walls. 0 turns it off. + * Defaults to 0.2. Accepts a static value or an animation signal. + */ + shadow?: AnimatableProp; +} + +/** + * Shows the image beneath it through repeating flutes of glass, lit from one + * side with a shadow at each groove. Mount it inside a `` after + * the Sources it should bend. It bends only what Sources drew, never the + * output of an Effect such as Grain, so mount such Effects after it, where + * the order you write matches what you see. Mode 1 only, because it needs + * the scene's output stage; Mode 2 users get the flute math as the + * `fluteSurface` primitive. + */ +export function FlutedGlass({ + fluteWidth = 40, + angle = 90, + refraction = 1, + crossSection = 'round', + lightAngle = 135, + highlight = 0.45, + highlightColor = 'oklch(1 0 0)', + shadow = 0.2, +}: FlutedGlassProps) { + // The idle vote. Still glass draws nothing that changes over time, and + // every dial repaints through its uniform when it moves, so the glass + // votes idle and a scene of still Sources under the glass parks. + useStaticSceneHint(true); + + return ( + + ); +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/shader.tsx b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/shader.tsx new file mode 100644 index 0000000..6124dce --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/fluted-glass/shader.tsx @@ -0,0 +1,326 @@ +'use client'; + +// The fluted glass's GPU half: a UV warp that bends the image beneath the +// glass, and a color transform that lights it, both reading one surface from +// the engine's fluteSurface primitive. This file converts between the pass's +// coordinate and the primitive's pixels. The wrapper (./fluted-glass.tsx) +// supplies the props. +import { useEffect, useMemo } from 'react'; + +import type { ShaderNodeObject } from 'three/tsl'; +import { + abs, + cos, + dot, + fract, + max, + normalize, + pow, + screenSize, + sin, + smoothstep, + uniform, + uv, + vec2, + vec3, + vec4, +} from 'three/tsl'; +import { type Node, Vector3 } from 'three/webgpu'; + +import { fluteSurface } from '../../engine.js'; +import type { AnimatableProp } from '../../react/hooks/animatable-signal/animatable-signal.js'; +import { useAnimatableUniform } from '../../react/hooks/use-animatable-uniform/use-animatable-uniform.js'; +import { useBasePassUv } from '../../react/hooks/use-base-pass-uv/use-base-pass-uv.js'; +import { usePostProcessPass } from '../../react/hooks/use-overlay-pass/use-overlay-pass.js'; +import { useResize } from '../../react/hooks/use-resize/use-resize.js'; +import { useShaderContext } from '../../react/hooks/use-shader-context/use-shader-context.js'; +import { parseColor } from '../shared/color.js'; + +export interface FlutedGlassShaderProps { + /** + * The width of one flute, in CSS pixels. Accepts a static value or an + * animation signal. + */ + fluteWidth: AnimatableProp; + /** + * The direction the flutes run, in degrees on screen: 0 runs them left to + * right and 90 runs them bottom to top. Accepts a static value or an + * animation signal. + */ + angle: AnimatableProp; + /** + * How far each flute bends the image beneath it, in half flute widths at + * the steepest part of the glass. 0 leaves the image unchanged. Accepts a + * static value or an animation signal. + */ + refraction: AnimatableProp; + /** + * The shape of each flute seen from its end, from 0 to 1: 0 is a prism, + * 0.5 is round, and 1 is a flat top with steep walls. The wrapper turns a + * name into its point on this dial. Accepts a static value or an animation + * signal. + */ + crossSection: AnimatableProp; + /** + * Where the light comes from, in degrees counterclockwise on screen, the + * way `angle` turns: 0 from the right, 90 from above, 180 from the left, + * and 270 from below. Accepts a static value or an animation signal. + */ + lightAngle: AnimatableProp; + /** + * How bright the highlight is, from 0 to 1, where 1 adds the full + * `highlightColor` at the brightest point. Accepts a static value or an + * animation signal. + */ + highlight: AnimatableProp; + /** The highlight's color: hex, `oklch()`, or `oklab()`. */ + highlightColor: string; + /** + * How dark the glass gets at each groove, from 0 to 1, where 1 is black + * right on the groove. Accepts a static value or an animation signal. + */ + shadow: AnimatableProp; +} + +// --------------------------------------------- +// Feel constants (tuned by eye at the build's visual gates) +// --------------------------------------------- +// How high the light sits above the glass, in degrees: 0 skims across it +// and 90 shines straight down. A lower light catches walls that lean +// further over, so it moves the highlight out toward the grooves. +const LIGHT_HEIGHT = 30; +const LIGHT_HEIGHT_RADIANS = (LIGHT_HEIGHT * Math.PI) / 180; +// The specular exponent. Higher draws a thinner, harder highlight, and +// lower spreads it into a soft sheen across the wall. +const HIGHLIGHT_SHARPNESS = 32; +// How far the shadow can reach in from each groove, as a share of the way to +// the flute's center. Within that reach it falls only where the glass is +// steep. 1 lets it reach the center. +const SHADOW_WIDTH = 0.5; + +// The pass's frame has y down and the primitive's has y up, so a vector +// that crosses between them flips its y. +function flipY(vector: ShaderNodeObject): ShaderNodeObject { + return vector.mul(vec2(1, -1)); +} + +// --------------------------------------------- +// Mirroring at the canvas edge +// --------------------------------------------- +// Near the canvas edge the bend can point off the canvas, where the scene +// texture has nothing. Left alone, the sampler, the GPU unit that reads a +// texture, clamps to the edge texel, the texture's outermost pixel, and +// repeats it, which draws a smeared strip. This folds the coordinate back +// instead, as if the image were mirrored past each edge. +// +// fract(x / 2) * 2 counts x along a 0-to-2 cycle, and subtracting 1 makes it +// run -1 to 1. Its absolute value then falls from 1 to 0 and climbs back, +// and 1 minus that rises from 0 to 1 and falls back: a triangle wave. On +// 0..1 it returns x unchanged, past 1 it walks back down, so 1.1 reads 0.9, +// and below 0 it walks back up, so -0.1 reads 0.1. It works per component. +function mirrorOntoCanvas(coordinate: ShaderNodeObject): ShaderNodeObject { + return abs(fract(coordinate.div(2)).mul(2).sub(1)).oneMinus(); +} + +export function FlutedGlassShader({ + fluteWidth, + angle, + refraction, + crossSection, + lightAngle, + highlight, + highlightColor, + shadow, +}: FlutedGlassShaderProps) { + const shaderContext = useShaderContext(); + + // The dials live in uniforms, values the CPU writes and the GPU reads at + // each draw, so changing them never rebuilds the output chain. + const fluteWidthUniform = useAnimatableUniform(fluteWidth); + const angleUniform = useAnimatableUniform(angle); + const refractionUniform = useAnimatableUniform(refraction); + const crossSectionUniform = useAnimatableUniform(crossSection); + const lightAngleUniform = useAnimatableUniform(lightAngle); + const highlightUniform = useAnimatableUniform(highlight); + const shadowUniform = useAnimatableUniform(shadow); + + // The highlight color decodes once into linear rgb (parseColor reads hex + // and the wide-gamut oklch() and oklab() strings). The Vector3 and its + // uniform are created once, and the effect below writes each new color + // into the Vector3, so a new color never rebuilds the color transform. + const highlightColorVector = useMemo(() => new Vector3(1, 1, 1), []); + const highlightColorUniform = useMemo( + () => uniform(highlightColorVector), + [highlightColorVector], + ); + + useEffect(() => { + highlightColorVector.set(...parseColor(highlightColor)); + // The bare-uniform-write gotcha in docs/agents/tsl.md: a write into the + // Vector3 repaints nothing on a static scene by itself. + shaderContext?.scheduler.requestRender(); + }, [highlightColor, highlightColorVector, shaderContext]); + + // --------------------------------------------- + // CSS pixels -> device pixels + // --------------------------------------------- + // fluteWidth is in CSS pixels so a flute keeps its size on a 1x and a 2x + // display, but the warp below counts device pixels. The renderer's pixel + // ratio, which respects maxDPR clamping, converts between them; useResize + // fires again on monitor moves and browser zoom, when that ratio changes. + // Dither builds its ratio the same way. + const resize = useResize(); + // Created once and never replaced: the effect below writes into it, and + // the warp depends on the stable wrapper. + const pixelRatioUniform = useMemo( + () => uniform(resize.get()[2] || 1), + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + useEffect(() => { + const apply = () => { + const rendererRatio = shaderContext?.renderer.three.getPixelRatio(); + + pixelRatioUniform.value = + rendererRatio !== undefined && rendererRatio > 0 ? rendererRatio : resize.get()[2] || 1; + // A bare uniform write repaints nothing on a static scene, so ask for + // a frame (the bare-uniform-write gotcha in docs/agents/tsl.md). + shaderContext?.scheduler.requestRender(); + }; + + apply(); + + return resize.on('change', apply); + }, [resize, pixelRatioUniform, shaderContext]); + + // --------------------------------------------- + // The glass at a pixel + // --------------------------------------------- + // Both passes below start here, so the light lands where the image bends. + // `coordinate` is a pass coordinate, [0, 0] at the top-left with y growing + // downward. Into the primitive's frame: subtract 0.5 to measure from the + // canvas center, so the middle flute sits in the middle of the canvas and + // the flutes turn around it as `angle` changes. Times screenSize, the + // drawing buffer in device pixels, turns 0..1 into pixels, so a flute is as + // wide across a wide canvas as across a tall one. The y flip points y up, + // the frame the primitive's angle is measured in. + const surfaceAt = (coordinate: ShaderNodeObject) => + fluteSurface(flipY(coordinate.sub(0.5).mul(screenSize)), { + fluteWidth: fluteWidthUniform.mul(pixelRatioUniform), + angle: angleUniform, + refraction: refractionUniform, + crossSection: crossSectionUniform, + }); + + // --------------------------------------------- + // Refraction: bend the scene through the flutes + // --------------------------------------------- + // A base-pass UV warp: the callback receives the 0..1 coordinate the + // rendered scene is about to be sampled at and returns a replacement. + useBasePassUv( + (coordinate) => { + const { bend } = surfaceAt(coordinate); + + // Back out: the bend comes back in device pixels with y up. Flip y + // again and divide by the buffer size to get a 0..1 offset in the + // pass's frame, then fold anything off the canvas back onto it. + const bent = coordinate.add(flipY(bend).div(screenSize)); + + return mirrorOntoCanvas(bent); + }, + [fluteWidthUniform, angleUniform, refractionUniform, crossSectionUniform, pixelRatioUniform], + ); + + // --------------------------------------------- + // Lighting: a shadow at the grooves, a highlight on the walls + // --------------------------------------------- + // A post-process pass: the callback receives each pixel the scene drew, + // already bent by the warp above (`input`, rgba), and returns a + // replacement. The warp and this pass draw in the same output quad, and + // uv() is the pixel's own pass coordinate, so surfaceAt(uv()) is the glass + // at this pixel. The warp starts from uv() too while it runs first. A UV + // warp mounted before FlutedGlass, such as Dither's, hands the glass its + // moved coordinate instead, so there the bend and the light read the + // surface at slightly different points. SHA-267 reworks how warps stack. + usePostProcessPass( + (input) => { + const { slope, fromCenter, steepness } = surfaceAt(uv()); + + // The shadow. smoothstep() climbs from 0 to 1 over the outer + // SHADOW_WIDTH of the way from the center to the groove, so the shade + // is 0 across the middle of the flute and full right on the groove. + // Times the steepness, it falls only where the glass is steep: a flat + // top darkens on its walls and not across its flat middle, and a + // prism, steep from ridge to groove, keeps the whole band. Both read + // the glass without the groove smoothing, which pulls the slope to 0 + // on the groove's own pixel and would leave a bright line down the + // middle of the dark one. Multiplying darkens without moving alpha, + // and a transparent pixel, which holds no color here, stays + // transparent. + const shade = smoothstep(1 - SHADOW_WIDTH, 1, fromCenter) + .mul(steepness) + .mul(shadowUniform); + const shaded = input.rgb.mul(shade.oneMinus()); + + // The light works in the primitive's frame, y up, where the slope + // already is and where `angle` turns. Only directions meet in the math + // below, so the frame changes nothing but which way the angles run. + // + // The surface normal: the direction the glass faces, one unit long. + // Flat glass faces straight out of the screen, (0, 0, 1). Tilt it by + // the slope, and a wall's normal leans the way the wall faces. At a + // slope of 1, at a groove, it leans 45 degrees. normalize() scales the + // vector back to one unit long. + const normal = normalize(vec3(slope, 1)); + + // The direction toward the light, one unit long. lightAngle turns it + // counterclockwise around the screen, the way `angle` turns the + // flutes, and LIGHT_HEIGHT lifts it off the glass: cos(height) of it + // lies along the screen and sin(height) points out of it. The height + // is fixed, so its cosine and sine are worked out once here rather + // than per pixel. + const lightRadians = lightAngleUniform.mul(Math.PI / 180); + const toLight = vec3( + cos(lightRadians).mul(Math.cos(LIGHT_HEIGHT_RADIANS)), + sin(lightRadians).mul(Math.cos(LIGHT_HEIGHT_RADIANS)), + Math.sin(LIGHT_HEIGHT_RADIANS), + ); + + // Specular light, the Blinn-Phong way. A mirror sends light from the + // light to the eye where its normal points halfway between the two. + // The eye looks straight at the screen, from (0, 0, 1), so the + // halfway vector is the normalized sum of that and toLight. dot() + // of two unit vectors is the cosine of the angle between them: 1 + // where the normal points exactly halfway, and lower as it turns + // away. pow() raises that to HIGHLIGHT_SHARPNESS, which keeps values + // near 1 and crushes the rest, so the highlight narrows to a bright + // band on the part of each wall that leans toward the light by the + // right amount. Flat glass and walls that face away stay dark. max() + // keeps pow()'s base from going below 0. + const halfway = normalize(toLight.add(vec3(0, 0, 1))); + const specular = pow(max(dot(normal, halfway), 0), HIGHLIGHT_SHARPNESS); + + // Added light, scaled by the dial and by the pixel's alpha, as + // CursorRipple's shine is: a transparent pixel has nothing for light + // to land on. The color multiplies last, as an argument (the + // vec-uniform gotcha in docs/agents/tsl.md). Alpha passes through. + const light = specular.mul(highlightUniform).mul(input.a).mul(highlightColorUniform); + + return vec4(shaded.add(light), input.a); + }, + [ + fluteWidthUniform, + angleUniform, + refractionUniform, + crossSectionUniform, + pixelRatioUniform, + lightAngleUniform, + highlightUniform, + highlightColorUniform, + shadowUniform, + ], + ); + + return null; +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/index.ts b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/index.ts new file mode 100644 index 0000000..918e603 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/components/index.ts @@ -0,0 +1,82 @@ +// The Tier 1 components and the prop shapes they share. Each component folder +// holds a wrapper (props, uniforms, mesh lifecycle) and a shader.tsx (the TSL). +// Only the wrappers and their public types leave this barrel; the *Shader +// functions and DEFAULT_* constants are implementation detail. +export { Aurora } from './aurora/aurora.js'; +export type { AuroraProps } from './aurora/aurora.js'; + +export { Blobs } from './blobs/blobs.js'; +export type { BlobsProps } from './blobs/blobs.js'; + +export { ConicGradient } from './conic-gradient/conic-gradient.js'; +export type { ConicGradientProps } from './conic-gradient/conic-gradient.js'; + +export { CursorRipple } from './cursor-ripple/cursor-ripple.js'; +export type { CursorRippleProps } from './cursor-ripple/cursor-ripple.js'; +// The shader half is public so a caller can mount it over a wave field it +// drives itself. The docs' dev probe route does that with a hand-seeded +// field, for a deterministic capture. +export { CursorRippleShader } from './cursor-ripple/shader.js'; +export type { CursorRippleShaderProps } from './cursor-ripple/shader.js'; + +export { CursorSmudge } from './cursor-smudge/cursor-smudge.js'; +export type { CursorSmudgeProps } from './cursor-smudge/cursor-smudge.js'; + +export { CursorSpotlight } from './cursor-spotlight/cursor-spotlight.js'; +export type { CursorSpotlightProps } from './cursor-spotlight/cursor-spotlight.js'; + +export { Dissolve } from './dissolve/dissolve.js'; +export type { DissolveProps } from './dissolve/dissolve.js'; + +export { Dither } from './dither/dither.js'; +export type { DitherProps } from './dither/dither.js'; + +export { DotField } from './dot-field/dot-field.js'; +export type { DotFieldProps, DotShape, SvgMarkup } from './dot-field/dot-field.js'; + +export { FlutedGlass } from './fluted-glass/fluted-glass.js'; +export type { CrossSectionName, FlutedGlassProps } from './fluted-glass/fluted-glass.js'; + +export { FractalNoise } from './fractal-noise/fractal-noise.js'; +export type { FractalNoiseProps } from './fractal-noise/fractal-noise.js'; +export type { FractalNoiseStyle } from './fractal-noise/shader.js'; +export { STYLE_DIAL_DEFAULTS } from './fractal-noise/style-dial-defaults.js'; + +export { GodRays } from './god-rays/god-rays.js'; +export type { GodRaysProps } from './god-rays/god-rays.js'; + +export { Grain } from './grain/grain.js'; +export type { GrainProps } from './grain/grain.js'; +export type { GrainBlend } from './grain/shader.js'; + +export { LedWall } from './led-wall/led-wall.js'; +export type { LedWallProps } from './led-wall/led-wall.js'; + +export { LinearGradient } from './linear-gradient/linear-gradient.js'; +export type { LinearGradientProps } from './linear-gradient/linear-gradient.js'; + +export { LiquidGradient } from './liquid-gradient/liquid-gradient.js'; +export type { LiquidGradientProps } from './liquid-gradient/liquid-gradient.js'; + +export { MeshGradient } from './mesh-gradient/mesh-gradient.js'; +export type { MeshGradientProps } from './mesh-gradient/mesh-gradient.js'; + +export { RadialGradient } from './radial-gradient/radial-gradient.js'; +export type { RadialGradientProps } from './radial-gradient/radial-gradient.js'; + +export { RadialWipe } from './radial-wipe/radial-wipe.js'; +export type { RadialWipeProps } from './radial-wipe/radial-wipe.js'; + +export { SimplexNoise } from './simplex-noise/simplex-noise.js'; +export type { SimplexNoiseProps } from './simplex-noise/simplex-noise.js'; + +export { Vignette } from './vignette/vignette.js'; +export type { VignetteProps } from './vignette/vignette.js'; + +export { Voronoi } from './voronoi/voronoi.js'; +export type { VoronoiProps } from './voronoi/voronoi.js'; + +export { WaveLines } from './wave-lines/wave-lines.js'; +export type { WaveLine, WaveLinesProps } from './wave-lines/wave-lines.js'; + +export type { ColorStop, Palette } from './shared/color.js'; diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/engine.ts b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/engine.ts new file mode 100644 index 0000000..a8c0237 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/engine.ts @@ -0,0 +1,130 @@ +// The framework-free half of @camp-dev/shaders: TSL primitives, the renderer, +// the scheduler, and CPU color math. src/react and src/components import this +// barrel rather than the root index, which keeps the dependency direction one +// way (root -> react -> engine, root -> components -> react -> engine) and +// leaves this half extractable into its own package if a second framework +// binding ever becomes real. + +export { createRenderer } from './runtime/create-renderer/create-renderer.js'; +export type { + GpuRenderer, + GpuBackend, + CreateRendererOptions, +} from './runtime/create-renderer/create-renderer.js'; + +export { CursorInput } from './inputs/cursor-input/cursor-input.js'; +export type { + CursorInputOptions, + StrokePoint, + Vector2, +} from './inputs/cursor-input/cursor-input.js'; +export type { PushStroke } from './inputs/scroll-push/scroll-push.js'; + +export { colorRamp } from './primitives/color-ramp/color-ramp.js'; +export type { ColorRampStop, TSLNode } from './primitives/color-ramp/color-ramp.js'; + +export { mixColor } from './primitives/color-space/index.js'; +export type { ColorSpace, HueInterpolation } from './primitives/color-space/index.js'; + +// Re-exported from the ./color subpath, which is where these actually live. +// Keeping them here means no existing import breaks; new call sites in the docs +// site are pushed to the subpath by a lint rule in the root eslint config, +// because reaching color math through this barrel drags in three/webgpu. +export { + linearChannelToSrgb, + linearSrgbToLinearDisplayP3, + linearSrgbToOklch, + oklabToLinearSrgb, + oklchInGamut, + oklchToGamut, + oklchToLinearSrgb, + parseColorString, + parseOklchString, + srgbChannelToLinear, +} from './color.js'; +export type { OutputGamut } from './color.js'; + +export { simplexNoise } from './primitives/noise/noise.js'; + +export { fractalNoise } from './primitives/fbm/fbm.js'; +export type { FractalFold, FractalNoiseOptions } from './primitives/fbm/fbm.js'; + +export { voronoi } from './primitives/voronoi/voronoi.js'; + +export { voronoiCells } from './primitives/voronoi/voronoi-cells.js'; +export type { + VoronoiCellsOptions, + VoronoiCellsResult, +} from './primitives/voronoi/voronoi-cells.js'; + +export { MAX_BLOBS, metaballs } from './primitives/metaballs/metaballs.js'; +export type { MetaballsOptions, MetaballsResult } from './primitives/metaballs/metaballs.js'; + +export { quantize } from './primitives/quantize/quantize.js'; + +export { signedDistanceFieldCircle } from './primitives/sdf-circle/sdf-circle.js'; +export { signedDistanceFieldCross } from './primitives/sdf-cross/sdf-cross.js'; + +export { displace } from './primitives/displace/displace.js'; + +export { fluteSurface } from './primitives/flute-surface/flute-surface.js'; +export type { + FluteSurfaceOptions, + FluteSurfaceResult, +} from './primitives/flute-surface/flute-surface.js'; + +export { cursorRipple } from './primitives/cursor-ripple/cursor-ripple.js'; +export type { CursorRippleOptions } from './primitives/cursor-ripple/cursor-ripple.js'; + +export { elapsedTime } from './primitives/time/time.js'; + +export { holdRendererClock, resetRendererClock } from './runtime/clock/reset-clock.js'; + +export { grain } from './primitives/grain/grain.js'; +export { stableHash, stableHashUint } from './primitives/stable-hash/stable-hash.js'; + +export { dither } from './primitives/dither/dither.js'; +export { ditherThreshold } from './primitives/dither-pattern/dither-pattern.js'; +export type { DitherPattern } from './primitives/dither-pattern/dither-pattern.js'; + +export { + setReducedMotionPolicy, + getReducedMotionPolicy, + getReducedMotionTimeScale, + createReducedMotionWatcher, +} from './runtime/reduced-motion/reduced-motion.js'; +export type { + ReducedMotionPolicy, + ReducedMotionWatcher, +} from './runtime/reduced-motion/reduced-motion.js'; + +export { createVisibilityWatcher } from './runtime/visibility/visibility.js'; +export type { VisibilityWatcher } from './runtime/visibility/visibility.js'; + +export { createIntersectionWatcher } from './runtime/intersection/intersection.js'; +export type { IntersectionWatcher } from './runtime/intersection/intersection.js'; + +export { FrameScheduler } from './runtime/frame-scheduler/frame-scheduler.js'; + +export { createOutputStage } from './runtime/output-stage/output-stage.js'; +export type { TimeGpu } from './runtime/gpu-timer/gpu-timer.js'; +export type { + OutputStage, + PostProcessTransform, + PrePass, + UvTransform, +} from './runtime/output-stage/output-stage.js'; + +export { createWaveField, dampingForLifetime } from './runtime/wave-field/wave-field.js'; +export type { + WaveField, + WaveFieldStroke, + WaveFieldTuning, +} from './runtime/wave-field/wave-field.js'; + +export { createTexturePass } from './runtime/texture-pass/texture-pass.js'; +export type { TexturePass, TexturePassOptions } from './runtime/texture-pass/texture-pass.js'; + +export { createPauseWatcher } from './runtime/pause-watcher/pause-watcher.js'; +export type { PauseWatcher } from './runtime/pause-watcher/pause-watcher.js'; +export type { SchedulerTick, SchedulerClient } from './runtime/frame-scheduler/frame-scheduler.js'; diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/primitives/flute-surface/flute-surface.test.ts b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/primitives/flute-surface/flute-surface.test.ts new file mode 100644 index 0000000..86b1478 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/primitives/flute-surface/flute-surface.test.ts @@ -0,0 +1,53 @@ +import { uniform, uv } from 'three/tsl'; +import { describe, expect, it } from 'vitest'; + +import { fluteSurface } from './flute-surface.js'; + +describe('fluteSurface', () => { + it('returns bend, slope, fromCenter, and steepness nodes with default options', () => { + const surface = fluteSurface(uv().mul(400)); + + expect(surface.bend).toBeDefined(); + expect(surface.slope).toBeDefined(); + expect(surface.fromCenter).toBeDefined(); + expect(surface.steepness).toBeDefined(); + }); + + it('accepts numeric options', () => { + const surface = fluteSurface(uv().mul(400), { fluteWidth: 24, angle: 30, refraction: 0.5 }); + + expect(surface.bend).toBeDefined(); + }); + + it('accepts node options (uniform-driven dials)', () => { + const surface = fluteSurface(uv().mul(400), { + fluteWidth: uniform(40), + angle: uniform(90), + refraction: uniform(1), + }); + + expect(surface.bend).toBeDefined(); + expect(surface.slope).toBeDefined(); + }); + + it('accepts a cross-section at either end of the dial and between', () => { + for (const crossSection of [0, 0.25, 0.5, 1]) { + expect(fluteSurface(uv().mul(400), { crossSection }).slope).toBeDefined(); + } + }); + + it('accepts a node cross-section', () => { + const surface = fluteSurface(uv().mul(400), { crossSection: uniform(0.5) }); + + expect(surface.bend).toBeDefined(); + }); + + it('accepts a cross-section outside 0 to 1 without throwing', () => { + expect(() => fluteSurface(uv(), { crossSection: -1 })).not.toThrow(); + expect(() => fluteSurface(uv(), { crossSection: 2 })).not.toThrow(); + }); + + it('accepts a flute width of 0 without throwing', () => { + expect(() => fluteSurface(uv(), { fluteWidth: 0 })).not.toThrow(); + }); +}); diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/primitives/flute-surface/flute-surface.ts b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/primitives/flute-surface/flute-surface.ts new file mode 100644 index 0000000..a19bd76 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/packages/shaders/src/primitives/flute-surface/flute-surface.ts @@ -0,0 +1,272 @@ +// The surface of fluted glass: identical flutes side by side, each a strip +// of glass that bends what is under it. For a point, this works out how +// steep the glass is and how far it moves the image, in one call, so light +// read from the slope lands where the image bends. FlutedGlass bends and +// lights the scene with it, and Mode 2 users call it on their own coordinates. +import type { ShaderNodeObject } from 'three/tsl'; +import { + abs, + clamp, + cos, + dot, + float, + fract, + fwidth, + max, + mix, + pow, + select, + sin, + smoothstep, + vec2, +} from 'three/tsl'; +import type { Node } from 'three/webgpu'; + +import type { TSLNode } from '../color-ramp/color-ramp.js'; + +export interface FluteSurfaceOptions { + /** + * The width of one flute, in the units of `position`. Values near 0 clamp + * to a tiny width rather than dividing by zero. Default 40. + */ + fluteWidth?: TSLNode | number; + /** + * The direction the flutes run, in degrees counterclockwise from the + * position's +x axis with y up: 0 runs them left to right and 90 runs them + * bottom to top, the degrees LinearGradient's angle uses. Default 90, + * vertical flutes. + */ + angle?: TSLNode | number; + /** + * How far the glass moves the image, in half flute widths per unit of + * slope. 0 leaves it in place. At 1, a groove shows the image half a flute + * width away, so each flute shows a copy of twice its own width, squeezed. + * Default 1. + */ + refraction?: TSLNode | number; + /** + * The shape of each flute seen from its end, from 0 to 1. 0 is a pointed + * prism, two flat faces meeting at a ridge. 0.5 is a round lens. 1 is a + * flat top with steep walls. Values between give the shapes between, and + * values outside 0 to 1 clamp. Default 0.5. + */ + crossSection?: TSLNode | number; +} + +export interface FluteSurfaceResult { + /** + * How far to move the point before you sample the image under the glass, + * as a vector in the units and frame of `position`. Add it to `position`. + */ + bend: ShaderNodeObject; + /** + * How steep the glass is, as a vector in the frame of `position`. It + * points across the flute, away from the flute's center, and its length + * runs from 0 at the center to 1 at a groove. + */ + slope: ShaderNodeObject; + /** + * How far the point sits from its flute's center, from 0 at the center to + * 1 at a groove, whatever the cross-section. Unlike the slope it skips the + * groove smoothing, so it reaches 1 at a groove from both sides with no + * dip. Shading meant to be darkest on the groove reads this. + */ + fromCenter: ShaderNodeObject; + /** + * How steep the glass is, from 0 where it is flat to 1 at a groove. It is + * the slope's length without the groove smoothing, so like `fromCenter` it + * reaches 1 at a groove from both sides, where the slope dips to 0. + * Shading meant to follow the shape of the glass reads this. + */ + steepness: ShaderNodeObject; +} + +// --------------------------------------------- +// Feel constants (tuned by eye at the build's visual gates) +// --------------------------------------------- +// The slope's exponent at each end of the cross-section dial. A cross-section +// of 0.5 always has an exponent of 1. Lower PRISM_EXPONENT sharpens the +// prism's faces toward perfectly flat, and higher FLAT_EXPONENT flattens the +// top and steepens the walls. +const PRISM_EXPONENT = 1 / 16; +const FLAT_EXPONENT = 16; +// How many screen pixels each groove and the prism's ridge smooth over on +// either side. 0 brings back the stair steps; above about 2 the grooves read +// as soft blurs rather than sharp breaks. +const GROOVE_SMOOTHING = 1; + +/** + * The bend and slope of fluted glass at `position`. A flute's center sits on + * the position's origin, so center the position where the middle flute + * should go. Call it from a fragment shader: it smooths each groove over the + * screen-space rate of change of `position`, which only a fragment shader + * can measure. + * + * @param position โ€” Vec2 TSL node in any units with y up, such as `uv()` + * scaled to pixels. `fluteWidth` and the returned bend share its units. + */ +export function fluteSurface( + position: TSLNode, + options: FluteSurfaceOptions = {}, +): FluteSurfaceResult { + const fluteWidth = max(float(options.fluteWidth ?? 40), 1e-4); + const angle = float(options.angle ?? 90); + const refraction = float(options.refraction ?? 1); + const crossSection = clamp(float(options.crossSection ?? 0.5), 0, 1); + + // --------------------------------------------- + // The direction across the flutes + // --------------------------------------------- + // The flutes run along (cos, sin) of the angle. Across is that direction + // turned a quarter turn clockwise, (sin, -cos), so at the default 90 + // degrees it is (1, 0): across runs left to right over vertical flutes. + const angleRadians = angle.mul(Math.PI / 180); + const across = vec2(sin(angleRadians), cos(angleRadians).negate()); + + // --------------------------------------------- + // Repeating space: which part of which flute + // --------------------------------------------- + // dot() projects the position onto the across direction: its signed + // distance across the flutes, in the position's units. Dividing by the + // width counts that distance in flutes. The +0.5 below centers a flute on + // every whole number, so 2.25 lies a quarter flute right of the center of + // flute 2, halfway to its groove. + // + // fract() keeps only the part after the decimal point, which is how a + // shader repeats space: every flute gets the same 0-to-1 ramp, whatever + // its number. The +0.5 shifts the ramp so its middle, not its start, lands + // on the origin, which puts a flute's center there. At each groove the + // ramp jumps from 1 back to 0, and that jump is what breaks the image into + // strips. + const flutePhase = dot(position, across).div(fluteWidth); + const ramp = fract(flutePhase.add(0.5)); + + // The distance from the center, before any smoothing. ramp*2-1 runs from + // -1 at one groove to 1 at the other, and abs() folds it to run 0 to 1 + // from the center out on both sides. Where the ramp jumps from 1 to 0 at a + // groove, this goes from 1 to 1, so it has no jump to smooth. + const fromCenter = abs(ramp.mul(2).sub(1)); + + // --------------------------------------------- + // Clean grooves: antialiasing from the screen-space rate of change + // --------------------------------------------- + // The scene has no MSAA, the GPU's own edge smoothing, so a jump in the + // ramp lands on whole pixels and a slanted groove draws a staircase. The + // fix is to replace the jump with a slope exactly one pixel wide, and to + // know how wide a pixel is in flutes, the shader asks the GPU. + // + // The GPU shades pixels in 2x2 blocks, so each pixel can read how much a + // value changes to its neighbor. fwidth() adds that change across and + // down, so fwidth(flutePhase) is how many flutes one pixel spans: 0.025 for + // a 40-pixel flute, and 0.125 for an 8-pixel one. It is taken of the phase + // before fract(), which has no jump in it to throw the measure off. The max + // keeps the band from shrinking to nothing, which would divide by zero in + // smoothstep() when a flute is very wide. + const rampPerPixel = max(fwidth(flutePhase).mul(GROOVE_SMOOTHING), 1e-5); + + // Paper's smoothFract. distanceToGroove is 0 at a groove and falls to -0.5 + // at the flute's center. smoothstep() turns the last pixel before a groove + // into a blend that climbs from 0 to 0.5, so mix() eases the ramp toward + // its mirror image, 1 - ramp, and the two meet at 0.5. Both sides of a + // groove now arrive at 0.5, the flute's center, where the glass is flat, so + // the groove pixel shows the image right at the groove instead of either + // flute's bent copy. Away from the grooves the blend is 0 and the ramp is + // unchanged. + const distanceToGroove = abs(ramp.sub(0.5)).sub(0.5); + const grooveBlend = smoothstep(rampPerPixel.negate(), rampPerPixel, distanceToGroove); + const smoothRamp = mix(ramp, ramp.oneMinus(), grooveBlend); + + // *2-1 stretches the ramp to run from -1 at one groove, through 0 at the + // center, to 1 at the other groove. + const fluteOffset = smoothRamp.mul(2).sub(1); + + // --------------------------------------------- + // The cross-section: picking an exponent from the dial + // --------------------------------------------- + // The dial picks the exponent that shapes the slope below, on a doubling + // scale. dialFromRound runs from -1 at a prism, through 0 at round, to 1 at + // flat. The prism half raises PRISM_EXPONENT to its distance from round, + // and the flat half raises FLAT_EXPONENT the same way. At round both give + // 1, since anything to the power 0 is 1, so 0.5 is exactly round whatever + // the ends are. Each equal step of the dial multiplies the exponent by the + // same factor, so a sweep morphs the shape evenly. With ends of 1/16 and + // 16, the exponent doubles every eighth of the dial. + const dialFromRound = crossSection.mul(2).sub(1); + const exponentEnd = select( + dialFromRound.lessThan(0), + float(PRISM_EXPONENT), + float(FLAT_EXPONENT), + ); + const exponent = pow(exponentEnd, abs(dialFromRound)); + + // --------------------------------------------- + // The slope: shaping a curve with an exponent + // --------------------------------------------- + // The slope's size is the distance from the flute's center, |offset|, + // raised to the exponent. Because that distance runs from 0 to 1, a power + // bends the curve between without moving its ends: the center stays flat + // and the groove stays steepest. + // + // - An exponent of 1 leaves a straight line, so the slope grows in step + // with the distance: a round lens. Strictly that draws a parabola, a + // shallow stand-in for a circular arc, whose slope would shoot up near + // the grooves. + // - Below 1 the power lifts small values toward 1. x^(1/16) is already + // 0.65 at x = 0.001 and 0.96 at 0.5, so the slope jumps to nearly full + // just off the center and stays there: two flat faces meeting at a + // ridge, a prism. + // - Above 1 the power presses small values toward 0. x^16 is 0.00002 at + // 0.5 and reaches 0.5 only at 0.96, so the glass stays flat across the + // top and turns steep right at each groove. + // + // pow() needs a base of 0 or more, so the power is taken of fromCenter, + // the distance without the groove smoothing, and the offset carries the + // sign: offset * distance^(exponent - 1). Away from the grooves the offset + // is ยฑdistance, so that works out to ยฑdistance^exponent, the curve above. + // On a groove's own pixel the distance is about 1, and 1 to any power is + // 1, so the slope follows the smoothed offset through 0 over the same + // pixel whatever the exponent. Raising the smoothed offset itself would + // undo the smoothing on a prism: x^(1/16) is still 0.82 at x = 0.045, so + // the slope would stay near full until a sliver from the groove, and the + // grooves would staircase again. The max() keeps the base off 0 at the + // center, where a prism's negative power, exponent - 1, would shoot to + // infinity. Pointing the result along `across` turns it into a vector, so + // on either side of the center the slope points toward the nearer groove. + const shapedSlope = fluteOffset.mul(pow(max(fromCenter, 1e-5), exponent.sub(1))); + + // A prism's ridge jumps too. Just off the center x^(1/16) leaps from 0 to + // about 0.65 inside a sliver much thinner than a pixel, so the two faces + // meet in a staircase of their own. The same pixel width, doubled because + // the offset runs twice as fast as the ramp, fades the slope in over the + // first pixel either side of the center. On a round or flat flute the + // slope there is already close to 0, so the fade changes nothing you can + // see. It also softens the groove pixel, where the smoothed offset passes + // through 0 on its way between flutes. + const ridgeBlend = smoothstep(0, rampPerPixel.mul(2), abs(fluteOffset)); + const slopeAmount = shapedSlope.mul(ridgeBlend); + const slope = across.mul(slopeAmount); + + // The steepness takes the same power of fromCenter rather than of the + // smoothed offset. Away from the grooves the two are equal, so it is the + // slope's length there. On a groove's own pixel, where the smoothing pulls + // the slope through 0, it keeps climbing toward 1. The ridge fade reads + // fromCenter too, so it fades only at the center and never at a groove. + const steepness = pow(fromCenter, exponent).mul(smoothstep(0, rampPerPixel.mul(2), fromCenter)); + + // --------------------------------------------- + // The bend: refraction as a sample offset + // --------------------------------------------- + // Glass doesn't move pixels. It changes which point of the image behind it + // reaches the eye, so a shader bends an image by sampling it somewhere + // else. The bend is the slope times the dial, in half flute widths. On a + // round flute at a refraction of 1, a point at offset x from the center + // samples at 2x, so the flute's own width shows twice that width of image: + // a squeezed copy of what lies right under it. The bend points away from + // the center on both sides, which is what squeezes rather than stretches. + // Other cross-sections spread the same span unevenly. A prism shifts each + // face's half of the copy outward whole, and a flat top barely moves its + // middle and crams the rest against the walls. + const bend = slope.mul(refraction).mul(fluteWidth.mul(0.5)); + + return { bend, slope, fromCenter, steepness }; +} diff --git a/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/scripts/build-posters.sh b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/scripts/build-posters.sh new file mode 100644 index 0000000..5a38554 --- /dev/null +++ b/tests/fixtures/repo-files/campdotdev/shaders/files/84cc41703117a566f6e181dd8aec5ec7ce889521/scripts/build-posters.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Regenerate every committed poster image from its docs scene module. +# Each poster's --source is the SAME scene the live docs page renders, so the +# poster cannot drift from the demo. Runs one at a time: the poster dev server +# binds a fixed port and concurrent runs collide. +# +# After each capture, build-thumbnail.mjs cuts the component's card thumbnail +# for the components index from the new poster. Pass --thumbnails-only to +# re-cut every thumbnail from the committed posters without capturing any. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +CLI="node packages/shaders-cli/dist/index.js poster" +COMPONENTS_DIR="apps/docs/src/app/components" +OUT_DIR="apps/docs/public/posters" +WIDTH=1080 +HEIGHT=720 + +THUMBNAILS_ONLY=0 +case "${1:-}" in + "") ;; + --thumbnails-only) THUMBNAILS_ONLY=1 ;; + *) + echo "error: unknown argument $1. The only option is --thumbnails-only." >&2 + exit 1 + ;; +esac + +if [ "$THUMBNAILS_ONLY" = 0 ] && [ ! -f packages/shaders-cli/dist/index.js ]; then + echo "error: CLI not built. Run: pnpm --filter @camp-dev/shaders-cli build" >&2 + exit 1 +fi + +# name:format[:background][:WxH][:crop] entries (png for the flat shaders, jpg +# for the busy ones; background is optional and only needed for shaders with a +# transparent base layer, e.g. aurora, so they flatten onto a sensible color; +# WxH overrides the default capture size and is only needed for pixel-sized +# shaders like dot-field, whose posters render pixel-locked in the demo โ€” see +# DemoPoster's pixelSize prop โ€” and so must be captured at least as large as +# any demo box they'll be cropped into) +# +# fluted-glass is captured at the widest demo box: the 4xl column's 896px at +# 3:2, rounded up to 598. Its flutes are sized in CSS pixels, so its poster +# renders pixel-locked too. The MeshGradient under the glass stretches to the +# canvas, so a larger capture would zoom the gradient in every box. At this +# size the poster matches the live scene in the widest box. A narrower box +# keeps the flutes' spacing but shows only the middle of the gradient. +# +# crop is the side, in poster pixels, of the center square the card thumbnail +# is cut from. Without it the thumbnail keeps the poster's full height. Only a +# pixel-sized shader needs one, because its poster keeps the pattern at its +# real on-screen size and a full-height cut shrinks the pattern out of sight +# (build-thumbnail.mjs works through DotField's numbers). DotField's 360 is +# six of its 60-pixel cells across. +# +# radial-gradient is the exception to "flat means png": PNG's row filters +# predict each pixel from its left and upper neighbours, which crushes a linear +# gradient (every row identical, 35 KB) but does almost nothing for a radial one +# (every row different, 1.3 MB). JPEG lands at 36 KB with no banding visible at +# poster size. +for pair in \ + "linear-gradient:png" \ + "simplex-noise:png" \ + "aurora:jpg:#0b0f1a" \ + "grain:jpg" \ + "mesh-gradient:jpg" \ + "wave-lines:jpg" \ + "vignette:jpg" \ + "dot-field:png:#0a0a14:2048x1280:360" \ + "radial-gradient:jpg" \ + "god-rays:jpg:#0b0f1a" \ + "conic-gradient:jpg" \ + "liquid-gradient:jpg" \ + "dither:jpg" \ + "voronoi:jpg" \ + "fractal-noise:jpg" \ + "blobs:jpg" \ + "led-wall:jpg:#0b0f0d" \ + "cursor-ripple:jpg" \ + "cursor-smudge:jpg" \ + "cursor-spotlight:jpg" \ + "fluted-glass:jpg::896x598" \ + "radial-wipe:jpg:#0b0f0d" \ + "dissolve:jpg:#0b0f0d"; do + IFS=':' read -r name format background size crop <<< "$pair" + width="$WIDTH" + height="$HEIGHT" + if [ -n "${size:-}" ]; then + width="${size%x*}" + height="${size#*x}" + fi + poster="${OUT_DIR}/${name}.${format}" + if [ "$THUMBNAILS_ONLY" = 0 ]; then + echo "==> $name ($format, ${width}x${height})" + args=( + --source "${COMPONENTS_DIR}/${name}/scene.tsx" + --output "$poster" + --format "${format}" + --width "$width" + --height "$height" + ) + if [ -n "$background" ]; then + args+=(--background "$background") + fi + $CLI "${args[@]}" + fi + thumbnail_args=("$poster") + if [ -n "${crop:-}" ]; then + thumbnail_args+=(--crop "$crop") + fi + node scripts/build-thumbnail.mjs "${thumbnail_args[@]}" +done + +if [ "$THUMBNAILS_ONLY" = 1 ]; then + echo "All thumbnails regenerated." + exit 0 +fi + +# The homepage hero gets its own Aurora poster at the hero's 1636 by 696. +# Aurora always fits the sky band to the canvas height and widens it at the +# sides, so the 3:2 poster, cover-cropped into the wider hero, showed a +# zoomed-in slice that jumped when the live scene replaced it. A poster at +# least as wide as the box crops only its sides, the way the shader does. +echo "==> aurora-hero (jpg, 1636x696)" +$CLI \ + --source "${COMPONENTS_DIR}/aurora/scene.tsx" \ + --output "${OUT_DIR}/aurora-hero.jpg" \ + --format jpg \ + --width 1636 \ + --height 696 \ + --background "#0b0f1a" + +# The homepage favorites render every live scene at 320 by 233, the card +# window's size at the grid's full 1440px (SCENE_SIZE in +# apps/docs/src/components/favorites/favorites-list.tsx), and scale it with +# the card. Each favorite's card poster is captured at that size, over the +# ground its scene sits on there, so the poster frames and crops the scene +# the way the live canvas does. A 3:2 poster cropped into the card drifted +# from shaders that stretch to the canvas, such as Simplex Noise, and from +# ones that size their cells in CSS pixels, such as Dither and LED Wall. +# Entries are name:format[:background]; the backgrounds are the demos' +# backdrops, and the page color for LED Wall's gaps. +for pair in \ + "simplex-noise:png" \ + "mesh-gradient:jpg" \ + "wave-lines:jpg:#0a0a14" \ + "voronoi:jpg" \ + "dither:jpg" \ + "god-rays:jpg:linear-gradient(to top, #131b31, #0b0f1a)" \ + "led-wall:jpg:#0b0f0d"; do + IFS=':' read -r name format background <<< "$pair" + echo "==> ${name}-card ($format, 320x233)" + card_args=( + --source "${COMPONENTS_DIR}/${name}/scene.tsx" + --output "${OUT_DIR}/${name}-card.${format}" + --format "$format" + --width 320 + --height 233 + ) + if [ -n "$background" ]; then + card_args+=(--background "$background") + fi + $CLI "${card_args[@]}" +done + +# The Components banner is captured from its own scene at the mock's width, +# 1728 by the 200px header block, and shown at that size, center-cropped, by +# banner-shader.tsx. Past 864px either side of center the glow has already +# reached page black, so a wider viewport shows the page past the poster's +# edges with no seam. +echo "==> banner (jpg, 1728x200)" +$CLI \ + --source apps/docs/src/components/section-banner/banner-scene.tsx \ + --output "${OUT_DIR}/banner.jpg" \ + --format jpg \ + --width 1728 \ + --height 200 \ + --background "#0b0f0d" + +echo "All posters regenerated." From 8315d0d857e4f9ae08c6e4572505689273489c25 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Wed, 28 Oct 2026 23:44:18 -0400 Subject: [PATCH 03/58] feat: render a guide as a two-pane page `/guides///` shows the guide's overview in the right pane and one collapsed block per step in the left pane. Clicking a block opens its hunks and shows the step's explanation in the right pane. Clicking it again brings back the overview. The guide model in `src/guide/` does the work, apart from the components. It parses the body, checks the structure, and resolves each block pointer to diff rows. A pointer shows its range with three lines of context. The rows are cut from GitHub's `patch` hunks, not computed with a diff library, so they match the pull request on GitHub. Prose goes through `rehype-sanitize`, because agents push guides without review. A malformed guide fails the build with an error that names the file and the field, such as `steps.2.pointers.1`. Frontmatter errors come from the collection schema. Browser behavior lives in `src/client/`, one module per feature. Step selection exports `selectStep` and fires `guide:step-selected`, so later features can select steps the same way a click does. The Playwright specs build the site from the fixtures, serve it under `/guides`, and block every request outside the preview server. Co-Authored-By: Claude Opus 5.5 --- playwright.config.ts | 34 ++++ src/client/step-selection.ts | 49 ++++++ src/components/FileDiff.astro | 154 ++++++++++++++++++ src/components/GuideHeader.astro | 56 +++++++ src/components/GuidePage.astro | 57 +++++++ src/components/Overview.astro | 20 +++ src/components/StepBlock.astro | 124 ++++++++++++++ src/components/StepExplanation.astro | 14 ++ src/content.config.ts | 13 ++ src/guide/errors.ts | 31 ++++ src/guide/hunks.ts | 57 +++++++ src/guide/load.ts | 68 ++++++++ src/guide/loader.ts | 44 +++++ src/guide/parse.ts | 144 ++++++++++++++++ src/guide/patch.ts | 77 +++++++++ src/guide/pointer.ts | 40 +++++ src/guide/prose.ts | 38 +++++ src/guide/resolve.ts | 101 ++++++++++++ src/highlight.ts | 33 ++++ src/layouts/BaseLayout.astro | 21 +++ src/pages/[repo]/[pr].astro | 15 ++ src/styles/global.css | 138 ++++++++++++++++ tests/e2e/guide-page.spec.ts | 114 +++++++++++++ tests/e2e/malformed-guide.spec.ts | 56 +++++++ tests/e2e/test.ts | 22 +++ tests/fixture-build.ts | 7 + .../malformed/backward-range/shaders/264.md | 48 ++++++ .../malformed/short-head-sha/shaders/264.md | 48 ++++++ .../malformed/unchanged-file/shaders/264.md | 48 ++++++ 29 files changed, 1671 insertions(+) create mode 100644 playwright.config.ts create mode 100644 src/client/step-selection.ts create mode 100644 src/components/FileDiff.astro create mode 100644 src/components/GuideHeader.astro create mode 100644 src/components/GuidePage.astro create mode 100644 src/components/Overview.astro create mode 100644 src/components/StepBlock.astro create mode 100644 src/components/StepExplanation.astro create mode 100644 src/content.config.ts create mode 100644 src/guide/errors.ts create mode 100644 src/guide/hunks.ts create mode 100644 src/guide/load.ts create mode 100644 src/guide/loader.ts create mode 100644 src/guide/parse.ts create mode 100644 src/guide/patch.ts create mode 100644 src/guide/pointer.ts create mode 100644 src/guide/prose.ts create mode 100644 src/guide/resolve.ts create mode 100644 src/highlight.ts create mode 100644 src/layouts/BaseLayout.astro create mode 100644 src/pages/[repo]/[pr].astro create mode 100644 src/styles/global.css create mode 100644 tests/e2e/guide-page.spec.ts create mode 100644 tests/e2e/malformed-guide.spec.ts create mode 100644 tests/e2e/test.ts create mode 100644 tests/fixture-build.ts create mode 100644 tests/fixtures/malformed/backward-range/shaders/264.md create mode 100644 tests/fixtures/malformed/short-head-sha/shaders/264.md create mode 100644 tests/fixtures/malformed/unchanged-file/shaders/264.md diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b9b9421 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test'; +import { FIXTURE_BUILD_ENV } from './tests/fixture-build.ts'; + +// Set PLAYWRIGHT_PORT when another preview server already holds 4321, such as one in a parallel worktree. +const PORT = Number(process.env.PLAYWRIGHT_PORT ?? 4321); + +export default defineConfig({ + testDir: 'tests/e2e', + // Specs that run `astro build` share Astro's caches in .astro/ and node_modules/.astro, so run one spec at a time. + workers: 1, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + reporter: process.env.CI ? 'github' : 'list', + use: { + baseURL: `http://localhost:${PORT}`, + trace: 'on-first-retry', + }, + // Build the real static site, then serve it under its /guides base, the way GitHub Pages does. + // `--ignore-lock` keeps `astro preview` in the foreground, so Playwright owns and stops it. + // Without it, Astro 7 moves the server to the background when it detects a coding agent. + webServer: { + command: `pnpm build && pnpm preview --port ${PORT} --ignore-lock`, + env: FIXTURE_BUILD_ENV, + url: `http://localhost:${PORT}/guides/shaders/264/`, + reuseExistingServer: false, + timeout: 120_000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } }, + }, + ], +}); diff --git a/src/client/step-selection.ts b/src/client/step-selection.ts new file mode 100644 index 0000000..7ed25be --- /dev/null +++ b/src/client/step-selection.ts @@ -0,0 +1,49 @@ +/** + * Step selection: one step at a time is open in the left pane and explained in the right pane. + * With no step selected, the right pane shows the overview. + * + * Other client features select steps through `selectStep` and follow changes through the + * `STEP_SELECTED` event, so a click, a key, and a link all select a step the same way. + */ + +/** Fired on `document` after the selected step changes. */ +export const STEP_SELECTED = 'guide:step-selected'; + +export interface StepSelectedDetail { + /** The selected step's number, or null when the overview shows. */ + step: number | null; +} + +/** The selected step's number, or null when the overview shows. */ +export function selectedStep(): number | null { + const block = document.querySelector('[data-step][data-selected]'); + return block ? Number(block.dataset.step) : null; +} + +/** Opens a step's block and shows its explanation, or shows the overview for null. */ +export function selectStep(step: number | null): void { + for (const block of document.querySelectorAll('[data-step]')) { + const selected = Number(block.dataset.step) === step; + block.toggleAttribute('data-selected', selected); + const toggle = block.querySelector('[data-step-toggle]'); + toggle?.setAttribute('aria-expanded', String(selected)); + const code = document.getElementById(toggle?.getAttribute('aria-controls') ?? ''); + if (code) code.hidden = !selected; + } + const explains = step === null ? 'overview' : String(step); + for (const panel of document.querySelectorAll('[data-explains]')) { + panel.hidden = panel.dataset.explains !== explains; + } + document.dispatchEvent(new CustomEvent(STEP_SELECTED, { detail: { step } })); +} + +/** A click on a step's header opens that step. A click on the open step's header returns to the overview. */ +export function initStepSelection(): void { + document.addEventListener('click', (event) => { + const toggle = (event.target as Element | null)?.closest('[data-step-toggle]'); + const block = toggle?.closest('[data-step]'); + if (!block) return; + const step = Number(block.dataset.step); + selectStep(selectedStep() === step ? null : step); + }); +} diff --git a/src/components/FileDiff.astro b/src/components/FileDiff.astro new file mode 100644 index 0000000..955b979 --- /dev/null +++ b/src/components/FileDiff.astro @@ -0,0 +1,154 @@ +--- +import type { DiffRow } from '../guide/patch.ts'; +import { hunkHeader } from '../guide/patch.ts'; +import type { StepFile } from '../guide/resolve.ts'; +import { highlightLines, tokenStyle } from '../highlight.ts'; + +interface Props { + file: StepFile; + /** A page-unique id for the file's header. */ + id: string; +} +const { file, id } = Astro.props; + +const headLines = file.head === null ? [] : await highlightLines(file.head, file.path); +const baseLines = file.base === null ? [] : await highlightLines(file.base, file.basePath); +const tokensOf = (row: DiffRow) => + (row.kind === 'del' ? baseLines[row.oldLine! - 1] : headLines[row.newLine! - 1]) ?? [ + { content: row.text, offset: 0 }, + ]; +const MARKER = { add: '+', del: '-', context: ' ' } as const; +const SPOKEN = { add: 'added', del: 'removed', context: '' } as const; +--- + +
+
+ {file.path} + {file.status} +
+
+ + + + + + + + + + { + file.hunks.map((hunk) => ( + + + + + {hunk.rows.map((row) => ( + + + + + + + ))} + + )) + } +
Base lineHead lineChangeCode
{hunkHeader(hunk)}
{row.oldLine}{row.newLine} + + {SPOKEN[row.kind] && {SPOKEN[row.kind]}} + + {tokensOf(row).map((token) => {token.content})} +
+
+
+ + diff --git a/src/components/GuideHeader.astro b/src/components/GuideHeader.astro new file mode 100644 index 0000000..9e1805f --- /dev/null +++ b/src/components/GuideHeader.astro @@ -0,0 +1,56 @@ +--- +import type { GuideFrontmatter } from '../guide/schema.ts'; + +interface Props { + meta: GuideFrontmatter; +} +const { meta } = Astro.props; +const github = `https://github.com/${meta.repo}`; +--- + +
+

+ {meta.repo}#{meta.pr} + {meta.scope && {meta.scope}} +

+

{meta.title}

+

+ Describes commit {meta.headSha.slice(0, 7)} +

+ {meta.afterTheFact &&

Written after the fact from the diff, without the sessionโ€™s reasoning.

} +
+ + diff --git a/src/components/GuidePage.astro b/src/components/GuidePage.astro new file mode 100644 index 0000000..90d7e04 --- /dev/null +++ b/src/components/GuidePage.astro @@ -0,0 +1,57 @@ +--- +import type { Guide } from '../guide/load.ts'; +import BaseLayout from '../layouts/BaseLayout.astro'; +import GuideHeader from './GuideHeader.astro'; +import Overview from './Overview.astro'; +import StepBlock from './StepBlock.astro'; +import StepExplanation from './StepExplanation.astro'; + +interface Props { + guide: Guide; +} +const { guide } = Astro.props; +--- + + + + +
+
+ {guide.steps.map((step) => )} +
+
+ + {guide.steps.map((step) => )} +
+
+
+ + diff --git a/src/components/Overview.astro b/src/components/Overview.astro new file mode 100644 index 0000000..4771148 --- /dev/null +++ b/src/components/Overview.astro @@ -0,0 +1,20 @@ +--- +import type { Guide } from '../guide/load.ts'; + +interface Props { + overview: Guide['overview']; +} +const { overview } = Astro.props; +--- + +
+

Overview

+ { + overview.map((part) => ( +
+

{part.heading}

+ +
+ )) + } +
diff --git a/src/components/StepBlock.astro b/src/components/StepBlock.astro new file mode 100644 index 0000000..73a676a --- /dev/null +++ b/src/components/StepBlock.astro @@ -0,0 +1,124 @@ +--- +import type { Step } from '../guide/load.ts'; +import FileDiff from './FileDiff.astro'; + +interface Props { + step: Step; +} +const { step } = Astro.props; +const id = `step-${step.number}`; +const fileCount = `${step.files.length} ${step.files.length === 1 ? 'file' : 'files'}`; +--- + +
+
+

+ +

+

+ {fileCount} + + {step.additions} lines added, {step.deletions} removed +

+
    + {step.files.map((file) =>
  • {file.path}
  • )} +
+
+ +
+ + + + diff --git a/src/components/StepExplanation.astro b/src/components/StepExplanation.astro new file mode 100644 index 0000000..b616d04 --- /dev/null +++ b/src/components/StepExplanation.astro @@ -0,0 +1,14 @@ +--- +import type { Step } from '../guide/load.ts'; + +interface Props { + step: Step; +} +const { step } = Astro.props; +const id = `step-${step.number}-explanation`; +--- + + diff --git a/src/content.config.ts b/src/content.config.ts new file mode 100644 index 0000000..4c985d0 --- /dev/null +++ b/src/content.config.ts @@ -0,0 +1,13 @@ +import { defineCollection } from 'astro:content'; +import { guideLoader } from './guide/loader.ts'; +import { guideFrontmatter } from './guide/schema.ts'; + +/** The guides the tests own. Production sets GUIDES_DIR to a checkout of the content branch. */ +const DEFAULT_GUIDES_DIR = 'tests/fixtures/guides'; + +export const collections = { + guides: defineCollection({ + loader: guideLoader(process.env.GUIDES_DIR ?? DEFAULT_GUIDES_DIR), + schema: guideFrontmatter, + }), +}; diff --git a/src/guide/errors.ts b/src/guide/errors.ts new file mode 100644 index 0000000..9adf0d3 --- /dev/null +++ b/src/guide/errors.ts @@ -0,0 +1,31 @@ +/** + * A problem in a guide file, named by the field it sits in, such as `headSha` or `steps.2.pointers.1`. + * Step and pointer numbers start at 1, like the step numbers on the page. + */ +export class GuideError extends Error { + readonly field: string; + + constructor(field: string, message: string) { + super(`${field}: ${message}`); + this.name = 'GuideError'; + this.field = field; + } +} + +/** A guide's path inside the guides directory, which is also its path on the content branch. */ +export function guideFile(id: string): string { + return `${id}.md`; +} + +/** + * Runs `check` on the guide with collection id `id`, such as `shaders/264`, + * and prefixes any GuideError with the guide's file, so the build log names both. + */ +export async function inGuide(id: string, check: () => T | Promise): Promise { + try { + return await check(); + } catch (error) { + if (error instanceof GuideError) throw new Error(`Invalid guide ${guideFile(id)}: ${error.message}`); + throw error; + } +} diff --git a/src/guide/hunks.ts b/src/guide/hunks.ts new file mode 100644 index 0000000..05ab146 --- /dev/null +++ b/src/guide/hunks.ts @@ -0,0 +1,57 @@ +import type { DiffRow, Hunk } from './patch.ts'; +import type { LineRange } from './pointer.ts'; + +/** How many lines a block pointer shows on each side of its range. */ +export const CONTEXT_LINES = 3; + +/** True when the range touches the head-side lines the hunk covers. */ +export function touches(hunk: Hunk, range: LineRange): boolean { + const last = hunk.newStart + hunk.newLines - 1; + return range.start <= last && range.end >= hunk.newStart; +} + +/** + * Cuts the rows that a file's block pointers show out of GitHub's hunks. + * Each pointer keeps the rows from `context` lines before its range to `context` lines after it, + * plus any deleted rows in between. Pointers whose rows meet or overlap share one hunk. + */ +export function cutHunks(hunks: Hunk[], ranges: LineRange[], context = CONTEXT_LINES): Hunk[] { + const cut: Hunk[] = []; + for (const hunk of hunks) { + const keep = hunk.rows.map(() => false); + for (const range of ranges) { + const inWindow = hunk.rows.flatMap((row, index) => + row.newLine !== null && row.newLine >= range.start - context && row.newLine <= range.end + context + ? [index] + : [], + ); + if (inWindow.length === 0) continue; + keep.fill(true, inWindow[0], inWindow.at(-1)! + 1); + } + for (let start = 0; start < keep.length; start++) { + if (!keep[start]) continue; + let end = start; + while (end < keep.length && keep[end]) end++; + cut.push(subHunk(hunk, start, end)); + start = end; + } + } + return cut; +} + +/** The rows `start` to `end` (exclusive) of a hunk, with its header numbers recounted. */ +function subHunk(hunk: Hunk, start: number, end: number): Hunk { + const rows = hunk.rows.slice(start, end); + const oldRowsBefore = countLines(hunk.rows.slice(0, start), 'oldLine'); + const newRowsBefore = countLines(hunk.rows.slice(0, start), 'newLine'); + const oldLines = countLines(rows, 'oldLine'); + const newLines = countLines(rows, 'newLine'); + // Unified diff numbers an empty side by the line before it, so an added file's base side is 0. + const oldStart = oldLines === 0 ? Math.max(0, hunk.oldStart + oldRowsBefore - 1) : hunk.oldStart + oldRowsBefore; + const newStart = newLines === 0 ? Math.max(0, hunk.newStart + newRowsBefore - 1) : hunk.newStart + newRowsBefore; + return { oldStart, oldLines, newStart, newLines, section: hunk.section, rows }; +} + +function countLines(rows: DiffRow[], side: 'oldLine' | 'newLine'): number { + return rows.filter((row) => row[side] !== null).length; +} diff --git a/src/guide/load.ts b/src/guide/load.ts new file mode 100644 index 0000000..cd05dcf --- /dev/null +++ b/src/guide/load.ts @@ -0,0 +1,68 @@ +import type { CollectionEntry } from 'astro:content'; +import { repoFileSource, type ChangedFile, type RepoFileSource } from '../repo-files/index.ts'; +import { inGuide } from './errors.ts'; +import { OVERVIEW_PARTS, parseGuide, type OverviewKey } from './parse.ts'; +import { renderProse, type Html } from './prose.ts'; +import { PullRequestFiles, type StepFile } from './resolve.ts'; +import { pullRequestOf, type GuideFrontmatter } from './schema.ts'; + +/** Everything a guide page renders. */ +export interface Guide { + meta: GuideFrontmatter; + overview: { key: OverviewKey; heading: string; html: Html }[]; + steps: Step[]; + /** Every file the pull request changes, whether or not a step points at it. */ + changedFiles: ChangedFile[]; +} + +export interface Step { + /** The step's number on the page, starting at 1. */ + number: number; + title: string; + html: Html; + files: StepFile[]; + /** Added and removed lines across the hunks the step shows. */ + additions: number; + deletions: number; +} + +/** + * Builds the page model for one guide entry: parses the body, renders the prose, and resolves + * each block pointer to hunks from the repo file source. A bad pointer fails the build here. + */ +export async function loadGuide( + entry: CollectionEntry<'guides'>, + source: RepoFileSource = repoFileSource(), +): Promise { + const meta = entry.data; + return inGuide(entry.id, async () => { + const parsed = parseGuide(entry.body ?? ''); + const files = await PullRequestFiles.load(pullRequestOf(meta), source); + const context = { repo: meta.repo, headSha: meta.headSha }; + const steps = await Promise.all( + parsed.steps.map(async (step): Promise => { + const stepFiles = await files.stepFiles(step); + const rows = stepFiles.flatMap((file) => file.hunks.flatMap((hunk) => hunk.rows)); + return { + number: step.number, + title: step.title, + html: renderProse(step.prose, context), + files: stepFiles, + additions: rows.filter((row) => row.kind === 'add').length, + deletions: rows.filter((row) => row.kind === 'del').length, + }; + }), + ); + return { + meta, + overview: OVERVIEW_PARTS.map((part) => ({ ...part, html: renderProse(parsed.overview[part.key], context) })), + steps, + changedFiles: files.changed, + }; + }); +} + +/** The route of a guide page: `///`. */ +export function guideParams(meta: GuideFrontmatter): { repo: string; pr: string } { + return { repo: meta.repo.split('/')[1]!, pr: String(meta.pr) }; +} diff --git a/src/guide/loader.ts b/src/guide/loader.ts new file mode 100644 index 0000000..8581653 --- /dev/null +++ b/src/guide/loader.ts @@ -0,0 +1,44 @@ +import { glob, type Loader } from 'astro/loaders'; +import { GuideError, guideFile, inGuide } from './errors.ts'; +import { parseGuide } from './parse.ts'; +import type { GuideFrontmatter } from './schema.ts'; + +/** + * Loads guide files from `base`, where each guide sits at `/.md`, such as `shaders/264.md`. + * An entry's id is that path without `.md`. + * + * The collection schema checks the frontmatter. This loader then checks the file's place and the body's structure, + * so a malformed guide fails the build with an error that names the bad field. + * Pointers are checked against the pull request's files when the page renders. + */ +export function guideLoader(base: string): Loader { + const inner = glob({ + // Files at the top level, such as the content branch's README, are not guides. + pattern: ['**/*.md', '!*.md'], + base, + generateId: ({ entry }) => entry.replace(/\.md$/, ''), + }); + return { + name: 'guide-loader', + load: async (context) => { + await inner.load(context); + for (const entry of context.store.values()) { + await inGuide(entry.id, () => { + checkPlace(entry.id, entry.data as GuideFrontmatter); + parseGuide(entry.body ?? ''); + }); + } + }, + }; +} + +function checkPlace(id: string, frontmatter: GuideFrontmatter): void { + const repoName = frontmatter.repo.split('/')[1]; + const expected = `${repoName}/${frontmatter.pr}`; + if (id !== expected) { + throw new GuideError( + 'pr', + `a guide to ${frontmatter.repo}#${frontmatter.pr} belongs at ${guideFile(expected)}, not ${guideFile(id)}.`, + ); + } +} diff --git a/src/guide/parse.ts b/src/guide/parse.ts new file mode 100644 index 0000000..1d99da1 --- /dev/null +++ b/src/guide/parse.ts @@ -0,0 +1,144 @@ +import type { Heading, Link, Root, RootContent } from 'mdast'; +import { toString } from 'mdast-util-to-string'; +import remarkGfm from 'remark-gfm'; +import remarkParse from 'remark-parse'; +import { unified } from 'unified'; +import { visit } from 'unist-util-visit'; +import { GuideError } from './errors.ts'; +import { parseBlockPointer, type Pointer } from './pointer.ts'; + +/** The overview's four parts, in the order a guide must write them. */ +export const OVERVIEW_PARTS = [ + { key: 'why', heading: 'Why' }, + { key: 'behaviorChange', heading: 'Behavior change' }, + { key: 'checks', heading: 'Checks' }, + { key: 'notCovered', heading: 'Not covered' }, +] as const; + +export type OverviewKey = (typeof OVERVIEW_PARTS)[number]['key']; + +/** A guide body split into its sections, still as Markdown syntax trees. */ +export interface ParsedGuide { + overview: Record; + steps: ParsedStep[]; +} + +export interface ParsedStep { + /** The step's number on the page, starting at 1. */ + number: number; + title: string; + /** The explanation, with the block pointers taken out. */ + prose: RootContent[]; + /** The block pointers, in the order the guide lists them. */ + pointers: Pointer[]; +} + +const OVERVIEW_HEADING = 'Overview'; + +/** Parses a guide body (the Markdown after the frontmatter) and checks its structure. */ +export function parseGuide(body: string): ParsedGuide { + const tree = unified().use(remarkParse).use(remarkGfm).parse(body) as Root; + const sections = splitSections(tree.children, 2); + + if (sections.lead.some(isContent)) { + throw new GuideError('body', `the body has text before "## ${OVERVIEW_HEADING}", which must come first.`); + } + const [first] = sections.sections; + if (!first || toString(first.heading) !== OVERVIEW_HEADING) { + throw new GuideError('overview', `the body must start with "## ${OVERVIEW_HEADING}".`); + } + + const overview = parseOverview(first.children); + rejectStrayPointers(first.children, 'overview'); + const steps = sections.sections.slice(1).map((section, index) => parseStep(section, index + 1)); + if (steps.length === 0) throw new GuideError('steps', 'the guide has no steps. Add a "## " section after the overview.'); + return { overview, steps }; +} + +function parseStep(section: Section, number: number): ParsedStep { + const field = `steps.${number}`; + const title = toString(section.heading).trim(); + if (title === '') throw new GuideError(field, 'the step heading has no title.'); + + const prose: RootContent[] = []; + const pointers: Pointer[] = []; + for (const node of section.children) { + const link = blockPointerLink(node); + if (link) pointers.push(parseBlockPointer(link.url, `${field}.pointers.${pointers.length + 1}`)); + else prose.push(node); + } + rejectStrayPointers(prose, field); + if (!prose.some(isContent)) throw new GuideError(field, `"${title}" has no explanation.`); + if (pointers.length === 0) { + throw new GuideError(`${field}.pointers`, `"${title}" has no block pointer. Add one on its own line, such as [](src/file.ts#L10-L20).`); + } + return { number, title, prose, pointers }; +} + +/** A block pointer is a paragraph that holds only a link with no text: `[](src/file.ts#L10-L20)`. */ +function blockPointerLink(node: RootContent): Link | undefined { + if (node.type !== 'paragraph' || node.children.length !== 1) return undefined; + const [child] = node.children; + return child?.type === 'link' && isEmptyLink(child) ? child : undefined; +} + +/** An empty link anywhere but alone on its line is a block pointer in the wrong place. */ +function rejectStrayPointers(nodes: RootContent[], field: string): void { + visit({ type: 'root', children: nodes } as Root, 'link', (link) => { + if (isEmptyLink(link)) { + throw new GuideError(field, `the link [](${link.url}) has no text. A block pointer sits alone on its own line, and an inline pointer needs link text.`); + } + }); +} + +function isEmptyLink(link: Link): boolean { + return toString(link).trim() === ''; +} + +function parseOverview(nodes: RootContent[]): ParsedGuide['overview'] { + const { lead, sections } = splitSections(nodes, 3); + if (lead.some(isContent)) { + throw new GuideError('overview', `put the overview's text under its "### ${OVERVIEW_PARTS[0].heading}" heading.`); + } + const found = sections.map((section) => toString(section.heading)); + const expected = OVERVIEW_PARTS.map((part) => part.heading); + if (found.join('\n') !== expected.join('\n')) { + throw new GuideError( + 'overview', + `expected the parts ${expected.map((h) => `"### ${h}"`).join(', ')} in that order, but found ${ + found.length ? found.map((h) => `"### ${h}"`).join(', ') : 'none' + }.`, + ); + } + + const overview = {} as ParsedGuide['overview']; + OVERVIEW_PARTS.forEach((part, index) => { + const children = sections[index]!.children; + if (!children.some(isContent)) throw new GuideError('overview', `"### ${part.heading}" is empty.`); + overview[part.key] = children; + }); + return overview; +} + +interface Section { + heading: Heading; + children: RootContent[]; +} + +/** Splits nodes at each heading of `depth`. `lead` holds the nodes before the first one. */ +function splitSections(nodes: RootContent[], depth: number): { lead: RootContent[]; sections: Section[] } { + const lead: RootContent[] = []; + const sections: Section[] = []; + for (const node of nodes) { + if (node.type === 'heading' && node.depth === depth) sections.push({ heading: node, children: [] }); + else if (node.type === 'heading' && node.depth < depth) { + throw new GuideError('body', `"${'#'.repeat(node.depth)} ${toString(node)}" is a heading level the format does not use here.`); + } else (sections.at(-1)?.children ?? lead).push(node); + } + return { lead, sections }; +} + +/** Raw HTML never renders, so it does not count as content. */ +function isContent(node: RootContent): boolean { + return node.type !== 'html'; +} diff --git a/src/guide/patch.ts b/src/guide/patch.ts new file mode 100644 index 0000000..463d44f --- /dev/null +++ b/src/guide/patch.ts @@ -0,0 +1,77 @@ +/** One line of a diff, numbered on each side it exists on. */ +export interface DiffRow { + kind: 'context' | 'add' | 'del'; + /** The line number at the base commit, or null for an added line. */ + oldLine: number | null; + /** The line number at the head commit, or null for a deleted line. */ + newLine: number | null; + text: string; +} + +/** A unified-diff hunk with numbered rows. */ +export interface Hunk { + oldStart: number; + oldLines: number; + newStart: number; + newLines: number; + /** The text GitHub prints after the second `@@`, usually the enclosing declaration. */ + section: string; + rows: DiffRow[]; +} + +const HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/; + +/** + * Parses the `patch` text GitHub returns for a file into hunks. + * The renderer uses GitHub's hunks instead of computing its own, so its rows match what GitHub shows for the PR. + */ +export function parsePatch(patch: string): Hunk[] { + const hunks: Hunk[] = []; + let hunk: Hunk | undefined; + let oldLine = 0; + let newLine = 0; + let oldLeft = 0; + let newLeft = 0; + + for (const line of patch.split('\n')) { + const header = HEADER.exec(line); + if (header) { + hunk = { + oldStart: Number(header[1]), + oldLines: header[2] === undefined ? 1 : Number(header[2]), + newStart: Number(header[3]), + newLines: header[4] === undefined ? 1 : Number(header[4]), + section: header[5] ?? '', + rows: [], + }; + hunks.push(hunk); + oldLine = hunk.oldStart; + newLine = hunk.newStart; + oldLeft = hunk.oldLines; + newLeft = hunk.newLines; + continue; + } + // The header's counts say where a hunk ends. Anything past them, such as a trailing + // empty line or "\ No newline at end of file", is not a row. + if (!hunk || (oldLeft === 0 && newLeft === 0) || line.startsWith('\\')) continue; + const text = line.slice(1); + if (line.startsWith('+')) { + hunk.rows.push({ kind: 'add', oldLine: null, newLine: newLine++, text }); + newLeft--; + } else if (line.startsWith('-')) { + hunk.rows.push({ kind: 'del', oldLine: oldLine++, newLine: null, text }); + oldLeft--; + } else { + hunk.rows.push({ kind: 'context', oldLine: oldLine++, newLine: newLine++, text }); + oldLeft--; + newLeft--; + } + } + return hunks; +} + +/** The `@@ -a,b +c,d @@` line for a hunk. */ +export function hunkHeader(hunk: Hunk): string { + const range = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`; + return hunk.section ? `${range} ${hunk.section}` : range; +} diff --git a/src/guide/pointer.ts b/src/guide/pointer.ts new file mode 100644 index 0000000..a13cb03 --- /dev/null +++ b/src/guide/pointer.ts @@ -0,0 +1,40 @@ +import { GuideError } from './errors.ts'; +import { isRepoRelative } from './prose.ts'; + +/** An inclusive range of line numbers, starting at 1. */ +export interface LineRange { + start: number; + end: number; +} + +/** A link to lines of a file at the guide's head commit, such as `packages/shaders/src/engine.ts#L70-L75`. */ +export interface Pointer { + path: string; + lines: LineRange; + /** Where the pointer sits in the guide, such as `steps.2.pointers.1`, for error messages. */ + field: string; +} + +const LINE_FRAGMENT = /^L(\d+)(?:-L(\d+))?$/; + +/** Parses a block pointer's link target. Throws a GuideError that names `field` when the target is malformed. */ +export function parseBlockPointer(href: string, field: string): Pointer { + if (!isRepoRelative(href)) { + throw new GuideError(field, `"${href}" is not a repo-relative path. A block pointer looks like [](src/file.ts#L10-L20).`); + } + const hash = href.indexOf('#'); + const path = decodeURI(hash === -1 ? href : href.slice(0, hash)); + const fragment = hash === -1 ? '' : href.slice(hash + 1); + if (path === '') throw new GuideError(field, `"${href}" names no file.`); + if (fragment === '') { + throw new GuideError(field, `"${href}" has no line range. Add one, such as #L10-L20 or #L10.`); + } + const match = LINE_FRAGMENT.exec(fragment); + if (!match) throw new GuideError(field, `"#${fragment}" is not a line range. Write #L10-L20 or #L10.`); + const start = Number(match[1]); + const end = match[2] === undefined ? start : Number(match[2]); + if (start < 1 || end < start) { + throw new GuideError(field, `"#${fragment}" is not a line range. Lines start at 1, and the range ends at or after its start.`); + } + return { path, lines: { start, end }, field }; +} diff --git a/src/guide/prose.ts b/src/guide/prose.ts new file mode 100644 index 0000000..cad4b66 --- /dev/null +++ b/src/guide/prose.ts @@ -0,0 +1,38 @@ +import type { Root, RootContent } from 'mdast'; +import rehypeSanitize from 'rehype-sanitize'; +import rehypeStringify from 'rehype-stringify'; +import remarkRehype from 'remark-rehype'; +import { unified } from 'unified'; +import { visit } from 'unist-util-visit'; + +/** + * HTML rendered from a guide's Markdown. Agents push guides without review, so raw HTML is dropped + * and the output passes through GitHub's sanitize schema, which strips links such as `javascript:` URLs. + */ +export type Html = string; + +export interface ProseContext { + /** owner/name of the repository the guide describes. */ + repo: string; + /** The commit that relative links resolve against. */ + headSha: string; +} + +const processor = unified().use(remarkRehype).use(rehypeSanitize).use(rehypeStringify); + +/** + * Renders guide prose to HTML. + * A relative link points into the repository the guide describes, so it becomes a GitHub link at the guide's head commit. + */ +export function renderProse(nodes: RootContent[], context: ProseContext): Html { + const tree: Root = { type: 'root', children: structuredClone(nodes) }; + visit(tree, 'link', (link) => { + if (isRepoRelative(link.url)) link.url = `https://github.com/${context.repo}/blob/${context.headSha}/${link.url}`; + }); + return processor.stringify(processor.runSync(tree)); +} + +/** True for a path such as `src/engine.ts#L1-L5`, false for a URL, an absolute path, or a same-page anchor. */ +export function isRepoRelative(url: string): boolean { + return !/^[a-z][a-z0-9+.-]*:/i.test(url) && !url.startsWith('/') && !url.startsWith('#'); +} diff --git a/src/guide/resolve.ts b/src/guide/resolve.ts new file mode 100644 index 0000000..e2e3310 --- /dev/null +++ b/src/guide/resolve.ts @@ -0,0 +1,101 @@ +import type { ChangedFile, FileStatus, PullRequestRef, RepoFileSource } from '../repo-files/index.ts'; +import { GuideError } from './errors.ts'; +import { cutHunks, touches } from './hunks.ts'; +import { parsePatch, type Hunk } from './patch.ts'; +import type { ParsedStep } from './parse.ts'; +import type { Pointer } from './pointer.ts'; + +/** One file in a step, with the hunks its block pointers cut from the diff. */ +export interface StepFile { + path: string; + /** The path at the base commit, which differs from `path` for a renamed file. */ + basePath: string; + status: FileStatus; + blobSha: string; + /** The file's text at the base commit, or null when it does not exist there. */ + base: string | null; + /** The file's text at the head commit, or null when it does not exist there. */ + head: string | null; + hunks: Hunk[]; +} + +/** Reads a pull request's files once per page and resolves each step's block pointers to hunks. */ +export class PullRequestFiles { + readonly pull: PullRequestRef; + readonly changed: ChangedFile[]; + readonly #source: RepoFileSource; + readonly #texts = new Map>(); + + private constructor(pull: PullRequestRef, source: RepoFileSource, changed: ChangedFile[]) { + this.pull = pull; + this.#source = source; + this.changed = changed; + } + + static async load(pull: PullRequestRef, source: RepoFileSource): Promise { + return new PullRequestFiles(pull, source, await source.listChangedFiles(pull)); + } + + /** The files a step's block pointers touch, in the order the step first points at them. */ + async stepFiles(step: ParsedStep): Promise { + const byPath = new Map(); + for (const pointer of step.pointers) byPath.set(pointer.path, [...(byPath.get(pointer.path) ?? []), pointer]); + return Promise.all([...byPath].map(([path, pointers]) => this.#stepFile(path, pointers))); + } + + async #stepFile(path: string, pointers: Pointer[]): Promise { + const [first] = pointers as [Pointer]; + const changed = this.changed.find((file) => file.path === path); + if (!changed) { + throw new GuideError(first.field, `${path} is not a file that ${this.#name()} changes.`); + } + if (changed.patch === undefined) { + throw new GuideError(first.field, `GitHub shows no text diff for ${path}, so a line range cannot point into it.`); + } + const basePath = changed.previousPath ?? path; + const [base, head] = await Promise.all([ + changed.status === 'added' ? null : this.#text(basePath, this.pull.baseSha), + changed.status === 'removed' ? null : this.#text(path, this.pull.headSha), + ]); + const lineCount = head === null ? 0 : countLines(head); + const hunks = parsePatch(changed.patch); + for (const pointer of pointers) { + const { start, end } = pointer.lines; + if (end > lineCount) { + throw new GuideError(pointer.field, `${path} has ${lineCount} lines at the head commit, so #L${start}-L${end} runs past its end.`); + } + if (!hunks.some((hunk) => touches(hunk, pointer.lines))) { + const covered = hunks.map((hunk) => `${hunk.newStart}-${hunk.newStart + hunk.newLines - 1}`).join(', '); + throw new GuideError(pointer.field, `lines ${start}-${end} of ${path} are outside the diff, which covers lines ${covered}.`); + } + } + return { + path, + basePath, + status: changed.status, + blobSha: changed.blobSha, + base, + head, + hunks: cutHunks(hunks, pointers.map((pointer) => pointer.lines)), + }; + } + + #text(path: string, commitSha: string): Promise { + const key = `${commitSha}:${path}`; + let text = this.#texts.get(key); + if (!text) { + text = this.#source.readFile(this.pull, path, commitSha).then((bytes) => (bytes === null ? null : new TextDecoder().decode(bytes))); + this.#texts.set(key, text); + } + return text; + } + + #name(): string { + return `${this.pull.owner}/${this.pull.repo}#${this.pull.pr}`; + } +} + +function countLines(text: string): number { + if (text === '') return 0; + return text.endsWith('\n') ? text.split('\n').length - 1 : text.split('\n').length; +} diff --git a/src/highlight.ts b/src/highlight.ts new file mode 100644 index 0000000..fdf353b --- /dev/null +++ b/src/highlight.ts @@ -0,0 +1,33 @@ +import { bundledLanguages, codeToTokens, type BundledLanguage, type ThemedToken } from 'shiki'; + +const THEMES = { light: 'github-light', dark: 'github-dark' } as const; + +/** Shiki's language for a path, picked by its extension. Unknown extensions render as plain text. */ +export function languageOf(path: string): BundledLanguage | 'text' { + const extension = path.split('/').at(-1)!.split('.').at(-1)!.toLowerCase(); + return extension in bundledLanguages ? (extension as BundledLanguage) : 'text'; +} + +const cache = new Map>(); + +/** + * Highlights a whole file once and returns its tokens per line, where index 0 is line 1. + * Highlighting the whole file keeps state such as an open comment right on every row a hunk shows. + */ +export function highlightLines(text: string, path: string): Promise { + const lang = languageOf(path); + const key = `${lang}\0${text}`; + let lines = cache.get(key); + if (!lines) { + lines = codeToTokens(text, { lang, themes: THEMES }).then((result) => result.tokens); + cache.set(key, lines); + } + return lines; +} + +/** A token's inline style, with the light color and the `--shiki-dark` color that the stylesheet swaps in. */ +export function tokenStyle(token: ThemedToken): string { + return Object.entries(token.htmlStyle ?? {}) + .map(([property, value]) => `${property}:${value}`) + .join(';'); +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..3a661b9 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,21 @@ +--- +import '../styles/global.css'; + +interface Props { + title: string; +} +const { title } = Astro.props; +--- + + + + + + + {title} + + + + + + diff --git a/src/pages/[repo]/[pr].astro b/src/pages/[repo]/[pr].astro new file mode 100644 index 0000000..9f41984 --- /dev/null +++ b/src/pages/[repo]/[pr].astro @@ -0,0 +1,15 @@ +--- +import { getCollection } from 'astro:content'; +import GuidePage from '../../components/GuidePage.astro'; +import { guideParams, loadGuide } from '../../guide/load.ts'; + +export async function getStaticPaths() { + const entries = await getCollection('guides'); + return entries.map((entry) => ({ params: guideParams(entry.data), props: { entry } })); +} + +const { entry } = Astro.props; +const guide = await loadGuide(entry); +--- + + diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..5fb70ea --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,138 @@ +:root { + color-scheme: light dark; + --bg: #ffffff; + --fg: #1f2328; + --muted: #59636e; + --border: #d1d9e0; + --surface: #f6f8fa; + --accent: #0969da; + --add-bg: #dafbe1; + --add-fg: #1a7f37; + --del-bg: #ffebe9; + --del-fg: #cf222e; + --hunk-bg: #ddf4ff; + --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --sans: system-ui, -apple-system, 'Segoe UI', sans-serif; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0d1117; + --fg: #e6edf3; + --muted: #9198a1; + --border: #3d444d; + --surface: #151b23; + --accent: #4493f8; + --add-bg: rgb(46 160 67 / 15%); + --add-fg: #3fb950; + --del-bg: rgb(248 81 73 / 10%); + --del-fg: #f85149; + --hunk-bg: rgb(56 139 253 / 10%); + } + + /* Shiki writes the light color inline and the dark one as --shiki-dark. */ + .code span, + .astro-code, + .astro-code span { + color: var(--shiki-dark) !important; + } + + .astro-code { + background-color: var(--shiki-dark-bg) !important; + } +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font: 16px/1.6 var(--sans); +} + +a { + color: var(--accent); +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +/* Prose: the overview, step explanations, and the format reference. */ + +.prose h2 { + margin: 0 0 12px; + font-size: 1.25rem; + line-height: 1.3; +} + +.prose h3 { + margin: 20px 0 4px; + font-size: 1rem; +} + +.prose p, +.prose ul, +.prose ol, +.prose table, +.prose pre { + margin: 0 0 12px; +} + +.prose code { + padding: 0.1em 0.3em; + border-radius: 4px; + background: var(--surface); + font: 0.875em var(--mono); +} + +.prose pre { + padding: 12px 16px; + overflow-x: auto; + border-radius: 6px; + font-size: 0.875rem; +} + +.prose pre code { + padding: 0; + background: none; +} + +.prose table { + border-collapse: collapse; + font-size: 0.9375rem; +} + +.prose th, +.prose td { + padding: 6px 10px; + border: 1px solid var(--border); + text-align: left; + vertical-align: top; +} + +.doc { + max-width: 780px; + margin: 0 auto; + padding: 32px; +} + +.doc h1 { + margin-top: 0; + line-height: 1.2; +} + +.doc h2 { + margin-top: 32px; +} diff --git a/tests/e2e/guide-page.spec.ts b/tests/e2e/guide-page.spec.ts new file mode 100644 index 0000000..34882f4 --- /dev/null +++ b/tests/e2e/guide-page.spec.ts @@ -0,0 +1,114 @@ +/** + * SHA-280: a guide file renders as a two-pane page. + * + * The specs read the fixture guide at tests/fixtures/guides/shaders/264.md. + * The build serves #264's files from the snapshot in tests/fixtures/repo-files, so nothing touches the network. + */ +import { expect, test } from './test.ts'; + +const GUIDE = '/guides/shaders/264/'; + +test('the right pane shows the overview before any click', async ({ page }) => { + await page.goto(GUIDE); + + await expect(page.getByRole('heading', { level: 1, name: 'feat(shaders): add FlutedGlass' })).toBeVisible(); + const explanation = page.getByRole('region', { name: 'Explanation' }); + await expect(explanation.getByRole('heading', { name: 'Overview' })).toBeVisible(); + for (const part of ['Why', 'Behavior change', 'Checks', 'Not covered']) { + await expect(explanation.getByRole('heading', { name: part, exact: true })).toBeVisible(); + } +}); + +test('the header links the pull request and names the commit the guide describes', async ({ page }) => { + await page.goto(GUIDE); + + const header = page.getByRole('banner'); + await expect(header.getByRole('link', { name: 'campdotdev/shaders#264' })).toHaveAttribute( + 'href', + 'https://github.com/campdotdev/shaders/pull/264', + ); + await expect(header.getByRole('link', { name: '84cc417' })).toHaveAttribute( + 'href', + 'https://github.com/campdotdev/shaders/commit/84cc41703117a566f6e181dd8aec5ec7ce889521', + ); + // The fixture sets afterTheFact: true. + await expect(header.getByText('Written after the fact from the diff, without the sessionโ€™s reasoning.')).toBeVisible(); +}); + +test('each step starts as a collapsed block in the left pane', async ({ page }) => { + await page.goto(GUIDE); + + const steps = page.getByRole('region', { name: 'Steps' }); + const toggles = steps.getByRole('button', { name: /^Step \d+: / }); + await expect(toggles).toHaveText([ + 'Step 1: Mount FlutedGlass after a Source', + 'Step 2: Shape each flute in one primitive', + ]); + for (const toggle of await toggles.all()) { + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + } + await expect(steps.getByRole('table')).toHaveCount(0); +}); + +test('clicking a step expands its hunks and shows its explanation', async ({ page }) => { + await page.goto(GUIDE); + const steps = page.getByRole('region', { name: 'Steps' }); + const explanation = page.getByRole('region', { name: 'Explanation' }); + const title = 'Step 2: Shape each flute in one primitive'; + + await steps.getByRole('button', { name: title }).click(); + + await expect(steps.getByRole('button', { name: title })).toHaveAttribute('aria-expanded', 'true'); + const step = steps.getByRole('article', { name: title }); + await expect(step.getByRole('table')).toHaveCount(2); + await expect(step.getByRole('table', { name: 'packages/shaders/src/primitives/flute-surface/flute-surface.ts' })).toBeVisible(); + await expect(step.getByRole('table', { name: 'packages/shaders/src/engine.ts' })).toBeVisible(); + await expect(explanation.getByRole('heading', { name: title })).toBeVisible(); + await expect(explanation.getByText(/works out how steep the glass is/)).toBeVisible(); + await expect(explanation.getByRole('heading', { name: 'Overview' })).toBeHidden(); + await expect(steps.getByRole('button', { name: /^Step 1: / })).toHaveAttribute('aria-expanded', 'false'); +}); + +test('a block pointer that covers a whole change shows GitHubโ€™s hunk', async ({ page }) => { + await page.goto(GUIDE); + await page.getByRole('button', { name: 'Step 2: Shape each flute in one primitive' }).click(); + + // engine.ts#L70-L75 covers every added line, so the hunk is the one GitHub shows for #264. + const engine = page.getByRole('table', { name: 'packages/shaders/src/engine.ts' }); + await expect(engine.getByRole('row', { name: /^@@ -67,6 \+67,12 @@/ })).toBeVisible(); + await expect( + engine.getByRole('row', { name: "70 added export { fluteSurface } from './primitives/flute-surface/flute-surface.js';" }), + ).toBeVisible(); +}); + +test('a block pointer shows its lines with three lines of context', async ({ page }) => { + await page.goto(GUIDE); + await page.getByRole('button', { name: 'Step 1: Mount FlutedGlass after a Source' }).click(); + + // fluted-glass.tsx is a new file and the pointer is #L91-L100, so the hunk runs from line 88 to line 103. + const component = page.getByRole('table', { name: 'packages/shaders/src/components/fluted-glass/fluted-glass.tsx' }); + await expect(component.getByRole('row', { name: /^@@ -0,0 \+88,16 @@/ })).toBeVisible(); + const lines = component.getByRole('row', { name: /^\d+ added/ }); + await expect(lines).toHaveCount(16); + await expect(lines.first()).toHaveAccessibleName(/^88 added/); + await expect(lines.last()).toHaveAccessibleName(/^103 added/); +}); + +test('opening another step closes the open one, and closing a step brings back the overview', async ({ page }) => { + await page.goto(GUIDE); + const steps = page.getByRole('region', { name: 'Steps' }); + const explanation = page.getByRole('region', { name: 'Explanation' }); + const first = steps.getByRole('button', { name: /^Step 1: / }); + const second = steps.getByRole('button', { name: /^Step 2: / }); + + await first.click(); + await second.click(); + await expect(first).toHaveAttribute('aria-expanded', 'false'); + await expect(explanation.getByRole('heading', { name: /^Step 1: / })).toBeHidden(); + await expect(explanation.getByRole('heading', { name: /^Step 2: / })).toBeVisible(); + + await second.click(); + await expect(second).toHaveAttribute('aria-expanded', 'false'); + await expect(explanation.getByRole('heading', { name: 'Overview' })).toBeVisible(); + await expect(explanation.getByRole('heading', { name: /^Step 2: / })).toBeHidden(); +}); diff --git a/tests/e2e/malformed-guide.spec.ts b/tests/e2e/malformed-guide.spec.ts new file mode 100644 index 0000000..9f6892f --- /dev/null +++ b/tests/e2e/malformed-guide.spec.ts @@ -0,0 +1,56 @@ +/** + * SHA-280: a malformed guide fails the build with an error that names the bad field. + * + * Each case builds the site from its own guides directory under tests/fixtures/malformed. + * The cases cover the three places a guide is checked: the frontmatter schema, the body's structure, + * and the pointers against the pull request's files. + * Each build writes to a temporary directory, so the dist/ that the preview server serves stays put. + */ +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { expect, test } from '@playwright/test'; +import { FIXTURE_BUILD_ENV } from '../fixture-build.ts'; + +function build(guidesDir: string): { status: number | null; output: string } { + const outDir = mkdtempSync(join(tmpdir(), 'guides-malformed-')); + try { + const result = spawnSync('pnpm', ['exec', 'astro', 'build', '--outDir', outDir], { + env: { ...process.env, ...FIXTURE_BUILD_ENV, GUIDES_DIR: guidesDir }, + encoding: 'utf8', + }); + // Playwright sets FORCE_COLOR, so strip the color codes Astro adds around field names. + const output = `${result.stdout}${result.stderr}`.replace(/\u001b\[[0-9;]*m/g, ''); + return { status: result.status, output }; + } finally { + rmSync(outDir, { recursive: true, force: true }); + } +} + +const cases = [ + { + guide: 'a headSha that is not a full commit SHA', + dir: 'short-head-sha', + error: /headSha: Expected a full 40-character lowercase commit SHA/, + }, + { + guide: 'a block pointer whose range ends before it starts', + dir: 'backward-range', + error: /Invalid guide shaders\/264\.md: steps\.1\.pointers\.2: "#L39-L37" is not a line range/, + }, + { + guide: 'a block pointer to a file the pull request does not change', + dir: 'unchanged-file', + error: /Invalid guide shaders\/264\.md: steps\.2\.pointers\.1: packages\/shaders\/src\/index\.ts is not a file that campdotdev\/shaders#264 changes/, + }, +]; + +for (const { guide, dir, error } of cases) { + test(`a guide with ${guide} fails the build and names the field`, () => { + test.setTimeout(60_000); + const result = build(`tests/fixtures/malformed/${dir}`); + expect(result.status).not.toBe(0); + expect(result.output).toMatch(error); + }); +} diff --git a/tests/e2e/test.ts b/tests/e2e/test.ts new file mode 100644 index 0000000..41291d9 --- /dev/null +++ b/tests/e2e/test.ts @@ -0,0 +1,22 @@ +import { expect, test as base } from '@playwright/test'; + +/** + * Every spec runs offline: the browser may reach only the preview server. + * A spec that needs a third-party response, such as the PR's head SHA, stubs it with `page.route`, + * which takes precedence over this context-wide route. + */ +export const test = base.extend<{ localOnly: void }>({ + localOnly: [ + async ({ context, baseURL }, use) => { + const origin = new URL(baseURL!).origin; + await context.route( + (url) => url.origin !== origin, + (route) => route.abort('internetdisconnected'), + ); + await use(); + }, + { auto: true }, + ], +}); + +export { expect }; diff --git a/tests/fixture-build.ts b/tests/fixture-build.ts new file mode 100644 index 0000000..67a8d15 --- /dev/null +++ b/tests/fixture-build.ts @@ -0,0 +1,7 @@ +/** The environment for a build against the test-owned fixture guides and the recorded #264 snapshot. */ +export const FIXTURE_BUILD_ENV = { + GUIDES_DIR: 'tests/fixtures/guides', + REPO_FILE_SOURCE: 'fixture', + REPO_FILE_FIXTURES: 'tests/fixtures/repo-files', + ASTRO_TELEMETRY_DISABLED: '1', +}; diff --git a/tests/fixtures/malformed/backward-range/shaders/264.md b/tests/fixtures/malformed/backward-range/shaders/264.md new file mode 100644 index 0000000..e47b5e3 --- /dev/null +++ b/tests/fixtures/malformed/backward-range/shaders/264.md @@ -0,0 +1,48 @@ +--- +repo: campdotdev/shaders +pr: 264 +title: "feat(shaders): add FlutedGlass" +scope: shaders +baseSha: 60b67b513ab1a21d2e33abc9dbf7b7b342c775aa +headSha: 84cc41703117a566f6e181dd8aec5ec7ce889521 +afterTheFact: true +--- + +## Overview + +### Why + +The homepage's Composable card stacks a Source, FlutedGlass, and Grain. Shaders had no reeded-glass Effect to put in the middle. + +### Behavior change + +`` is a new Effect. Mount it after a Source inside a ``, and it shows that Source through strips of glass. Each flute bends the image beneath it and catches the light along one wall. Mode 2 users get the same math as the `fluteSurface` primitive. + +### Checks + +- Unit tests for the component and for `fluteSurface`. +- A visual-regression baseline, `fluted-glass-default`, in `apps/docs-tests`. +- Five pixel checks against the stripe probe at `/dev/fluted-glass-probe`. +- A GPU budget run on `/dev/fluted-glass-benchmark`. FlutedGlass over MeshGradient adds 0.04 ms against a 0.2 ms budget. + +### Not covered + +A UV warp mounted before FlutedGlass, such as Dither, hands the glass a coordinate it already moved. SHA-267 changes how UV warps stack. + +## Mount FlutedGlass after a Source + +`FlutedGlass` is an Effect. It bends the image that the Sources before it drew, so it goes after them in a ``. The component owns the props and their defaults, then hands them to `FlutedGlassShader`. + +[](packages/shaders/src/components/fluted-glass/fluted-glass.tsx#L91-L100) + +The package index exports the component and its prop types. + +[](packages/shaders/src/components/index.ts#L39-L37) + +## Shape each flute in one primitive + +`fluteSurface` works out how steep the glass is and how far it moves the image, in one call. The light reads the same slope, so it lands where the image bends. The engine exports the primitive for Mode 2. + +[](packages/shaders/src/primitives/flute-surface/flute-surface.ts#L108-L115) + +[](packages/shaders/src/engine.ts#L70-L75) diff --git a/tests/fixtures/malformed/short-head-sha/shaders/264.md b/tests/fixtures/malformed/short-head-sha/shaders/264.md new file mode 100644 index 0000000..bacde11 --- /dev/null +++ b/tests/fixtures/malformed/short-head-sha/shaders/264.md @@ -0,0 +1,48 @@ +--- +repo: campdotdev/shaders +pr: 264 +title: "feat(shaders): add FlutedGlass" +scope: shaders +baseSha: 60b67b513ab1a21d2e33abc9dbf7b7b342c775aa +headSha: 84cc417 +afterTheFact: true +--- + +## Overview + +### Why + +The homepage's Composable card stacks a Source, FlutedGlass, and Grain. Shaders had no reeded-glass Effect to put in the middle. + +### Behavior change + +`` is a new Effect. Mount it after a Source inside a ``, and it shows that Source through strips of glass. Each flute bends the image beneath it and catches the light along one wall. Mode 2 users get the same math as the `fluteSurface` primitive. + +### Checks + +- Unit tests for the component and for `fluteSurface`. +- A visual-regression baseline, `fluted-glass-default`, in `apps/docs-tests`. +- Five pixel checks against the stripe probe at `/dev/fluted-glass-probe`. +- A GPU budget run on `/dev/fluted-glass-benchmark`. FlutedGlass over MeshGradient adds 0.04 ms against a 0.2 ms budget. + +### Not covered + +A UV warp mounted before FlutedGlass, such as Dither, hands the glass a coordinate it already moved. SHA-267 changes how UV warps stack. + +## Mount FlutedGlass after a Source + +`FlutedGlass` is an Effect. It bends the image that the Sources before it drew, so it goes after them in a ``. The component owns the props and their defaults, then hands them to `FlutedGlassShader`. + +[](packages/shaders/src/components/fluted-glass/fluted-glass.tsx#L91-L100) + +The package index exports the component and its prop types. + +[](packages/shaders/src/components/index.ts#L37-L39) + +## Shape each flute in one primitive + +`fluteSurface` works out how steep the glass is and how far it moves the image, in one call. The light reads the same slope, so it lands where the image bends. The engine exports the primitive for Mode 2. + +[](packages/shaders/src/primitives/flute-surface/flute-surface.ts#L108-L115) + +[](packages/shaders/src/engine.ts#L70-L75) diff --git a/tests/fixtures/malformed/unchanged-file/shaders/264.md b/tests/fixtures/malformed/unchanged-file/shaders/264.md new file mode 100644 index 0000000..3bc64c3 --- /dev/null +++ b/tests/fixtures/malformed/unchanged-file/shaders/264.md @@ -0,0 +1,48 @@ +--- +repo: campdotdev/shaders +pr: 264 +title: "feat(shaders): add FlutedGlass" +scope: shaders +baseSha: 60b67b513ab1a21d2e33abc9dbf7b7b342c775aa +headSha: 84cc41703117a566f6e181dd8aec5ec7ce889521 +afterTheFact: true +--- + +## Overview + +### Why + +The homepage's Composable card stacks a Source, FlutedGlass, and Grain. Shaders had no reeded-glass Effect to put in the middle. + +### Behavior change + +`` is a new Effect. Mount it after a Source inside a ``, and it shows that Source through strips of glass. Each flute bends the image beneath it and catches the light along one wall. Mode 2 users get the same math as the `fluteSurface` primitive. + +### Checks + +- Unit tests for the component and for `fluteSurface`. +- A visual-regression baseline, `fluted-glass-default`, in `apps/docs-tests`. +- Five pixel checks against the stripe probe at `/dev/fluted-glass-probe`. +- A GPU budget run on `/dev/fluted-glass-benchmark`. FlutedGlass over MeshGradient adds 0.04 ms against a 0.2 ms budget. + +### Not covered + +A UV warp mounted before FlutedGlass, such as Dither, hands the glass a coordinate it already moved. SHA-267 changes how UV warps stack. + +## Mount FlutedGlass after a Source + +`FlutedGlass` is an Effect. It bends the image that the Sources before it drew, so it goes after them in a ``. The component owns the props and their defaults, then hands them to `FlutedGlassShader`. + +[](packages/shaders/src/components/fluted-glass/fluted-glass.tsx#L91-L100) + +The package index exports the component and its prop types. + +[](packages/shaders/src/components/index.ts#L37-L39) + +## Shape each flute in one primitive + +`fluteSurface` works out how steep the glass is and how far it moves the image, in one call. The light reads the same slope, so it lands where the image bends. The engine exports the primitive for Mode 2. + +[](packages/shaders/src/index.ts#L1-L3) + +[](packages/shaders/src/engine.ts#L70-L75) From 02fa7882855f95127ab61fed539974abd462e2ab Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Wed, 28 Oct 2026 23:44:29 -0400 Subject: [PATCH 04/58] docs: add the guide format reference at /guides/format/ `docs/guide-format.md` documents the frontmatter, the overview, steps, block pointers, and the build errors. The `pr-guide` skill reads it as raw Markdown, and the site renders the same file at `/guides/format/`. Co-Authored-By: Claude Opus 5.5 --- docs/guide-format.md | 155 +++++++++++++++++++++++++++++ src/pages/format.astro | 11 ++ tests/e2e/format-reference.spec.ts | 18 ++++ 3 files changed, 184 insertions(+) create mode 100644 docs/guide-format.md create mode 100644 src/pages/format.astro create mode 100644 tests/e2e/format-reference.spec.ts diff --git a/docs/guide-format.md b/docs/guide-format.md new file mode 100644 index 0000000..836d21d --- /dev/null +++ b/docs/guide-format.md @@ -0,0 +1,155 @@ +# Guide file format + +A guide file walks through one pull request in steps. It is Markdown with YAML frontmatter, and it holds prose and pointers, never pasted code. The renderer reads the code for each pointer from GitHub at the commits the frontmatter names, so a guide always shows the code that the pull request changed. + +## File location + +Guides live on the `content` branch of `campdotdev/guides`, at `/.md`. The guide to `campdotdev/shaders#264` is `shaders/264.md`, and its page is `https://campdotdev.github.io/guides/shaders/264/`. + +Markdown files at the top level of the branch, such as `README.md`, are not guides. + +## Frontmatter + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `repo` | string | Yes | The repository, as `owner/name`. The name is the folder the file sits in. | +| `pr` | positive integer | Yes | The pull request number. The file's name is this number plus `.md`. | +| `title` | string | Yes | The pull request title, which is a Conventional Commit. The page shows it as its heading. | +| `scope` | string | No | The Conventional Commit scope of the title. | +| `baseSha` | commit SHA | Yes | The commit the pull request is based on. | +| `headSha` | commit SHA | Yes | The head commit the guide describes. Pointer line numbers refer to this commit. | +| `afterTheFact` | boolean | No | `true` when the guide was written from the diff after the session ended. The page then says it lacks the session's reasoning. Defaults to `false`. | + +A commit SHA is the full 40-character lowercase hex SHA. A field the table does not list fails the build. + +```yaml +--- +repo: campdotdev/shaders +pr: 264 +title: "feat(shaders): add FlutedGlass" +scope: shaders +baseSha: 60b67b513ab1a21d2e33abc9dbf7b7b342c775aa +headSha: 84cc41703117a566f6e181dd8aec5ec7ce889521 +afterTheFact: true +--- +``` + +## Body + +The body is a list of second-level sections. `## Overview` comes first, and every section after it is a step. The body has no text before `## Overview` and no first-level heading. + +Raw HTML does not render. + +## Overview + +The `## Overview` section holds four parts, each a third-level heading with text under it, in this order: + +| Heading | Holds | +| --- | --- | +| `### Why` | Why the change exists. | +| `### Behavior change` | What a user or caller sees differently after the change. | +| `### Checks` | The checks the agent ran, and what they showed. | +| `### Not covered` | What the pull request leaves out. | + +The overview does not list what changed. The steps do that. The overview has no block pointers. + +The page shows the overview in the right pane until the reader opens a step. + +## Steps + +Each second-level section after the overview is a step, and its heading is the step's title. The page numbers the steps from 1, in file order. + +A step holds two things: + +- Prose that explains the step. The right pane shows it when the reader opens the step. +- One or more block pointers. The left pane shows their hunks when the reader opens the step. + +The prose and the block pointers can sit in any order. The left pane groups a step's hunks by file, in the order the step first points at each file. A step can use third-level headings in its prose. + +## Block pointers + +A block pointer is a Markdown link with no text, alone on its line: + +```markdown +[](packages/shaders/src/engine.ts#L70-L75) +``` + +The link target has two parts: + +- The path, relative to the repository root, with no leading `/` or `./`. The file must be one the pull request changes between `baseSha` and `headSha`, and GitHub must show a text diff for it. +- The line range, as `#L-L` or `#L`. The lines are numbered in the file at `headSha`, and the range includes both ends. It must lie inside the file and overlap the diff. + +The page shows the diff rows from three lines before the range to three lines after it. The rows come from GitHub's hunks for the file, so they match the pull request's diff on GitHub. The page shows no rows outside those hunks. Two pointers into one file in the same step share one table, and rows they both show appear once. + +A block pointer cannot name deleted lines. To show a deletion, point at the lines around it. + +## Inline pointers and links + +A link with text and a repository-relative target renders as a link to that path on GitHub at `headSha`. When the target has a line range, such as `[the export](packages/shaders/src/engine.ts#L70)`, the link is an inline pointer, and it links to those lines. + +Any other link, such as a link to a glossary entry or an ADR, renders as an ordinary link. + +A link with no text fails the build unless it is a block pointer alone on its line. + +## Build errors + +The build fails on a malformed guide. The error names the file, by its path on the `content` branch, and the field: + +```text +Invalid guide shaders/264.md: steps.2.pointers.1: packages/shaders/src/index.ts is not a file that campdotdev/shaders#264 changes. +``` + +A frontmatter error names the field the same way, such as `headSha: Expected a full 40-character lowercase commit SHA`. + +| Field | Where the problem is | +| --- | --- | +| A frontmatter field, such as `headSha` | The frontmatter. | +| `pr` | The file's location does not match `repo` and `pr`. | +| `body` | The text before `## Overview`, or a first-level heading. | +| `overview` | The overview's parts. | +| `steps` | The guide has no steps. | +| `steps.` | Step `n`'s title or prose. | +| `steps..pointers` | Step `n` has no block pointer. | +| `steps..pointers.` | Block pointer `m` in step `n`. | + +Step and pointer numbers start at 1, like the step numbers on the page. + +## Example + +```markdown +--- +repo: campdotdev/shaders +pr: 264 +title: "feat(shaders): add FlutedGlass" +scope: shaders +baseSha: 60b67b513ab1a21d2e33abc9dbf7b7b342c775aa +headSha: 84cc41703117a566f6e181dd8aec5ec7ce889521 +afterTheFact: true +--- + +## Overview + +### Why + +The homepage's Composable card stacks a Source, FlutedGlass, and Grain. Shaders had no reeded-glass Effect to put in the middle. + +### Behavior change + +`` is a new Effect. Mount it after a Source inside a ``. + +### Checks + +- Unit tests for the component and for `fluteSurface`. + +### Not covered + +A UV warp mounted before FlutedGlass hands the glass a coordinate it already moved. + +## Shape each flute in one primitive + +`fluteSurface` works out how steep the glass is and how far it moves the image, in one call. The engine exports it for Mode 2. + +[](packages/shaders/src/primitives/flute-surface/flute-surface.ts#L108-L115) + +[](packages/shaders/src/engine.ts#L70-L75) +``` diff --git a/src/pages/format.astro b/src/pages/format.astro new file mode 100644 index 0000000..a46c8ed --- /dev/null +++ b/src/pages/format.astro @@ -0,0 +1,11 @@ +--- +// The format reference lives in docs/guide-format.md, so it reads as raw Markdown in the repository too. +import { Content } from '../../docs/guide-format.md'; +import BaseLayout from '../layouts/BaseLayout.astro'; +--- + + +
+ +
+
diff --git a/tests/e2e/format-reference.spec.ts b/tests/e2e/format-reference.spec.ts new file mode 100644 index 0000000..6e2c1d6 --- /dev/null +++ b/tests/e2e/format-reference.spec.ts @@ -0,0 +1,18 @@ +/** + * SHA-280: the guide format reference renders on the site. + * The page's source is docs/guide-format.md, which the pr-guide skill reads as raw Markdown. + */ +import { expect, test } from './test.ts'; + +test('the format reference documents the frontmatter, the overview, steps, and block pointers', async ({ page }) => { + await page.goto('/guides/format/'); + + await expect(page.getByRole('heading', { level: 1, name: 'Guide file format' })).toBeVisible(); + for (const section of ['Frontmatter', 'Overview', 'Steps', 'Block pointers']) { + await expect(page.getByRole('heading', { level: 2, name: section, exact: true })).toBeVisible(); + } + const frontmatter = page.getByRole('table').filter({ has: page.getByRole('columnheader', { name: 'Required' }) }); + for (const field of ['repo', 'pr', 'title', 'scope', 'baseSha', 'headSha', 'afterTheFact']) { + await expect(frontmatter.getByRole('cell', { name: field, exact: true })).toBeVisible(); + } +}); From dd5b62c443d3b94280716f2e75f6c4102ca982b5 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Wed, 28 Oct 2026 23:44:29 -0400 Subject: [PATCH 05/58] ci: deploy the site to Pages and run the specs on pull requests `deploy.yml` runs on pushes to `main` and on `workflow_dispatch`. It checks out `content` beside the renderer, builds with the `github` source, and deploys to Pages. Only `main` may deploy to the `github-pages` environment, so `content` dispatches this workflow instead of deploying itself. `ci.yml` type-checks and runs the Playwright specs on pull requests to `main`. The specs use the fixture source, so CI needs no token. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 38 +++++++++++++++++++++++ .github/workflows/deploy.yml | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5720c65 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +# Type-checks the renderer and runs the Playwright specs against the fixture guides. +# The specs build the site from the recorded snapshot in tests/fixtures, so they need no network. +name: CI + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Typecheck and Playwright + runs-on: ubuntu-latest + env: + ASTRO_TELEMETRY_DISABLED: '1' + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm check + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm test + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: playwright-results + path: test-results + retention-days: 7 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..bfd7054 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,59 @@ +# Builds main's renderer with the guides on the content branch and deploys the site to GitHub Pages. +# A push to content runs that branch's trigger-deploy.yml, which dispatches this workflow on main. +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +# Each job grants only the permissions it needs. +permissions: {} + +# One deploy at a time. A newer run waits for the current one instead of canceling it. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + - name: Check out the guides from the content branch + uses: actions/checkout@v7 + with: + ref: content + path: content + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Build the site + run: pnpm build + env: + GUIDES_DIR: content + REPO_FILE_SOURCE: github + GITHUB_TOKEN: ${{ github.token }} + ASTRO_TELEMETRY_DISABLED: '1' + - uses: actions/configure-pages@v6 + - uses: actions/upload-pages-artifact@v5 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 From 0abcdbc866cb25dd887176791c480b3c113ef2dc Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Wed, 28 Oct 2026 23:44:29 -0400 Subject: [PATCH 06/58] docs: describe setup, env vars, tests, and deploys in the README Co-Authored-By: Claude Opus 5.5 --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9506e14..374188b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,75 @@ # guides -Step-by-step guides to campdotdev pull requests, written by the agent that wrote each one. + +Step-by-step guides to campdotdev pull requests, written by the agent that wrote each one. The site is https://campdotdev.github.io/guides/. + +The repository has two branches with different jobs: + +- `main` holds the renderer, an Astro site. Changes reach it through pull requests. +- `content` holds the guide files, one per pull request at `/.md`. Agents push to it directly. + +[Guide file format](docs/guide-format.md) documents the guide file. The site serves the same page at `/guides/format/`. + +## Run the site locally + +You need Node 24 and pnpm 11. + +```sh +pnpm install +pnpm exec playwright install chromium # once, for pnpm test +pnpm dev # serves the fixture guide at http://localhost:4321/guides/shaders/264/ +pnpm build # writes the static site to dist/ +pnpm check # type-checks the project +pnpm test # builds the site from the fixtures, serves it, and runs the Playwright specs +``` + +If another server already holds port 4321, pick another port for the tests, such as `PLAYWRIGHT_PORT=4400 pnpm test`. + +To build the published guides, check out `content` into `content/` and read the pull requests' files from GitHub: + +```sh +git worktree add content content +GUIDES_DIR=content REPO_FILE_SOURCE=github GITHUB_TOKEN=$(gh auth token) pnpm build +``` + +## Environment variables + +| Variable | Default | Effect | +| --- | --- | --- | +| `GUIDES_DIR` | `tests/fixtures/guides` | The directory the build reads guides from. | +| `REPO_FILE_SOURCE` | `fixture` | Where the build reads the pull requests' files. `github` reads the GitHub API, and `fixture` reads a recorded snapshot. | +| `GITHUB_TOKEN` | None | The token the `github` source sends. `REPO_FILE_SOURCE=github` requires it. | +| `REPO_FILE_FIXTURES` | `tests/fixtures/repo-files` | The snapshot the `fixture` source reads. | +| `PLAYWRIGHT_PORT` | `4321` | The port that `pnpm test` serves the build on. | + +## Tests + +The specs in `tests/e2e` drive the built site in Chromium and assert through roles and accessible names. They read the fixture guides in `tests/fixtures/guides`, which the test suite owns, so the sample guide on `content` can change without breaking them. Each guide in `tests/fixtures/malformed` must fail the build with an error that names the bad field. + +The `fixture` source serves each fixture guide's files from `tests/fixtures/repo-files`, a snapshot of the pull request at its base and head commits. The tests never touch the network. To record the snapshot for a new fixture guide, run: + +```sh +GITHUB_TOKEN=$(gh auth token) pnpm record-fixture tests/fixtures/guides//.md +``` + +The script reads the files through the production `github` source and checks each one against the blob SHA that GitHub lists. + +## Deploys + +`.github/workflows/deploy.yml` builds `main`'s renderer with the guides on `content` and deploys the site to GitHub Pages. It runs on every push to `main`. Only `main` may deploy to the `github-pages` environment, so a push to `content` runs that branch's `trigger-deploy.yml`, which dispatches `deploy.yml` on `main`. + +A malformed guide on `content` fails the deploy build, and the site keeps its last good deployment. + +`.github/workflows/ci.yml` type-checks the project and runs the Playwright specs on every pull request to `main`. + +## Code layout + +| Path | Holds | +| --- | --- | +| `src/content.config.ts` | The `guides` collection, read from `GUIDES_DIR`. | +| `src/guide/` | The guide model. It parses and validates a guide file and resolves its pointers to diff hunks. Components never parse Markdown. | +| `src/repo-files/` | The repo file source: its interface, the `github` and `fixture` sources, and the switch between them. | +| `src/components/` | The page components. | +| `src/client/` | Browser behavior, one module per feature. | +| `src/highlight.ts` | Syntax highlighting with Shiki, one token list per line. | +| `docs/guide-format.md` | The guide format reference. | +| `scripts/record-fixture.ts` | The script that records a fixture snapshot. | From 3018297bf849b68c63d4f248eab0aa64aed3489a Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Thu, 29 Oct 2026 00:01:59 -0400 Subject: [PATCH 07/58] feat: link to a step with its #step- anchor Opening a guide at `#step-` selects that step. Selecting a step any other way writes its anchor to the URL with `history.replaceState`, so the address bar links to the current step and a click adds no history entry. A change to the anchor, such as Back after following a step link, selects the step it names. An anchor to a step that the guide doesn't have leaves the overview showing. Co-Authored-By: Claude Opus 5.5 --- src/client/step-links.ts | 36 ++++++++++++++++ src/components/StepBlock.astro | 6 +++ tests/e2e/step-navigation.spec.ts | 69 +++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 src/client/step-links.ts create mode 100644 tests/e2e/step-navigation.spec.ts diff --git a/src/client/step-links.ts b/src/client/step-links.ts new file mode 100644 index 0000000..0d1601a --- /dev/null +++ b/src/client/step-links.ts @@ -0,0 +1,36 @@ +/** + * Step links: every step has an anchor, `#step-`. + * Opening a guide at a step's anchor selects that step. + * Selecting a step any other way writes its anchor to the URL, so the address bar always links to the current step. + */ +import { STEP_SELECTED, type StepSelectedDetail, selectStep } from './step-selection.ts'; + +/** The step an anchor such as `#step-2` names, or null when the anchor names no step on this page. */ +function stepInHash(hash: string): number | null { + const match = /^#step-(\d+)$/.exec(hash); + if (!match || !document.querySelector(`[data-step="${match[1]}"]`)) return null; + return Number(match[1]); +} + +/** Replaces the URL's anchor without a history entry or a scroll. */ +function writeHash(step: number | null): void { + const url = new URL(location.href); + url.hash = step === null ? '' : `step-${step}`; + if (url.href !== location.href) history.replaceState(history.state, '', url); +} + +export function initStepLinks(): void { + const step = stepInHash(location.hash); + if (step !== null) selectStep(step); + + // A link on the page, an edit in the address bar, or Back and Forward. An empty anchor means the overview. + // Any other anchor, such as a heading's, leaves the selection alone. + window.addEventListener('hashchange', () => { + const step = stepInHash(location.hash); + if (step !== null || location.hash === '') selectStep(step); + }); + + document.addEventListener(STEP_SELECTED, (event) => { + writeHash((event as CustomEvent).detail.step); + }); +} diff --git a/src/components/StepBlock.astro b/src/components/StepBlock.astro index 73a676a..834b3ae 100644 --- a/src/components/StepBlock.astro +++ b/src/components/StepBlock.astro @@ -40,6 +40,12 @@ const fileCount = `${step.files.length} ${step.files.length === 1 ? 'file' : 'fi initStepSelection(); + + diff --git a/src/components/ScopeFilter.astro b/src/components/ScopeFilter.astro new file mode 100644 index 0000000..3938a2a --- /dev/null +++ b/src/components/ScopeFilter.astro @@ -0,0 +1,46 @@ +--- +interface Props { + /** The scopes to offer, in display order. */ + scopes: string[]; + /** The id of the list the filter narrows. */ + list: string; +} +const { scopes, list } = Astro.props; +--- + + + + + + diff --git a/src/guide/catalog.ts b/src/guide/catalog.ts new file mode 100644 index 0000000..320c807 --- /dev/null +++ b/src/guide/catalog.ts @@ -0,0 +1,44 @@ +import type { CollectionEntry } from 'astro:content'; +import { guideParams } from './load.ts'; +import type { GuideFrontmatter } from './schema.ts'; + +/** One repo's guides, for the home page and the repo's page. */ +export interface RepoGuides { + /** The repo as `owner/name`, such as `campdotdev/shaders`. */ + repo: string; + /** The repo's route segment, such as `shaders`. */ + slug: string; + /** Newest first, by pull request number. */ + guides: GuideFrontmatter[]; + /** The scopes the repo's guides use, in name order. */ + scopes: string[]; +} + +/** Groups the guides by repo. Repos sort by name, and each repo's guides run newest first. */ +export function guidesByRepo(entries: CollectionEntry<'guides'>[]): RepoGuides[] { + const repos = new Map(); + for (const { data } of entries) { + const slug = guideParams(data).repo; + const group = repos.get(slug) ?? { repo: data.repo, slug, guides: [], scopes: [] }; + group.guides.push(data); + repos.set(slug, group); + } + for (const group of repos.values()) { + group.guides.sort((a, b) => b.pr - a.pr); + group.scopes = [...new Set(group.guides.flatMap((guide) => guide.scope ?? []))].sort(); + } + return [...repos.values()].sort((a, b) => a.repo.localeCompare(b.repo)); +} + +const BASE = import.meta.env.BASE_URL.replace(/\/$/, ''); + +/** The URL of a repo's page, such as `/guides/shaders/`. */ +export function repoUrl(slug: string): string { + return `${BASE}/${slug}/`; +} + +/** The URL of a guide's page, such as `/guides/shaders/264/`. */ +export function guideUrl(meta: GuideFrontmatter): string { + const { repo, pr } = guideParams(meta); + return `${BASE}/${repo}/${pr}/`; +} diff --git a/src/pages/[repo]/index.astro b/src/pages/[repo]/index.astro new file mode 100644 index 0000000..1d738b2 --- /dev/null +++ b/src/pages/[repo]/index.astro @@ -0,0 +1,26 @@ +--- +import { getCollection } from 'astro:content'; +import GuideList from '../../components/GuideList.astro'; +import ScopeFilter from '../../components/ScopeFilter.astro'; +import { guidesByRepo, type RepoGuides } from '../../guide/catalog.ts'; +import BaseLayout from '../../layouts/BaseLayout.astro'; + +export async function getStaticPaths() { + const repos = guidesByRepo(await getCollection('guides')); + return repos.map((group) => ({ params: { repo: group.slug }, props: { group } })); +} + +interface Props { + group: RepoGuides; +} +const { group } = Astro.props; +--- + + +
+

{group.repo}

+ + {group.scopes.length > 0 && } + +
+
diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..c1c3e36 --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,24 @@ +--- +import { getCollection } from 'astro:content'; +import { guidesByRepo, repoUrl } from '../guide/catalog.ts'; +import BaseLayout from '../layouts/BaseLayout.astro'; + +const repos = guidesByRepo(await getCollection('guides')); +--- + + +
+

Guides

+

Step-by-step guides to campdotdev pull requests, written by the agent that wrote each one.

+

Repositories

+
    + { + repos.map((group) => ( +
  • + {group.repo} +
  • + )) + } +
+
+
diff --git a/tests/e2e/index.spec.ts b/tests/e2e/index.spec.ts new file mode 100644 index 0000000..9b1d629 --- /dev/null +++ b/tests/e2e/index.spec.ts @@ -0,0 +1,53 @@ +/** + * SHA-286: the home page lists each repo that has guides, and each repo page lists its guides newest first. + * + * The specs name the fixture guides shaders/264.md (scope `shaders`), shaders/241.md (scope `docs`), and bones/23.md (no scope). + * Other specs add fixture guides too, so these specs check the guides they name and the order, never the total count. + */ +import { expect, test } from './test.ts'; + +test('the home page lists each repo that has guides and links to its page', async ({ page }) => { + await page.goto('/guides/'); + + await expect(page.getByRole('heading', { level: 1, name: 'Guides' })).toBeVisible(); + const repos = page.getByRole('list', { name: 'Repositories' }); + await expect(repos.getByRole('link', { name: 'campdotdev/bones' })).toBeVisible(); + + await repos.getByRole('link', { name: 'campdotdev/shaders' }).click(); + + await expect(page).toHaveURL('/guides/shaders/'); + await expect(page.getByRole('heading', { level: 1, name: 'campdotdev/shaders' })).toBeVisible(); +}); + +test('a repo page lists its guides newest first, each with its title, number, and scope', async ({ page }) => { + await page.goto('/guides/shaders/'); + + const guides = page.getByRole('list', { name: 'Guides' }).getByRole('listitem'); + const flutedGlass = guides.filter({ has: page.getByRole('link', { name: 'feat(shaders): add FlutedGlass' }) }); + await expect(flutedGlass).toContainText('#264'); + await expect(flutedGlass).toContainText('Scope: shaders'); + const posterSizes = guides.filter({ + has: page.getByRole('link', { name: 'refactor(docs): remove POSTER_SIZES, which does nothing while images are unoptimized' }), + }); + await expect(posterSizes).toContainText('#241'); + await expect(posterSizes).toContainText('Scope: docs'); + + const numbers = (await guides.getByText(/^#\d+$/).allTextContents()).map((text) => Number(text.slice(1))); + expect(numbers.indexOf(264)).toBeLessThan(numbers.indexOf(241)); + expect(numbers).toEqual(numbers.toSorted((a, b) => b - a)); + + await flutedGlass.getByRole('link').click(); + await expect(page).toHaveURL('/guides/shaders/264/'); + await expect(page.getByRole('heading', { level: 1, name: 'feat(shaders): add FlutedGlass' })).toBeVisible(); +}); + +test('a guide without a scope lists no scope', async ({ page }) => { + await page.goto('/guides/bones/'); + + const guide = page + .getByRole('list', { name: 'Guides' }) + .getByRole('listitem') + .filter({ has: page.getByRole('link', { name: 'fix: build bones before demo to fix Vercel build' }) }); + await expect(guide).toContainText('#23'); + await expect(guide).not.toContainText('Scope:'); +}); diff --git a/tests/e2e/scope-filter.spec.ts b/tests/e2e/scope-filter.spec.ts new file mode 100644 index 0000000..59309e5 --- /dev/null +++ b/tests/e2e/scope-filter.spec.ts @@ -0,0 +1,28 @@ +/** + * SHA-286: the scope filter narrows a repo page to the guides with one Conventional Commit scope. + * + * The specs name the fixture guides shaders/264.md (scope `shaders`) and shaders/241.md (scope `docs`). + * Other specs add fixture guides too, so these specs check the guides they name, never the total count. + */ +import { expect, test } from './test.ts'; + +test('the scope filter shows only the guides with the chosen scope', async ({ page }) => { + await page.goto('/guides/shaders/'); + const scope = page.getByRole('combobox', { name: 'Scope' }); + const guides = page.getByRole('list', { name: 'Guides' }).getByRole('listitem'); + const flutedGlass = guides.filter({ has: page.getByRole('link', { name: 'feat(shaders): add FlutedGlass' }) }); + const posterSizes = guides.filter({ hasText: '#241' }); + await expect(flutedGlass).toBeVisible(); + await expect(posterSizes).toBeVisible(); + + await scope.selectOption('docs'); + + await expect(flutedGlass).toBeHidden(); + await expect(posterSizes).toBeVisible(); + for (const guide of await guides.all()) await expect(guide).toContainText('Scope: docs'); + + await scope.selectOption({ label: 'All scopes' }); + + await expect(flutedGlass).toBeVisible(); + await expect(posterSizes).toBeVisible(); +}); From ecbefbe1f4d5275a359420b3dbd82e6a30fc4299 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Thu, 29 Oct 2026 00:07:07 -0400 Subject: [PATCH 14/58] feat: search the prose of every guide with Pagefind `pnpm build` now runs `pagefind --site dist` after `astro build`, so the deploy and CI workflows build the index with no changes. The guide page marks its explanation pane with `data-pagefind-body`, so the index holds the overview and the steps' prose but not the diffs. Pages without the marker, such as the index pages, stay out of the index. The home page's search box loads the Pagefind bundle from under the `/guides` base on first focus and lists the matching guides with an excerpt. The search specs query the real index from the built site. Co-Authored-By: Claude Opus 5.5 --- package.json | 3 +- pnpm-lock.yaml | 73 ++++++++++++++++++++++++++ src/client/search.ts | 88 ++++++++++++++++++++++++++++++++ src/components/GuidePage.astro | 3 +- src/components/GuideSearch.astro | 77 ++++++++++++++++++++++++++++ src/pages/index.astro | 2 + tests/e2e/search.spec.ts | 48 +++++++++++++++++ 7 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 src/client/search.ts create mode 100644 src/components/GuideSearch.astro create mode 100644 tests/e2e/search.spec.ts diff --git a/package.json b/package.json index 99fa601..b41ef6d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "scripts": { "dev": "astro dev", - "build": "astro build", + "build": "astro build && pagefind --site dist", "preview": "astro preview", "check": "astro check", "test": "playwright test", @@ -17,6 +17,7 @@ "dependencies": { "astro": "7.4.4", "mdast-util-to-string": "^4.0.0", + "pagefind": "1.5.2", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 724911a..a511a8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: mdast-util-to-string: specifier: ^4.0.0 version: 4.0.0 + pagefind: + specifier: 1.5.2 + version: 1.5.2 rehype-sanitize: specifier: ^6.0.0 version: 6.0.0 @@ -485,6 +488,41 @@ packages: '@oxc-project/types@0.155.0': resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} + '@pagefind/darwin-arm64@1.5.2': + resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} + cpu: [arm64] + os: [darwin] + + '@pagefind/darwin-x64@1.5.2': + resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==} + cpu: [x64] + os: [darwin] + + '@pagefind/freebsd-x64@1.5.2': + resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==} + cpu: [x64] + os: [freebsd] + + '@pagefind/linux-arm64@1.5.2': + resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==} + cpu: [arm64] + os: [linux] + + '@pagefind/linux-x64@1.5.2': + resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==} + cpu: [x64] + os: [linux] + + '@pagefind/windows-arm64@1.5.2': + resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==} + cpu: [arm64] + os: [win32] + + '@pagefind/windows-x64@1.5.2': + resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==} + cpu: [x64] + os: [win32] + '@playwright/test@1.59.1': resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} @@ -1432,6 +1470,10 @@ packages: package-manager-detector@1.9.0: resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + pagefind@1.5.2: + resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} + hasBin: true + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -2455,6 +2497,27 @@ snapshots: '@oxc-project/types@0.155.0': {} + '@pagefind/darwin-arm64@1.5.2': + optional: true + + '@pagefind/darwin-x64@1.5.2': + optional: true + + '@pagefind/freebsd-x64@1.5.2': + optional: true + + '@pagefind/linux-arm64@1.5.2': + optional: true + + '@pagefind/linux-x64@1.5.2': + optional: true + + '@pagefind/windows-arm64@1.5.2': + optional: true + + '@pagefind/windows-x64@1.5.2': + optional: true + '@playwright/test@1.59.1': dependencies: playwright: 1.59.1 @@ -3783,6 +3846,16 @@ snapshots: package-manager-detector@1.9.0: {} + pagefind@1.5.2: + optionalDependencies: + '@pagefind/darwin-arm64': 1.5.2 + '@pagefind/darwin-x64': 1.5.2 + '@pagefind/freebsd-x64': 1.5.2 + '@pagefind/linux-arm64': 1.5.2 + '@pagefind/linux-x64': 1.5.2 + '@pagefind/windows-arm64': 1.5.2 + '@pagefind/windows-x64': 1.5.2 + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 diff --git a/src/client/search.ts b/src/client/search.ts new file mode 100644 index 0000000..848f077 --- /dev/null +++ b/src/client/search.ts @@ -0,0 +1,88 @@ +/** + * Guide search: queries the Pagefind index that `pnpm build` writes to dist/pagefind/ and lists the matching guides. + * The index holds only elements marked `data-pagefind-body`, which on a guide page is the prose. + * `astro dev` serves no index, so search works only on a built site. + */ + +/** The parts of a Pagefind result that the list shows. */ +interface PagefindResultData { + /** The page's URL. Pagefind adds the site's /guides base, because it loads from under it. */ + url: string; + /** Text around the match, as HTML with the matched words in ``. Pagefind escapes the page text. */ + excerpt: string; + meta: { title?: string }; +} + +interface Pagefind { + search(query: string): Promise<{ results: { data(): Promise }[] }>; +} + +/** How many results the list shows. Pagefind fetches each shown result's data separately. */ +const MAX_SHOWN = 20; + +const BUNDLE = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/pagefind/pagefind.js`; + +let pagefind: Promise | undefined; + +/** Loads the Pagefind bundle once, on first use. */ +function loadPagefind(): Promise { + pagefind ??= import(/* @vite-ignore */ BUNDLE) as Promise; + return pagefind; +} + +/** Shows the search form, which the page renders hidden because it does nothing without this script. */ +export function initSearch(root: HTMLElement): void { + const form = root.querySelector('form')!; + const input = root.querySelector('input')!; + const status = root.querySelector('[role="status"]')!; + const list = root.querySelector('[data-search-results]')!; + let latest = 0; + + async function search(): Promise { + const query = input.value.trim(); + const run = ++latest; + if (query === '') { + status.textContent = ''; + list.replaceChildren(); + return; + } + try { + const { results } = await (await loadPagefind()).search(query); + const shown = await Promise.all(results.slice(0, MAX_SHOWN).map((result) => result.data())); + // A newer query may have finished first. Its results win. + if (run !== latest) return; + status.textContent = describe(results.length, shown.length, query); + list.replaceChildren(...shown.map(resultItem)); + } catch { + if (run !== latest) return; + status.textContent = 'Search is unavailable. It needs the built site and its Pagefind index.'; + list.replaceChildren(); + } + } + + input.addEventListener('focus', () => void loadPagefind().catch(() => {}), { once: true }); + input.addEventListener('input', () => void search()); + form.addEventListener('submit', (event) => { + event.preventDefault(); + void search(); + }); + root.hidden = false; +} + +function describe(total: number, shown: number, query: string): string { + if (total === 0) return `No guides match โ€œ${query}โ€.`; + const matches = `${total} ${total === 1 ? 'guide matches' : 'guides match'} โ€œ${query}โ€.`; + return shown < total ? `${matches} Showing the first ${shown}.` : matches; +} + +function resultItem(data: PagefindResultData): HTMLLIElement { + const item = document.createElement('li'); + const link = document.createElement('a'); + link.href = data.url; + link.textContent = data.meta.title ?? data.url; + const excerpt = document.createElement('p'); + excerpt.className = 'search-excerpt'; + excerpt.innerHTML = data.excerpt; + item.append(link, excerpt); + return item; +} diff --git a/src/components/GuidePage.astro b/src/components/GuidePage.astro index 90d7e04..acba039 100644 --- a/src/components/GuidePage.astro +++ b/src/components/GuidePage.astro @@ -27,7 +27,8 @@ const { guide } = Astro.props;
{guide.steps.map((step) => )}
-
+ {/* Search indexes the prose in this pane. Pagefind skips pages without data-pagefind-body. */} +
{guide.steps.map((step) => )}
diff --git a/src/components/GuideSearch.astro b/src/components/GuideSearch.astro new file mode 100644 index 0000000..382f544 --- /dev/null +++ b/src/components/GuideSearch.astro @@ -0,0 +1,77 @@ +--- +// Search across every guide's prose. The script shows the form, because it does nothing without the script. +--- + + + + + + diff --git a/src/pages/index.astro b/src/pages/index.astro index c1c3e36..789d770 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,5 +1,6 @@ --- import { getCollection } from 'astro:content'; +import GuideSearch from '../components/GuideSearch.astro'; import { guidesByRepo, repoUrl } from '../guide/catalog.ts'; import BaseLayout from '../layouts/BaseLayout.astro'; @@ -10,6 +11,7 @@ const repos = guidesByRepo(await getCollection('guides'));

Guides

Step-by-step guides to campdotdev pull requests, written by the agent that wrote each one.

+

Repositories