Find the cost of adding an npm package to your app's bundle size teardown.kelinci.dev

initial commit

+11702
+13
.gitignore
··· 1 + logs 2 + *.log 3 + npm-debug.log* 4 + yarn-debug.log* 5 + yarn-error.log* 6 + pnpm-debug.log* 7 + lerna-debug.log* 8 + 9 + node_modules 10 + dist 11 + *.local 12 + 13 + .research
+36
.oxfmtrc.json
··· 1 + { 2 + "$schema": "https://unpkg.com/oxfmt/configuration_schema.json", 3 + 4 + "trailingComma": "all", 5 + "useTabs": true, 6 + "tabWidth": 2, 7 + "printWidth": 110, 8 + "semi": true, 9 + "singleQuote": true, 10 + "bracketSpacing": true, 11 + 12 + "experimentalSortImports": { 13 + "groups": [["builtin"], ["external"], ["parent"], ["sibling", "index"]] 14 + }, 15 + "experimentalTailwindcss": { 16 + "stylesheet": "src/index.css", 17 + "functions": ["tw"], 18 + "attributes": ["class", "className", "classList"] 19 + }, 20 + 21 + "overrides": [ 22 + { 23 + "files": ["tsconfig.json", "tsconfig.*.json"], 24 + "options": { 25 + "parser": "jsonc" 26 + } 27 + }, 28 + { 29 + "files": ["*.md"], 30 + "options": { 31 + "printWidth": 100, 32 + "proseWrap": "always" 33 + } 34 + } 35 + ] 36 + }
+1
.prettierignore
··· 1 + pnpm-lock.yaml
+3
.vscode/extensions.json
··· 1 + { 2 + "recommendations": ["oxc.oxc-vscode", "bradlc.vscode-tailwindcss"] 3 + }
+6
.vscode/settings.json
··· 1 + { 2 + "editor.defaultFormatter": "oxc.oxc-vscode", 3 + "typescript.tsdk": "node_modules/typescript/lib", 4 + "tailwindCSS.classFunctions": ["tw"], 5 + "tailwindCSS.classAttributes": ["class", "className", "classList"] 6 + }
+96
CLAUDE.md
··· 1 + teardown is a bundlephobia alternative built with @rolldown/browser, using Vite and Solid.js. 2 + 3 + ## development notes 4 + 5 + ### project management 6 + 7 + - pnpm is managed by mise, to run commands, use `mise exec -- pnpm ...` 8 + - install dependencies with `pnpm install` 9 + - run dev server with `pnpm dev` 10 + - build with `pnpm build` 11 + - preview production build with `pnpm preview` 12 + - format via `pnpm run fmt` (oxfmt) 13 + - lint via `pnpm run lint` (oxlint) 14 + - typecheck via `pnpm tsc -b` 15 + - check `pnpm view <package>` before adding a new dependency 16 + 17 + ### code writing 18 + 19 + - new files should be in kebab-case 20 + - use tabs for indentation, spaces allowed for diagrams in comments 21 + - use single quotes and add trailing commas 22 + - prefer arrow functions, but use regular methods in classes unless arrow functions are necessary 23 + (e.g., when passing the method as a callback that needs `this` binding) 24 + - use braces for control statements, even single-line bodies 25 + - use bare blocks `{ }` to group related code and limit variable scope 26 + - use template literals for user-facing strings and error messages 27 + - avoid barrel exports (index files that re-export from other modules); import directly from source 28 + - use `// #region <name>` and `// #endregion` to denote regions when a file needs to contain a lot 29 + of code 30 + - prefer required parameters over optional ones; optional parameters are acceptable when: 31 + - the default is obvious and used by the vast majority of callers (e.g., `encoding = 'utf-8'`) 32 + - it's a configuration value with a sensible default (e.g., `timeout = 5000`) 33 + - avoid optional parameters that change behavioral modes or make the function do different things 34 + based on presence/absence; prefer separate functions instead 35 + - when adding optional parameters for backwards compatibility, consider whether a new function with 36 + a clearer name would be better 37 + 38 + ### documentation 39 + 40 + - documentations include README, code comments, commit messages 41 + - any writing should be in lowercase, except for proper nouns, acronyms and 'I'; this does not apply 42 + to public-facing interfaces like web UI 43 + - only comment non-trivial code, focusing on _why_ rather than _what_ 44 + - write comments and JSDoc in lowercase (except proper nouns, acronyms, and 'I') 45 + - add JSDoc comments to new publicly exported functions, methods, classes, fields, and enums 46 + - JSDoc should include proper annotations: 47 + - use `@param` for parameters (no dashes after param names) 48 + - use `@returns` for return values 49 + - use `@throws` for exceptions when applicable 50 + - keep descriptions concise but informative 51 + 52 + ### agentic coding 53 + 54 + - `.research/` directory in the project root serves as a workspace for temporary experiments, 55 + analysis, and planning materials. create if not present (it's gitignored). this directory may 56 + contain cloned repositories or other reference materials that can help inform implementation 57 + decisions 58 + - this document is intentionally incomplete; discover everything else in the repo 59 + - don't make assumptions or speculate about code, plans, or requirements without exploring first; 60 + pause and ask for clarification when you're still unsure after looking into it 61 + - in plan mode, present the plan for review before exiting to allow for feedback or follow-up 62 + questions 63 + - when debugging problems, isolate the root cause first before attempting fixes: add logging, 64 + reproduce the issue, narrow down the scope, and confirm the exact source of the problem 65 + 66 + ### Claude Code-specific 67 + 68 + - Bash tool persists directory changes (`cd`) across calls; always specify cd with absolute paths to 69 + be sure 70 + - Task tool (subagents for exploration, planning, etc.) may not always be accurate; verify subagent 71 + findings when needed 72 + 73 + ### cgr 74 + 75 + use `@oomfware/cgr` to ask questions about external repositories. 76 + 77 + ``` 78 + npx @oomfware/cgr ask [options] <repo>[#branch] <question> 79 + 80 + options: 81 + -m, --model <model> model to use: opus, sonnet, haiku (default: haiku) 82 + -d, --deep clone full history (enables git log/blame/show) 83 + -w, --with <repo> additional repository to include, supports #branch (repeatable) 84 + ``` 85 + 86 + useful repositories: 87 + 88 + - `github.com/rolldown/rolldown` for Rolldown bundler, @rolldown/browser API 89 + - `github.com/solidjs/solid` for Solid.js core reactivity and components 90 + - `github.com/vitejs/vite` for Vite dev server, build tooling, plugin API 91 + - `github.com/oxc-project/oxc` for Oxlint linter, Oxfmt formatter 92 + 93 + cgr works best with detailed questions. include file/folder paths when you know them, and reference 94 + details from previous answers in follow-ups. 95 + 96 + run `npx @oomfware/cgr --help` for more options.
+14
LICENSE
··· 1 + BSD Zero Clause License 2 + 3 + Copyright (c) 2026 Mary 4 + 5 + Permission to use, copy, modify, and/or distribute this software for any 6 + purpose with or without fee is hereby granted. 7 + 8 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 + PERFORMANCE OF THIS SOFTWARE.
+28
README.md
··· 1 + ## Usage 2 + 3 + ```bash 4 + $ npm install # or pnpm install or yarn install 5 + ``` 6 + 7 + ### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) 8 + 9 + ## Available Scripts 10 + 11 + In the project directory, you can run: 12 + 13 + ### `npm run dev` 14 + 15 + Runs the app in the development mode.<br> Open [http://localhost:5173](http://localhost:5173) to 16 + view it in the browser. 17 + 18 + ### `npm run build` 19 + 20 + Builds the app for production to the `dist` folder.<br> It correctly bundles Solid in production 21 + mode and optimizes the build for the best performance. 22 + 23 + The build is minified and the filenames include the hashes.<br> Your app is ready to be deployed! 24 + 25 + ## Deployment 26 + 27 + Learn more about deploying your application with the 28 + [documentations](https://vite.dev/guide/static-deploy.html)
+12
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>teardown</title> 7 + </head> 8 + <body> 9 + <div id="root"></div> 10 + <script type="module" src="/src/index.tsx"></script> 11 + </body> 12 + </html>
+2
mise.toml
··· 1 + [tools] 2 + node = "lts"
+39
package.json
··· 1 + { 2 + "name": "teardown", 3 + "version": "0.0.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsc -b && vite build", 9 + "preview": "vite preview", 10 + "fmt": "oxfmt", 11 + "lint": "oxlint", 12 + "test": "vitest" 13 + }, 14 + "dependencies": { 15 + "@atcute/uint8array": "^1.0.6", 16 + "@floating-ui/dom": "^1.7.4", 17 + "@mary/array-fns": "jsr:^0.1.5", 18 + "@mary/tar": "jsr:^0.3.1", 19 + "@rolldown/browser": "^1.0.0-rc.1", 20 + "dequal": "^2.0.3", 21 + "memfs": "^4.56.9", 22 + "solid-js": "^1.9.10", 23 + "valibot": "^1.2.0" 24 + }, 25 + "devDependencies": { 26 + "@tailwindcss/vite": "^4.1.18", 27 + "@types/node": "^24.10.1", 28 + "@types/semver": "^7.7.1", 29 + "oxfmt": "^0.26.0", 30 + "oxlint": "^1.41.0", 31 + "semver": "^7.7.3", 32 + "tailwindcss": "^4.1.18", 33 + "typescript": "~5.9.3", 34 + "vite": "^7.3.1", 35 + "vite-plugin-solid": "^2.11.10", 36 + "vitest": "^4.0.17", 37 + "wrangler": "^4.60.0" 38 + } 39 + }
+3307
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@atcute/uint8array': 12 + specifier: ^1.0.6 13 + version: 1.0.6 14 + '@floating-ui/dom': 15 + specifier: ^1.7.4 16 + version: 1.7.4 17 + '@mary/array-fns': 18 + specifier: jsr:^0.1.5 19 + version: '@jsr/mary__array-fns@0.1.5' 20 + '@mary/tar': 21 + specifier: jsr:^0.3.1 22 + version: '@jsr/mary__tar@0.3.1' 23 + '@rolldown/browser': 24 + specifier: ^1.0.0-rc.1 25 + version: 1.0.0-rc.1 26 + dequal: 27 + specifier: ^2.0.3 28 + version: 2.0.3 29 + memfs: 30 + specifier: ^4.56.9 31 + version: 4.56.9(tslib@2.8.1) 32 + solid-js: 33 + specifier: ^1.9.10 34 + version: 1.9.10 35 + valibot: 36 + specifier: ^1.2.0 37 + version: 1.2.0(typescript@5.9.3) 38 + devDependencies: 39 + '@tailwindcss/vite': 40 + specifier: ^4.1.18 41 + version: 4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)) 42 + '@types/node': 43 + specifier: ^24.10.1 44 + version: 24.10.9 45 + '@types/semver': 46 + specifier: ^7.7.1 47 + version: 7.7.1 48 + oxfmt: 49 + specifier: ^0.26.0 50 + version: 0.26.0 51 + oxlint: 52 + specifier: ^1.41.0 53 + version: 1.41.0 54 + semver: 55 + specifier: ^7.7.3 56 + version: 7.7.3 57 + tailwindcss: 58 + specifier: ^4.1.18 59 + version: 4.1.18 60 + typescript: 61 + specifier: ~5.9.3 62 + version: 5.9.3 63 + vite: 64 + specifier: ^7.3.1 65 + version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1) 66 + vite-plugin-solid: 67 + specifier: ^2.11.10 68 + version: 2.11.10(solid-js@1.9.10)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)) 69 + vitest: 70 + specifier: ^4.0.17 71 + version: 4.0.17(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1) 72 + wrangler: 73 + specifier: ^4.60.0 74 + version: 4.60.0 75 + 76 + packages: 77 + 78 + '@atcute/uint8array@1.0.6': 79 + resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==} 80 + 81 + '@babel/code-frame@7.28.6': 82 + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} 83 + engines: {node: '>=6.9.0'} 84 + 85 + '@babel/compat-data@7.28.6': 86 + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} 87 + engines: {node: '>=6.9.0'} 88 + 89 + '@babel/core@7.28.6': 90 + resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} 91 + engines: {node: '>=6.9.0'} 92 + 93 + '@babel/generator@7.28.6': 94 + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} 95 + engines: {node: '>=6.9.0'} 96 + 97 + '@babel/helper-compilation-targets@7.28.6': 98 + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} 99 + engines: {node: '>=6.9.0'} 100 + 101 + '@babel/helper-globals@7.28.0': 102 + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} 103 + engines: {node: '>=6.9.0'} 104 + 105 + '@babel/helper-module-imports@7.18.6': 106 + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} 107 + engines: {node: '>=6.9.0'} 108 + 109 + '@babel/helper-module-imports@7.28.6': 110 + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} 111 + engines: {node: '>=6.9.0'} 112 + 113 + '@babel/helper-module-transforms@7.28.6': 114 + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} 115 + engines: {node: '>=6.9.0'} 116 + peerDependencies: 117 + '@babel/core': ^7.0.0 118 + 119 + '@babel/helper-plugin-utils@7.28.6': 120 + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} 121 + engines: {node: '>=6.9.0'} 122 + 123 + '@babel/helper-string-parser@7.27.1': 124 + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 125 + engines: {node: '>=6.9.0'} 126 + 127 + '@babel/helper-validator-identifier@7.28.5': 128 + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} 129 + engines: {node: '>=6.9.0'} 130 + 131 + '@babel/helper-validator-option@7.27.1': 132 + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} 133 + engines: {node: '>=6.9.0'} 134 + 135 + '@babel/helpers@7.28.6': 136 + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} 137 + engines: {node: '>=6.9.0'} 138 + 139 + '@babel/parser@7.28.6': 140 + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} 141 + engines: {node: '>=6.0.0'} 142 + hasBin: true 143 + 144 + '@babel/plugin-syntax-jsx@7.28.6': 145 + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} 146 + engines: {node: '>=6.9.0'} 147 + peerDependencies: 148 + '@babel/core': ^7.0.0-0 149 + 150 + '@babel/template@7.28.6': 151 + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} 152 + engines: {node: '>=6.9.0'} 153 + 154 + '@babel/traverse@7.28.6': 155 + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} 156 + engines: {node: '>=6.9.0'} 157 + 158 + '@babel/types@7.28.6': 159 + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} 160 + engines: {node: '>=6.9.0'} 161 + 162 + '@cloudflare/kv-asset-handler@0.4.2': 163 + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} 164 + engines: {node: '>=18.0.0'} 165 + 166 + '@cloudflare/unenv-preset@2.11.0': 167 + resolution: {integrity: sha512-z3hxFajL765VniNPGV0JRStZolNz63gU3B3AktwoGdDlnQvz5nP+Ah4RL04PONlZQjwmDdGHowEStJ94+RsaJg==} 168 + peerDependencies: 169 + unenv: 2.0.0-rc.24 170 + workerd: ^1.20260115.0 171 + peerDependenciesMeta: 172 + workerd: 173 + optional: true 174 + 175 + '@cloudflare/workerd-darwin-64@1.20260120.0': 176 + resolution: {integrity: sha512-JLHx3p5dpwz4wjVSis45YNReftttnI3ndhdMh5BUbbpdreN/g0jgxNt5Qp9tDFqEKl++N63qv+hxJiIIvSLR+Q==} 177 + engines: {node: '>=16'} 178 + cpu: [x64] 179 + os: [darwin] 180 + 181 + '@cloudflare/workerd-darwin-arm64@1.20260120.0': 182 + resolution: {integrity: sha512-1Md2tCRhZjwajsZNOiBeOVGiS3zbpLPzUDjHr4+XGTXWOA6FzzwScJwQZLa0Doc28Cp4Nr1n7xGL0Dwiz1XuOA==} 183 + engines: {node: '>=16'} 184 + cpu: [arm64] 185 + os: [darwin] 186 + 187 + '@cloudflare/workerd-linux-64@1.20260120.0': 188 + resolution: {integrity: sha512-O0mIfJfvU7F8N5siCoRDaVDuI12wkz2xlG4zK6/Ct7U9c9FiE0ViXNFWXFQm5PPj+qbkNRyhjUwhP+GCKTk5EQ==} 189 + engines: {node: '>=16'} 190 + cpu: [x64] 191 + os: [linux] 192 + 193 + '@cloudflare/workerd-linux-arm64@1.20260120.0': 194 + resolution: {integrity: sha512-aRHO/7bjxVpjZEmVVcpmhbzpN6ITbFCxuLLZSW0H9O0C0w40cDCClWSi19T87Ax/PQcYjFNT22pTewKsupkckA==} 195 + engines: {node: '>=16'} 196 + cpu: [arm64] 197 + os: [linux] 198 + 199 + '@cloudflare/workerd-windows-64@1.20260120.0': 200 + resolution: {integrity: sha512-ASZIz1E8sqZQqQCgcfY1PJbBpUDrxPt8NZ+lqNil0qxnO4qX38hbCsdDF2/TDAuq0Txh7nu8ztgTelfNDlb4EA==} 201 + engines: {node: '>=16'} 202 + cpu: [x64] 203 + os: [win32] 204 + 205 + '@cspotcode/source-map-support@0.8.1': 206 + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 207 + engines: {node: '>=12'} 208 + 209 + '@emnapi/core@1.8.1': 210 + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} 211 + 212 + '@emnapi/runtime@1.8.1': 213 + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} 214 + 215 + '@emnapi/wasi-threads@1.1.0': 216 + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} 217 + 218 + '@esbuild/aix-ppc64@0.27.0': 219 + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} 220 + engines: {node: '>=18'} 221 + cpu: [ppc64] 222 + os: [aix] 223 + 224 + '@esbuild/aix-ppc64@0.27.2': 225 + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} 226 + engines: {node: '>=18'} 227 + cpu: [ppc64] 228 + os: [aix] 229 + 230 + '@esbuild/android-arm64@0.27.0': 231 + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} 232 + engines: {node: '>=18'} 233 + cpu: [arm64] 234 + os: [android] 235 + 236 + '@esbuild/android-arm64@0.27.2': 237 + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} 238 + engines: {node: '>=18'} 239 + cpu: [arm64] 240 + os: [android] 241 + 242 + '@esbuild/android-arm@0.27.0': 243 + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} 244 + engines: {node: '>=18'} 245 + cpu: [arm] 246 + os: [android] 247 + 248 + '@esbuild/android-arm@0.27.2': 249 + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} 250 + engines: {node: '>=18'} 251 + cpu: [arm] 252 + os: [android] 253 + 254 + '@esbuild/android-x64@0.27.0': 255 + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} 256 + engines: {node: '>=18'} 257 + cpu: [x64] 258 + os: [android] 259 + 260 + '@esbuild/android-x64@0.27.2': 261 + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} 262 + engines: {node: '>=18'} 263 + cpu: [x64] 264 + os: [android] 265 + 266 + '@esbuild/darwin-arm64@0.27.0': 267 + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} 268 + engines: {node: '>=18'} 269 + cpu: [arm64] 270 + os: [darwin] 271 + 272 + '@esbuild/darwin-arm64@0.27.2': 273 + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} 274 + engines: {node: '>=18'} 275 + cpu: [arm64] 276 + os: [darwin] 277 + 278 + '@esbuild/darwin-x64@0.27.0': 279 + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} 280 + engines: {node: '>=18'} 281 + cpu: [x64] 282 + os: [darwin] 283 + 284 + '@esbuild/darwin-x64@0.27.2': 285 + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} 286 + engines: {node: '>=18'} 287 + cpu: [x64] 288 + os: [darwin] 289 + 290 + '@esbuild/freebsd-arm64@0.27.0': 291 + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} 292 + engines: {node: '>=18'} 293 + cpu: [arm64] 294 + os: [freebsd] 295 + 296 + '@esbuild/freebsd-arm64@0.27.2': 297 + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} 298 + engines: {node: '>=18'} 299 + cpu: [arm64] 300 + os: [freebsd] 301 + 302 + '@esbuild/freebsd-x64@0.27.0': 303 + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} 304 + engines: {node: '>=18'} 305 + cpu: [x64] 306 + os: [freebsd] 307 + 308 + '@esbuild/freebsd-x64@0.27.2': 309 + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} 310 + engines: {node: '>=18'} 311 + cpu: [x64] 312 + os: [freebsd] 313 + 314 + '@esbuild/linux-arm64@0.27.0': 315 + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} 316 + engines: {node: '>=18'} 317 + cpu: [arm64] 318 + os: [linux] 319 + 320 + '@esbuild/linux-arm64@0.27.2': 321 + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} 322 + engines: {node: '>=18'} 323 + cpu: [arm64] 324 + os: [linux] 325 + 326 + '@esbuild/linux-arm@0.27.0': 327 + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} 328 + engines: {node: '>=18'} 329 + cpu: [arm] 330 + os: [linux] 331 + 332 + '@esbuild/linux-arm@0.27.2': 333 + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} 334 + engines: {node: '>=18'} 335 + cpu: [arm] 336 + os: [linux] 337 + 338 + '@esbuild/linux-ia32@0.27.0': 339 + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} 340 + engines: {node: '>=18'} 341 + cpu: [ia32] 342 + os: [linux] 343 + 344 + '@esbuild/linux-ia32@0.27.2': 345 + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} 346 + engines: {node: '>=18'} 347 + cpu: [ia32] 348 + os: [linux] 349 + 350 + '@esbuild/linux-loong64@0.27.0': 351 + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} 352 + engines: {node: '>=18'} 353 + cpu: [loong64] 354 + os: [linux] 355 + 356 + '@esbuild/linux-loong64@0.27.2': 357 + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} 358 + engines: {node: '>=18'} 359 + cpu: [loong64] 360 + os: [linux] 361 + 362 + '@esbuild/linux-mips64el@0.27.0': 363 + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} 364 + engines: {node: '>=18'} 365 + cpu: [mips64el] 366 + os: [linux] 367 + 368 + '@esbuild/linux-mips64el@0.27.2': 369 + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} 370 + engines: {node: '>=18'} 371 + cpu: [mips64el] 372 + os: [linux] 373 + 374 + '@esbuild/linux-ppc64@0.27.0': 375 + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} 376 + engines: {node: '>=18'} 377 + cpu: [ppc64] 378 + os: [linux] 379 + 380 + '@esbuild/linux-ppc64@0.27.2': 381 + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} 382 + engines: {node: '>=18'} 383 + cpu: [ppc64] 384 + os: [linux] 385 + 386 + '@esbuild/linux-riscv64@0.27.0': 387 + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} 388 + engines: {node: '>=18'} 389 + cpu: [riscv64] 390 + os: [linux] 391 + 392 + '@esbuild/linux-riscv64@0.27.2': 393 + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} 394 + engines: {node: '>=18'} 395 + cpu: [riscv64] 396 + os: [linux] 397 + 398 + '@esbuild/linux-s390x@0.27.0': 399 + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} 400 + engines: {node: '>=18'} 401 + cpu: [s390x] 402 + os: [linux] 403 + 404 + '@esbuild/linux-s390x@0.27.2': 405 + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} 406 + engines: {node: '>=18'} 407 + cpu: [s390x] 408 + os: [linux] 409 + 410 + '@esbuild/linux-x64@0.27.0': 411 + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} 412 + engines: {node: '>=18'} 413 + cpu: [x64] 414 + os: [linux] 415 + 416 + '@esbuild/linux-x64@0.27.2': 417 + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} 418 + engines: {node: '>=18'} 419 + cpu: [x64] 420 + os: [linux] 421 + 422 + '@esbuild/netbsd-arm64@0.27.0': 423 + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} 424 + engines: {node: '>=18'} 425 + cpu: [arm64] 426 + os: [netbsd] 427 + 428 + '@esbuild/netbsd-arm64@0.27.2': 429 + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} 430 + engines: {node: '>=18'} 431 + cpu: [arm64] 432 + os: [netbsd] 433 + 434 + '@esbuild/netbsd-x64@0.27.0': 435 + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} 436 + engines: {node: '>=18'} 437 + cpu: [x64] 438 + os: [netbsd] 439 + 440 + '@esbuild/netbsd-x64@0.27.2': 441 + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} 442 + engines: {node: '>=18'} 443 + cpu: [x64] 444 + os: [netbsd] 445 + 446 + '@esbuild/openbsd-arm64@0.27.0': 447 + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} 448 + engines: {node: '>=18'} 449 + cpu: [arm64] 450 + os: [openbsd] 451 + 452 + '@esbuild/openbsd-arm64@0.27.2': 453 + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} 454 + engines: {node: '>=18'} 455 + cpu: [arm64] 456 + os: [openbsd] 457 + 458 + '@esbuild/openbsd-x64@0.27.0': 459 + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} 460 + engines: {node: '>=18'} 461 + cpu: [x64] 462 + os: [openbsd] 463 + 464 + '@esbuild/openbsd-x64@0.27.2': 465 + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} 466 + engines: {node: '>=18'} 467 + cpu: [x64] 468 + os: [openbsd] 469 + 470 + '@esbuild/openharmony-arm64@0.27.0': 471 + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} 472 + engines: {node: '>=18'} 473 + cpu: [arm64] 474 + os: [openharmony] 475 + 476 + '@esbuild/openharmony-arm64@0.27.2': 477 + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} 478 + engines: {node: '>=18'} 479 + cpu: [arm64] 480 + os: [openharmony] 481 + 482 + '@esbuild/sunos-x64@0.27.0': 483 + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} 484 + engines: {node: '>=18'} 485 + cpu: [x64] 486 + os: [sunos] 487 + 488 + '@esbuild/sunos-x64@0.27.2': 489 + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} 490 + engines: {node: '>=18'} 491 + cpu: [x64] 492 + os: [sunos] 493 + 494 + '@esbuild/win32-arm64@0.27.0': 495 + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} 496 + engines: {node: '>=18'} 497 + cpu: [arm64] 498 + os: [win32] 499 + 500 + '@esbuild/win32-arm64@0.27.2': 501 + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} 502 + engines: {node: '>=18'} 503 + cpu: [arm64] 504 + os: [win32] 505 + 506 + '@esbuild/win32-ia32@0.27.0': 507 + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} 508 + engines: {node: '>=18'} 509 + cpu: [ia32] 510 + os: [win32] 511 + 512 + '@esbuild/win32-ia32@0.27.2': 513 + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} 514 + engines: {node: '>=18'} 515 + cpu: [ia32] 516 + os: [win32] 517 + 518 + '@esbuild/win32-x64@0.27.0': 519 + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} 520 + engines: {node: '>=18'} 521 + cpu: [x64] 522 + os: [win32] 523 + 524 + '@esbuild/win32-x64@0.27.2': 525 + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} 526 + engines: {node: '>=18'} 527 + cpu: [x64] 528 + os: [win32] 529 + 530 + '@floating-ui/core@1.7.3': 531 + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} 532 + 533 + '@floating-ui/dom@1.7.4': 534 + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} 535 + 536 + '@floating-ui/utils@0.2.10': 537 + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} 538 + 539 + '@img/colour@1.0.0': 540 + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} 541 + engines: {node: '>=18'} 542 + 543 + '@img/sharp-darwin-arm64@0.34.5': 544 + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} 545 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 546 + cpu: [arm64] 547 + os: [darwin] 548 + 549 + '@img/sharp-darwin-x64@0.34.5': 550 + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} 551 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 552 + cpu: [x64] 553 + os: [darwin] 554 + 555 + '@img/sharp-libvips-darwin-arm64@1.2.4': 556 + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} 557 + cpu: [arm64] 558 + os: [darwin] 559 + 560 + '@img/sharp-libvips-darwin-x64@1.2.4': 561 + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} 562 + cpu: [x64] 563 + os: [darwin] 564 + 565 + '@img/sharp-libvips-linux-arm64@1.2.4': 566 + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} 567 + cpu: [arm64] 568 + os: [linux] 569 + 570 + '@img/sharp-libvips-linux-arm@1.2.4': 571 + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} 572 + cpu: [arm] 573 + os: [linux] 574 + 575 + '@img/sharp-libvips-linux-ppc64@1.2.4': 576 + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} 577 + cpu: [ppc64] 578 + os: [linux] 579 + 580 + '@img/sharp-libvips-linux-riscv64@1.2.4': 581 + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} 582 + cpu: [riscv64] 583 + os: [linux] 584 + 585 + '@img/sharp-libvips-linux-s390x@1.2.4': 586 + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} 587 + cpu: [s390x] 588 + os: [linux] 589 + 590 + '@img/sharp-libvips-linux-x64@1.2.4': 591 + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} 592 + cpu: [x64] 593 + os: [linux] 594 + 595 + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': 596 + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} 597 + cpu: [arm64] 598 + os: [linux] 599 + 600 + '@img/sharp-libvips-linuxmusl-x64@1.2.4': 601 + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} 602 + cpu: [x64] 603 + os: [linux] 604 + 605 + '@img/sharp-linux-arm64@0.34.5': 606 + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} 607 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 608 + cpu: [arm64] 609 + os: [linux] 610 + 611 + '@img/sharp-linux-arm@0.34.5': 612 + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} 613 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 614 + cpu: [arm] 615 + os: [linux] 616 + 617 + '@img/sharp-linux-ppc64@0.34.5': 618 + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} 619 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 620 + cpu: [ppc64] 621 + os: [linux] 622 + 623 + '@img/sharp-linux-riscv64@0.34.5': 624 + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} 625 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 626 + cpu: [riscv64] 627 + os: [linux] 628 + 629 + '@img/sharp-linux-s390x@0.34.5': 630 + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} 631 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 632 + cpu: [s390x] 633 + os: [linux] 634 + 635 + '@img/sharp-linux-x64@0.34.5': 636 + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} 637 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 638 + cpu: [x64] 639 + os: [linux] 640 + 641 + '@img/sharp-linuxmusl-arm64@0.34.5': 642 + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} 643 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 644 + cpu: [arm64] 645 + os: [linux] 646 + 647 + '@img/sharp-linuxmusl-x64@0.34.5': 648 + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} 649 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 650 + cpu: [x64] 651 + os: [linux] 652 + 653 + '@img/sharp-wasm32@0.34.5': 654 + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} 655 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 656 + cpu: [wasm32] 657 + 658 + '@img/sharp-win32-arm64@0.34.5': 659 + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} 660 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 661 + cpu: [arm64] 662 + os: [win32] 663 + 664 + '@img/sharp-win32-ia32@0.34.5': 665 + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} 666 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 667 + cpu: [ia32] 668 + os: [win32] 669 + 670 + '@img/sharp-win32-x64@0.34.5': 671 + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} 672 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 673 + cpu: [x64] 674 + os: [win32] 675 + 676 + '@jridgewell/gen-mapping@0.3.13': 677 + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 678 + 679 + '@jridgewell/remapping@2.3.5': 680 + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} 681 + 682 + '@jridgewell/resolve-uri@3.1.2': 683 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 684 + engines: {node: '>=6.0.0'} 685 + 686 + '@jridgewell/sourcemap-codec@1.5.5': 687 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 688 + 689 + '@jridgewell/trace-mapping@0.3.31': 690 + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 691 + 692 + '@jridgewell/trace-mapping@0.3.9': 693 + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 694 + 695 + '@jsonjoy.com/base64@1.1.2': 696 + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} 697 + engines: {node: '>=10.0'} 698 + peerDependencies: 699 + tslib: '2' 700 + 701 + '@jsonjoy.com/base64@17.65.0': 702 + resolution: {integrity: sha512-Xrh7Fm/M0QAYpekSgmskdZYnFdSGnsxJ/tHaolA4bNwWdG9i65S8m83Meh7FOxyJyQAdo4d4J97NOomBLEfkDQ==} 703 + engines: {node: '>=10.0'} 704 + peerDependencies: 705 + tslib: '2' 706 + 707 + '@jsonjoy.com/buffers@1.2.1': 708 + resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} 709 + engines: {node: '>=10.0'} 710 + peerDependencies: 711 + tslib: '2' 712 + 713 + '@jsonjoy.com/buffers@17.65.0': 714 + resolution: {integrity: sha512-eBrIXd0/Ld3p9lpDDlMaMn6IEfWqtHMD+z61u0JrIiPzsV1r7m6xDZFRxJyvIFTEO+SWdYF9EiQbXZGd8BzPfA==} 715 + engines: {node: '>=10.0'} 716 + peerDependencies: 717 + tslib: '2' 718 + 719 + '@jsonjoy.com/codegen@1.0.0': 720 + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} 721 + engines: {node: '>=10.0'} 722 + peerDependencies: 723 + tslib: '2' 724 + 725 + '@jsonjoy.com/codegen@17.65.0': 726 + resolution: {integrity: sha512-7MXcRYe7n3BG+fo3jicvjB0+6ypl2Y/bQp79Sp7KeSiiCgLqw4Oled6chVv07/xLVTdo3qa1CD0VCCnPaw+RGA==} 727 + engines: {node: '>=10.0'} 728 + peerDependencies: 729 + tslib: '2' 730 + 731 + '@jsonjoy.com/fs-core@4.56.9': 732 + resolution: {integrity: sha512-BUkXXWL3I7VZ34cpmP7WSttmP5o+z+lxi3teYMnEcUOKBu7DhCFxCesOevw+UATUewMHRMUtsmFYxOxgV7SQwg==} 733 + engines: {node: '>=10.0'} 734 + peerDependencies: 735 + tslib: '2' 736 + 737 + '@jsonjoy.com/fs-fsa@4.56.9': 738 + resolution: {integrity: sha512-g15wwrvRRsy73p/b93XltxMkARyh3efxZNkrKbiocUNaPnHF+iDXQ1IlBwsTi5zxijdCYOsmVuyEdBX87tLqlw==} 739 + engines: {node: '>=10.0'} 740 + peerDependencies: 741 + tslib: '2' 742 + 743 + '@jsonjoy.com/fs-node-builtins@4.56.9': 744 + resolution: {integrity: sha512-q9MEsySAwyhgy1GT1FKfnKJ1a8bJmzbQnMGQA94F663C/wycrSgRrM33byzTAwn6FBRzMfTvABANkYvkOeYGhw==} 745 + engines: {node: '>=10.0'} 746 + peerDependencies: 747 + tslib: '2' 748 + 749 + '@jsonjoy.com/fs-node-to-fsa@4.56.9': 750 + resolution: {integrity: sha512-rOnn9FBLY+JWy0UDSXaYXY45j7FxfRJepRW5pZvNbdAzHYFZ0/M3OQ1+RfZsMYgWeMkaN9pGhOsIj/A7P9pAXA==} 751 + engines: {node: '>=10.0'} 752 + peerDependencies: 753 + tslib: '2' 754 + 755 + '@jsonjoy.com/fs-node-utils@4.56.9': 756 + resolution: {integrity: sha512-UMUirCu0jDPyJEsfllKX1SmK9E7ww2VltWiq2qBCy3ZcyHqDuHswPycrxLTwGrLJnGiHPW9f7LOniP7enl9jYQ==} 757 + engines: {node: '>=10.0'} 758 + peerDependencies: 759 + tslib: '2' 760 + 761 + '@jsonjoy.com/fs-node@4.56.9': 762 + resolution: {integrity: sha512-YiI2iqVMi/Ro9BcqWWLQBv939gje748pC4t376M/goQoLaM0sItsj0bBTiQr4eXyLsLdGw10n/F/kH5/snBe7g==} 763 + engines: {node: '>=10.0'} 764 + peerDependencies: 765 + tslib: '2' 766 + 767 + '@jsonjoy.com/fs-print@4.56.9': 768 + resolution: {integrity: sha512-Op6rXFnmhHHAClNvHFGx9zALHgZfyPdPBd0WIf/MBr4DEoShhAj0MZxg0jMO7foqleq2YSNNCNBMFGkmY43wAQ==} 769 + engines: {node: '>=10.0'} 770 + peerDependencies: 771 + tslib: '2' 772 + 773 + '@jsonjoy.com/fs-snapshot@4.56.9': 774 + resolution: {integrity: sha512-nMxEvDku2bCdCCNLkjd9hjPyUng8mLIfok8yAQ0zHNbZqeE44K5CSXnT0o3TGzv/zWynM49rUlF95ZjlNazFAQ==} 775 + engines: {node: '>=10.0'} 776 + peerDependencies: 777 + tslib: '2' 778 + 779 + '@jsonjoy.com/json-pack@1.21.0': 780 + resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} 781 + engines: {node: '>=10.0'} 782 + peerDependencies: 783 + tslib: '2' 784 + 785 + '@jsonjoy.com/json-pack@17.65.0': 786 + resolution: {integrity: sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ==} 787 + engines: {node: '>=10.0'} 788 + peerDependencies: 789 + tslib: '2' 790 + 791 + '@jsonjoy.com/json-pointer@1.0.2': 792 + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} 793 + engines: {node: '>=10.0'} 794 + peerDependencies: 795 + tslib: '2' 796 + 797 + '@jsonjoy.com/json-pointer@17.65.0': 798 + resolution: {integrity: sha512-uhTe+XhlIZpWOxgPcnO+iSCDgKKBpwkDVTyYiXX9VayGV8HSFVJM67M6pUE71zdnXF1W0Da21AvnhlmdwYPpow==} 799 + engines: {node: '>=10.0'} 800 + peerDependencies: 801 + tslib: '2' 802 + 803 + '@jsonjoy.com/util@1.9.0': 804 + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} 805 + engines: {node: '>=10.0'} 806 + peerDependencies: 807 + tslib: '2' 808 + 809 + '@jsonjoy.com/util@17.65.0': 810 + resolution: {integrity: sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w==} 811 + engines: {node: '>=10.0'} 812 + peerDependencies: 813 + tslib: '2' 814 + 815 + '@jsr/mary__array-fns@0.1.5': 816 + resolution: {integrity: sha512-gI4scq/Hh9GtFUJfS8cvZf5nr+cs7udvrEpMv75grws5/0LIwBycKeeJcNi4+xNl6x4CGW6Fp46puhtJiQOpMg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__array-fns/0.1.5.tgz} 817 + 818 + '@jsr/mary__tar@0.3.1': 819 + resolution: {integrity: sha512-T803kucwCLVOXFJGzVbpkT5vRK6fARy5HL6xMiLK5hJFck72bsAeluENlRnvD0kFPSlFNp/5EJWfTHnpDK0qYA==, tarball: https://npm.jsr.io/~/11/@jsr/mary__tar/0.3.1.tgz} 820 + 821 + '@napi-rs/wasm-runtime@1.1.1': 822 + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} 823 + 824 + '@oxfmt/darwin-arm64@0.26.0': 825 + resolution: {integrity: sha512-AAGc+8CffkiWeVgtWf4dPfQwHEE5c/j/8NWH7VGVxxJRCZFdmWcqCXprvL2H6qZFewvDLrFbuSPRCqYCpYGaTQ==} 826 + cpu: [arm64] 827 + os: [darwin] 828 + 829 + '@oxfmt/darwin-x64@0.26.0': 830 + resolution: {integrity: sha512-xFx5ijCTjw577wJvFlZEMmKDnp3HSCcbYdCsLRmC5i3TZZiDe9DEYh3P46uqhzj8BkEw1Vm1ZCWdl48aEYAzvQ==} 831 + cpu: [x64] 832 + os: [darwin] 833 + 834 + '@oxfmt/linux-arm64-gnu@0.26.0': 835 + resolution: {integrity: sha512-GubkQeQT5d3B/Jx/IiR7NMkSmXrCZcVI0BPh1i7mpFi8HgD1hQ/LbhiBKAMsMqs5bbugdQOgBEl8bOhe8JhW1g==} 836 + cpu: [arm64] 837 + os: [linux] 838 + 839 + '@oxfmt/linux-arm64-musl@0.26.0': 840 + resolution: {integrity: sha512-OEypUwK69bFPj+aa3/LYCnlIUPgoOLu//WNcriwpnWNmt47808Ht7RJSg+MNK8a7pSZHpXJ5/E6CRK/OTwFdaQ==} 841 + cpu: [arm64] 842 + os: [linux] 843 + 844 + '@oxfmt/linux-x64-gnu@0.26.0': 845 + resolution: {integrity: sha512-xO6iEW2bC6ZHyOTPmPWrg/nM6xgzyRPaS84rATy6F8d79wz69LdRdJ3l/PXlkqhi7XoxhvX4ExysA0Nf10ZZEQ==} 846 + cpu: [x64] 847 + os: [linux] 848 + 849 + '@oxfmt/linux-x64-musl@0.26.0': 850 + resolution: {integrity: sha512-Z3KuZFC+MIuAyFCXBHY71kCsdRq1ulbsbzTe71v+hrEv7zVBn6yzql+/AZcgfIaKzWO9OXNuz5WWLWDmVALwow==} 851 + cpu: [x64] 852 + os: [linux] 853 + 854 + '@oxfmt/win32-arm64@0.26.0': 855 + resolution: {integrity: sha512-3zRbqwVWK1mDhRhTknlQFpRFL9GhEB5GfU6U7wawnuEwpvi39q91kJ+SRJvJnhyPCARkjZBd1V8XnweN5IFd1g==} 856 + cpu: [arm64] 857 + os: [win32] 858 + 859 + '@oxfmt/win32-x64@0.26.0': 860 + resolution: {integrity: sha512-m8TfIljU22i9UEIkD+slGPifTFeaCwIUfxszN3E6ABWP1KQbtwSw9Ak0TdoikibvukF/dtbeyG3WW63jv9DnEg==} 861 + cpu: [x64] 862 + os: [win32] 863 + 864 + '@oxlint/darwin-arm64@1.41.0': 865 + resolution: {integrity: sha512-K0Bs0cNW11oWdSrKmrollKF44HMM2HKr4QidZQHMlhJcSX8pozxv0V5FLdqB4sddzCY0J9Wuuw+oRAfR8sdRwA==} 866 + cpu: [arm64] 867 + os: [darwin] 868 + 869 + '@oxlint/darwin-x64@1.41.0': 870 + resolution: {integrity: sha512-1LCCXCe9nN8LbrJ1QOGari2HqnxrZrveYKysWDIg8gFsQglIg00XF/8lRbA0kWHMdLgt4X0wfNYhhFz+c3XXLQ==} 871 + cpu: [x64] 872 + os: [darwin] 873 + 874 + '@oxlint/linux-arm64-gnu@1.41.0': 875 + resolution: {integrity: sha512-Fow7H84Bs8XxuaK1yfSEWBC8HI7rfEQB9eR2A0J61un1WgCas7jNrt1HbT6+p6KmUH2bhR+r/RDu/6JFAvvj4g==} 876 + cpu: [arm64] 877 + os: [linux] 878 + 879 + '@oxlint/linux-arm64-musl@1.41.0': 880 + resolution: {integrity: sha512-WoRRDNwgP5W3rjRh42Zdx8ferYnqpKoYCv2QQLenmdrLjRGYwAd52uywfkcS45mKEWHeY1RPwPkYCSROXiGb2w==} 881 + cpu: [arm64] 882 + os: [linux] 883 + 884 + '@oxlint/linux-x64-gnu@1.41.0': 885 + resolution: {integrity: sha512-75k3CKj3fOc/a/2aSgO81s3HsTZOFROthPJ+UI2Oatic1LhvH6eKjKfx3jDDyVpzeDS2qekPlc/y3N33iZz5Og==} 886 + cpu: [x64] 887 + os: [linux] 888 + 889 + '@oxlint/linux-x64-musl@1.41.0': 890 + resolution: {integrity: sha512-8r82eBwGPoAPn67ZvdxTlX/Z3gVb+ZtN6nbkyFzwwHWAh8yGutX+VBcVkyrePSl6XgBP4QAaddPnHmkvJjqY0g==} 891 + cpu: [x64] 892 + os: [linux] 893 + 894 + '@oxlint/win32-arm64@1.41.0': 895 + resolution: {integrity: sha512-aK+DAcckQsNCOXKruatyYuY/ROjNiRejQB1PeJtkZwM21+8rV9ODYbvKNvt0pW+YCws7svftBSFMCpl3ke2unw==} 896 + cpu: [arm64] 897 + os: [win32] 898 + 899 + '@oxlint/win32-x64@1.41.0': 900 + resolution: {integrity: sha512-dVBXkZ6MGLd3owV7jvuqJsZwiF3qw7kEkDVsYVpS/O96eEvlHcxVbaPjJjrTBgikXqyC22vg3dxBU7MW0utGfw==} 901 + cpu: [x64] 902 + os: [win32] 903 + 904 + '@poppinss/colors@4.1.6': 905 + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} 906 + 907 + '@poppinss/dumper@0.6.5': 908 + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} 909 + 910 + '@poppinss/exception@1.2.3': 911 + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} 912 + 913 + '@rolldown/browser@1.0.0-rc.1': 914 + resolution: {integrity: sha512-n/h6Oo2Udn5IhtLqYPqtZor09BPu+ml35ccbMv9XsJOpBTExjp39sqhGB3qogqowu5xwb9JHp2pR2JfPJIta4w==} 915 + hasBin: true 916 + 917 + '@rollup/rollup-android-arm-eabi@4.55.3': 918 + resolution: {integrity: sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg==} 919 + cpu: [arm] 920 + os: [android] 921 + 922 + '@rollup/rollup-android-arm64@4.55.3': 923 + resolution: {integrity: sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g==} 924 + cpu: [arm64] 925 + os: [android] 926 + 927 + '@rollup/rollup-darwin-arm64@4.55.3': 928 + resolution: {integrity: sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw==} 929 + cpu: [arm64] 930 + os: [darwin] 931 + 932 + '@rollup/rollup-darwin-x64@4.55.3': 933 + resolution: {integrity: sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA==} 934 + cpu: [x64] 935 + os: [darwin] 936 + 937 + '@rollup/rollup-freebsd-arm64@4.55.3': 938 + resolution: {integrity: sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q==} 939 + cpu: [arm64] 940 + os: [freebsd] 941 + 942 + '@rollup/rollup-freebsd-x64@4.55.3': 943 + resolution: {integrity: sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA==} 944 + cpu: [x64] 945 + os: [freebsd] 946 + 947 + '@rollup/rollup-linux-arm-gnueabihf@4.55.3': 948 + resolution: {integrity: sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==} 949 + cpu: [arm] 950 + os: [linux] 951 + 952 + '@rollup/rollup-linux-arm-musleabihf@4.55.3': 953 + resolution: {integrity: sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==} 954 + cpu: [arm] 955 + os: [linux] 956 + 957 + '@rollup/rollup-linux-arm64-gnu@4.55.3': 958 + resolution: {integrity: sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==} 959 + cpu: [arm64] 960 + os: [linux] 961 + 962 + '@rollup/rollup-linux-arm64-musl@4.55.3': 963 + resolution: {integrity: sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==} 964 + cpu: [arm64] 965 + os: [linux] 966 + 967 + '@rollup/rollup-linux-loong64-gnu@4.55.3': 968 + resolution: {integrity: sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==} 969 + cpu: [loong64] 970 + os: [linux] 971 + 972 + '@rollup/rollup-linux-loong64-musl@4.55.3': 973 + resolution: {integrity: sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==} 974 + cpu: [loong64] 975 + os: [linux] 976 + 977 + '@rollup/rollup-linux-ppc64-gnu@4.55.3': 978 + resolution: {integrity: sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==} 979 + cpu: [ppc64] 980 + os: [linux] 981 + 982 + '@rollup/rollup-linux-ppc64-musl@4.55.3': 983 + resolution: {integrity: sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==} 984 + cpu: [ppc64] 985 + os: [linux] 986 + 987 + '@rollup/rollup-linux-riscv64-gnu@4.55.3': 988 + resolution: {integrity: sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==} 989 + cpu: [riscv64] 990 + os: [linux] 991 + 992 + '@rollup/rollup-linux-riscv64-musl@4.55.3': 993 + resolution: {integrity: sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==} 994 + cpu: [riscv64] 995 + os: [linux] 996 + 997 + '@rollup/rollup-linux-s390x-gnu@4.55.3': 998 + resolution: {integrity: sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==} 999 + cpu: [s390x] 1000 + os: [linux] 1001 + 1002 + '@rollup/rollup-linux-x64-gnu@4.55.3': 1003 + resolution: {integrity: sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==} 1004 + cpu: [x64] 1005 + os: [linux] 1006 + 1007 + '@rollup/rollup-linux-x64-musl@4.55.3': 1008 + resolution: {integrity: sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==} 1009 + cpu: [x64] 1010 + os: [linux] 1011 + 1012 + '@rollup/rollup-openbsd-x64@4.55.3': 1013 + resolution: {integrity: sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==} 1014 + cpu: [x64] 1015 + os: [openbsd] 1016 + 1017 + '@rollup/rollup-openharmony-arm64@4.55.3': 1018 + resolution: {integrity: sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw==} 1019 + cpu: [arm64] 1020 + os: [openharmony] 1021 + 1022 + '@rollup/rollup-win32-arm64-msvc@4.55.3': 1023 + resolution: {integrity: sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A==} 1024 + cpu: [arm64] 1025 + os: [win32] 1026 + 1027 + '@rollup/rollup-win32-ia32-msvc@4.55.3': 1028 + resolution: {integrity: sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA==} 1029 + cpu: [ia32] 1030 + os: [win32] 1031 + 1032 + '@rollup/rollup-win32-x64-gnu@4.55.3': 1033 + resolution: {integrity: sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA==} 1034 + cpu: [x64] 1035 + os: [win32] 1036 + 1037 + '@rollup/rollup-win32-x64-msvc@4.55.3': 1038 + resolution: {integrity: sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg==} 1039 + cpu: [x64] 1040 + os: [win32] 1041 + 1042 + '@sindresorhus/is@7.2.0': 1043 + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} 1044 + engines: {node: '>=18'} 1045 + 1046 + '@speed-highlight/core@1.2.14': 1047 + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} 1048 + 1049 + '@standard-schema/spec@1.1.0': 1050 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 1051 + 1052 + '@tailwindcss/node@4.1.18': 1053 + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} 1054 + 1055 + '@tailwindcss/oxide-android-arm64@4.1.18': 1056 + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} 1057 + engines: {node: '>= 10'} 1058 + cpu: [arm64] 1059 + os: [android] 1060 + 1061 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 1062 + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} 1063 + engines: {node: '>= 10'} 1064 + cpu: [arm64] 1065 + os: [darwin] 1066 + 1067 + '@tailwindcss/oxide-darwin-x64@4.1.18': 1068 + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} 1069 + engines: {node: '>= 10'} 1070 + cpu: [x64] 1071 + os: [darwin] 1072 + 1073 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 1074 + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} 1075 + engines: {node: '>= 10'} 1076 + cpu: [x64] 1077 + os: [freebsd] 1078 + 1079 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 1080 + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} 1081 + engines: {node: '>= 10'} 1082 + cpu: [arm] 1083 + os: [linux] 1084 + 1085 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 1086 + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} 1087 + engines: {node: '>= 10'} 1088 + cpu: [arm64] 1089 + os: [linux] 1090 + 1091 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 1092 + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} 1093 + engines: {node: '>= 10'} 1094 + cpu: [arm64] 1095 + os: [linux] 1096 + 1097 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 1098 + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} 1099 + engines: {node: '>= 10'} 1100 + cpu: [x64] 1101 + os: [linux] 1102 + 1103 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 1104 + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} 1105 + engines: {node: '>= 10'} 1106 + cpu: [x64] 1107 + os: [linux] 1108 + 1109 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 1110 + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} 1111 + engines: {node: '>=14.0.0'} 1112 + cpu: [wasm32] 1113 + bundledDependencies: 1114 + - '@napi-rs/wasm-runtime' 1115 + - '@emnapi/core' 1116 + - '@emnapi/runtime' 1117 + - '@tybys/wasm-util' 1118 + - '@emnapi/wasi-threads' 1119 + - tslib 1120 + 1121 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 1122 + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} 1123 + engines: {node: '>= 10'} 1124 + cpu: [arm64] 1125 + os: [win32] 1126 + 1127 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 1128 + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} 1129 + engines: {node: '>= 10'} 1130 + cpu: [x64] 1131 + os: [win32] 1132 + 1133 + '@tailwindcss/oxide@4.1.18': 1134 + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} 1135 + engines: {node: '>= 10'} 1136 + 1137 + '@tailwindcss/vite@4.1.18': 1138 + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} 1139 + peerDependencies: 1140 + vite: ^5.2.0 || ^6 || ^7 1141 + 1142 + '@tybys/wasm-util@0.10.1': 1143 + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} 1144 + 1145 + '@types/babel__core@7.20.5': 1146 + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 1147 + 1148 + '@types/babel__generator@7.27.0': 1149 + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} 1150 + 1151 + '@types/babel__template@7.4.4': 1152 + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 1153 + 1154 + '@types/babel__traverse@7.28.0': 1155 + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} 1156 + 1157 + '@types/chai@5.2.3': 1158 + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 1159 + 1160 + '@types/deep-eql@4.0.2': 1161 + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 1162 + 1163 + '@types/estree@1.0.8': 1164 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 1165 + 1166 + '@types/node@24.10.9': 1167 + resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} 1168 + 1169 + '@types/semver@7.7.1': 1170 + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} 1171 + 1172 + '@vitest/expect@4.0.17': 1173 + resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==} 1174 + 1175 + '@vitest/mocker@4.0.17': 1176 + resolution: {integrity: sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==} 1177 + peerDependencies: 1178 + msw: ^2.4.9 1179 + vite: ^6.0.0 || ^7.0.0-0 1180 + peerDependenciesMeta: 1181 + msw: 1182 + optional: true 1183 + vite: 1184 + optional: true 1185 + 1186 + '@vitest/pretty-format@4.0.17': 1187 + resolution: {integrity: sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==} 1188 + 1189 + '@vitest/runner@4.0.17': 1190 + resolution: {integrity: sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==} 1191 + 1192 + '@vitest/snapshot@4.0.17': 1193 + resolution: {integrity: sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==} 1194 + 1195 + '@vitest/spy@4.0.17': 1196 + resolution: {integrity: sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==} 1197 + 1198 + '@vitest/utils@4.0.17': 1199 + resolution: {integrity: sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==} 1200 + 1201 + assertion-error@2.0.1: 1202 + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 1203 + engines: {node: '>=12'} 1204 + 1205 + babel-plugin-jsx-dom-expressions@0.40.3: 1206 + resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==} 1207 + peerDependencies: 1208 + '@babel/core': ^7.20.12 1209 + 1210 + babel-preset-solid@1.9.10: 1211 + resolution: {integrity: sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==} 1212 + peerDependencies: 1213 + '@babel/core': ^7.0.0 1214 + solid-js: ^1.9.10 1215 + peerDependenciesMeta: 1216 + solid-js: 1217 + optional: true 1218 + 1219 + baseline-browser-mapping@2.9.17: 1220 + resolution: {integrity: sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==} 1221 + hasBin: true 1222 + 1223 + blake3-wasm@2.1.5: 1224 + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 1225 + 1226 + browserslist@4.28.1: 1227 + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} 1228 + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 1229 + hasBin: true 1230 + 1231 + caniuse-lite@1.0.30001765: 1232 + resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==} 1233 + 1234 + chai@6.2.2: 1235 + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} 1236 + engines: {node: '>=18'} 1237 + 1238 + convert-source-map@2.0.0: 1239 + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 1240 + 1241 + cookie@1.1.1: 1242 + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} 1243 + engines: {node: '>=18'} 1244 + 1245 + csstype@3.2.3: 1246 + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 1247 + 1248 + debug@4.4.3: 1249 + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 1250 + engines: {node: '>=6.0'} 1251 + peerDependencies: 1252 + supports-color: '*' 1253 + peerDependenciesMeta: 1254 + supports-color: 1255 + optional: true 1256 + 1257 + dequal@2.0.3: 1258 + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 1259 + engines: {node: '>=6'} 1260 + 1261 + detect-libc@2.1.2: 1262 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 1263 + engines: {node: '>=8'} 1264 + 1265 + electron-to-chromium@1.5.267: 1266 + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} 1267 + 1268 + enhanced-resolve@5.18.4: 1269 + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} 1270 + engines: {node: '>=10.13.0'} 1271 + 1272 + entities@6.0.1: 1273 + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} 1274 + engines: {node: '>=0.12'} 1275 + 1276 + error-stack-parser-es@1.0.5: 1277 + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} 1278 + 1279 + es-module-lexer@1.7.0: 1280 + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 1281 + 1282 + esbuild@0.27.0: 1283 + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} 1284 + engines: {node: '>=18'} 1285 + hasBin: true 1286 + 1287 + esbuild@0.27.2: 1288 + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} 1289 + engines: {node: '>=18'} 1290 + hasBin: true 1291 + 1292 + escalade@3.2.0: 1293 + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 1294 + engines: {node: '>=6'} 1295 + 1296 + estree-walker@3.0.3: 1297 + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 1298 + 1299 + expect-type@1.3.0: 1300 + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 1301 + engines: {node: '>=12.0.0'} 1302 + 1303 + fdir@6.5.0: 1304 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 1305 + engines: {node: '>=12.0.0'} 1306 + peerDependencies: 1307 + picomatch: ^3 || ^4 1308 + peerDependenciesMeta: 1309 + picomatch: 1310 + optional: true 1311 + 1312 + fsevents@2.3.3: 1313 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 1314 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 1315 + os: [darwin] 1316 + 1317 + gensync@1.0.0-beta.2: 1318 + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 1319 + engines: {node: '>=6.9.0'} 1320 + 1321 + glob-to-regex.js@1.2.0: 1322 + resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} 1323 + engines: {node: '>=10.0'} 1324 + peerDependencies: 1325 + tslib: '2' 1326 + 1327 + graceful-fs@4.2.11: 1328 + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 1329 + 1330 + html-entities@2.3.3: 1331 + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} 1332 + 1333 + hyperdyperid@1.2.0: 1334 + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} 1335 + engines: {node: '>=10.18'} 1336 + 1337 + is-what@4.1.16: 1338 + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} 1339 + engines: {node: '>=12.13'} 1340 + 1341 + jiti@2.6.1: 1342 + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 1343 + hasBin: true 1344 + 1345 + js-tokens@4.0.0: 1346 + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 1347 + 1348 + jsesc@3.1.0: 1349 + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 1350 + engines: {node: '>=6'} 1351 + hasBin: true 1352 + 1353 + json5@2.2.3: 1354 + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 1355 + engines: {node: '>=6'} 1356 + hasBin: true 1357 + 1358 + kleur@4.1.5: 1359 + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 1360 + engines: {node: '>=6'} 1361 + 1362 + lightningcss-android-arm64@1.30.2: 1363 + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} 1364 + engines: {node: '>= 12.0.0'} 1365 + cpu: [arm64] 1366 + os: [android] 1367 + 1368 + lightningcss-android-arm64@1.31.1: 1369 + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} 1370 + engines: {node: '>= 12.0.0'} 1371 + cpu: [arm64] 1372 + os: [android] 1373 + 1374 + lightningcss-darwin-arm64@1.30.2: 1375 + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} 1376 + engines: {node: '>= 12.0.0'} 1377 + cpu: [arm64] 1378 + os: [darwin] 1379 + 1380 + lightningcss-darwin-arm64@1.31.1: 1381 + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} 1382 + engines: {node: '>= 12.0.0'} 1383 + cpu: [arm64] 1384 + os: [darwin] 1385 + 1386 + lightningcss-darwin-x64@1.30.2: 1387 + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} 1388 + engines: {node: '>= 12.0.0'} 1389 + cpu: [x64] 1390 + os: [darwin] 1391 + 1392 + lightningcss-darwin-x64@1.31.1: 1393 + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} 1394 + engines: {node: '>= 12.0.0'} 1395 + cpu: [x64] 1396 + os: [darwin] 1397 + 1398 + lightningcss-freebsd-x64@1.30.2: 1399 + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} 1400 + engines: {node: '>= 12.0.0'} 1401 + cpu: [x64] 1402 + os: [freebsd] 1403 + 1404 + lightningcss-freebsd-x64@1.31.1: 1405 + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} 1406 + engines: {node: '>= 12.0.0'} 1407 + cpu: [x64] 1408 + os: [freebsd] 1409 + 1410 + lightningcss-linux-arm-gnueabihf@1.30.2: 1411 + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} 1412 + engines: {node: '>= 12.0.0'} 1413 + cpu: [arm] 1414 + os: [linux] 1415 + 1416 + lightningcss-linux-arm-gnueabihf@1.31.1: 1417 + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} 1418 + engines: {node: '>= 12.0.0'} 1419 + cpu: [arm] 1420 + os: [linux] 1421 + 1422 + lightningcss-linux-arm64-gnu@1.30.2: 1423 + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} 1424 + engines: {node: '>= 12.0.0'} 1425 + cpu: [arm64] 1426 + os: [linux] 1427 + 1428 + lightningcss-linux-arm64-gnu@1.31.1: 1429 + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} 1430 + engines: {node: '>= 12.0.0'} 1431 + cpu: [arm64] 1432 + os: [linux] 1433 + 1434 + lightningcss-linux-arm64-musl@1.30.2: 1435 + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} 1436 + engines: {node: '>= 12.0.0'} 1437 + cpu: [arm64] 1438 + os: [linux] 1439 + 1440 + lightningcss-linux-arm64-musl@1.31.1: 1441 + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} 1442 + engines: {node: '>= 12.0.0'} 1443 + cpu: [arm64] 1444 + os: [linux] 1445 + 1446 + lightningcss-linux-x64-gnu@1.30.2: 1447 + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} 1448 + engines: {node: '>= 12.0.0'} 1449 + cpu: [x64] 1450 + os: [linux] 1451 + 1452 + lightningcss-linux-x64-gnu@1.31.1: 1453 + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} 1454 + engines: {node: '>= 12.0.0'} 1455 + cpu: [x64] 1456 + os: [linux] 1457 + 1458 + lightningcss-linux-x64-musl@1.30.2: 1459 + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} 1460 + engines: {node: '>= 12.0.0'} 1461 + cpu: [x64] 1462 + os: [linux] 1463 + 1464 + lightningcss-linux-x64-musl@1.31.1: 1465 + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} 1466 + engines: {node: '>= 12.0.0'} 1467 + cpu: [x64] 1468 + os: [linux] 1469 + 1470 + lightningcss-win32-arm64-msvc@1.30.2: 1471 + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} 1472 + engines: {node: '>= 12.0.0'} 1473 + cpu: [arm64] 1474 + os: [win32] 1475 + 1476 + lightningcss-win32-arm64-msvc@1.31.1: 1477 + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} 1478 + engines: {node: '>= 12.0.0'} 1479 + cpu: [arm64] 1480 + os: [win32] 1481 + 1482 + lightningcss-win32-x64-msvc@1.30.2: 1483 + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} 1484 + engines: {node: '>= 12.0.0'} 1485 + cpu: [x64] 1486 + os: [win32] 1487 + 1488 + lightningcss-win32-x64-msvc@1.31.1: 1489 + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} 1490 + engines: {node: '>= 12.0.0'} 1491 + cpu: [x64] 1492 + os: [win32] 1493 + 1494 + lightningcss@1.30.2: 1495 + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} 1496 + engines: {node: '>= 12.0.0'} 1497 + 1498 + lightningcss@1.31.1: 1499 + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} 1500 + engines: {node: '>= 12.0.0'} 1501 + 1502 + lru-cache@5.1.1: 1503 + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 1504 + 1505 + magic-string@0.30.21: 1506 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1507 + 1508 + memfs@4.56.9: 1509 + resolution: {integrity: sha512-Fo+xSG0MhtaPyEi7B2AEj4omBen3kb5RCeFKaM/YVsxgO8vkcpX0tkheRIoCGqXw9oAnFQRe1oWuR1xq4oE17A==} 1510 + peerDependencies: 1511 + tslib: '2' 1512 + 1513 + merge-anything@5.1.7: 1514 + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} 1515 + engines: {node: '>=12.13'} 1516 + 1517 + miniflare@4.20260120.0: 1518 + resolution: {integrity: sha512-XXZyE2pDKMtP5OLuv0LPHEAzIYhov4jrYjcqrhhqtxGGtXneWOHvXIPo+eV8sqwqWd3R7j4DlEKcyb+87BR49Q==} 1519 + engines: {node: '>=18.0.0'} 1520 + hasBin: true 1521 + 1522 + ms@2.1.3: 1523 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1524 + 1525 + nanoid@3.3.11: 1526 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 1527 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1528 + hasBin: true 1529 + 1530 + node-releases@2.0.27: 1531 + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} 1532 + 1533 + obug@2.1.1: 1534 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 1535 + 1536 + oxfmt@0.26.0: 1537 + resolution: {integrity: sha512-UDD1wFNwfeorMm2ZY0xy1KRAAvJ5NjKBfbDmiMwGP7baEHTq65cYpC0aPP+BGHc8weXUbSZaK8MdGyvuRUvS4Q==} 1538 + engines: {node: ^20.19.0 || >=22.12.0} 1539 + hasBin: true 1540 + 1541 + oxlint@1.41.0: 1542 + resolution: {integrity: sha512-Dyaoup82uhgAgp5xLNt4dPdvl5eSJTIzqzL7DcKbkooUE4PDViWURIPlSUF8hu5a+sCnNIp/LlQMDsKoyaLTBA==} 1543 + engines: {node: ^20.19.0 || >=22.12.0} 1544 + hasBin: true 1545 + peerDependencies: 1546 + oxlint-tsgolint: '>=0.11.1' 1547 + peerDependenciesMeta: 1548 + oxlint-tsgolint: 1549 + optional: true 1550 + 1551 + parse5@7.3.0: 1552 + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 1553 + 1554 + path-to-regexp@6.3.0: 1555 + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 1556 + 1557 + pathe@2.0.3: 1558 + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 1559 + 1560 + picocolors@1.1.1: 1561 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1562 + 1563 + picomatch@4.0.3: 1564 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 1565 + engines: {node: '>=12'} 1566 + 1567 + postcss@8.5.6: 1568 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1569 + engines: {node: ^10 || ^12 || >=14} 1570 + 1571 + rollup@4.55.3: 1572 + resolution: {integrity: sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==} 1573 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1574 + hasBin: true 1575 + 1576 + semver@6.3.1: 1577 + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1578 + hasBin: true 1579 + 1580 + semver@7.7.3: 1581 + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 1582 + engines: {node: '>=10'} 1583 + hasBin: true 1584 + 1585 + seroval-plugins@1.3.3: 1586 + resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} 1587 + engines: {node: '>=10'} 1588 + peerDependencies: 1589 + seroval: ^1.0 1590 + 1591 + seroval@1.3.2: 1592 + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} 1593 + engines: {node: '>=10'} 1594 + 1595 + sharp@0.34.5: 1596 + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} 1597 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1598 + 1599 + siginfo@2.0.0: 1600 + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1601 + 1602 + solid-js@1.9.10: 1603 + resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==} 1604 + 1605 + solid-refresh@0.6.3: 1606 + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} 1607 + peerDependencies: 1608 + solid-js: ^1.3 1609 + 1610 + source-map-js@1.2.1: 1611 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1612 + engines: {node: '>=0.10.0'} 1613 + 1614 + stackback@0.0.2: 1615 + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1616 + 1617 + std-env@3.10.0: 1618 + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 1619 + 1620 + supports-color@10.2.2: 1621 + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} 1622 + engines: {node: '>=18'} 1623 + 1624 + tailwindcss@4.1.18: 1625 + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} 1626 + 1627 + tapable@2.3.0: 1628 + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 1629 + engines: {node: '>=6'} 1630 + 1631 + thingies@2.5.0: 1632 + resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} 1633 + engines: {node: '>=10.18'} 1634 + peerDependencies: 1635 + tslib: ^2 1636 + 1637 + tinybench@2.9.0: 1638 + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1639 + 1640 + tinyexec@1.0.2: 1641 + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} 1642 + engines: {node: '>=18'} 1643 + 1644 + tinyglobby@0.2.15: 1645 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 1646 + engines: {node: '>=12.0.0'} 1647 + 1648 + tinypool@2.0.0: 1649 + resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==} 1650 + engines: {node: ^20.0.0 || >=22.0.0} 1651 + 1652 + tinyrainbow@3.0.3: 1653 + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} 1654 + engines: {node: '>=14.0.0'} 1655 + 1656 + tree-dump@1.1.0: 1657 + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} 1658 + engines: {node: '>=10.0'} 1659 + peerDependencies: 1660 + tslib: '2' 1661 + 1662 + tslib@2.8.1: 1663 + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1664 + 1665 + typescript@5.9.3: 1666 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1667 + engines: {node: '>=14.17'} 1668 + hasBin: true 1669 + 1670 + undici-types@7.16.0: 1671 + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 1672 + 1673 + undici@7.18.2: 1674 + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} 1675 + engines: {node: '>=20.18.1'} 1676 + 1677 + unenv@2.0.0-rc.24: 1678 + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} 1679 + 1680 + update-browserslist-db@1.2.3: 1681 + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} 1682 + hasBin: true 1683 + peerDependencies: 1684 + browserslist: '>= 4.21.0' 1685 + 1686 + valibot@1.2.0: 1687 + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} 1688 + peerDependencies: 1689 + typescript: '>=5' 1690 + peerDependenciesMeta: 1691 + typescript: 1692 + optional: true 1693 + 1694 + vite-plugin-solid@2.11.10: 1695 + resolution: {integrity: sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==} 1696 + peerDependencies: 1697 + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* 1698 + solid-js: ^1.7.2 1699 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 1700 + peerDependenciesMeta: 1701 + '@testing-library/jest-dom': 1702 + optional: true 1703 + 1704 + vite@7.3.1: 1705 + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} 1706 + engines: {node: ^20.19.0 || >=22.12.0} 1707 + hasBin: true 1708 + peerDependencies: 1709 + '@types/node': ^20.19.0 || >=22.12.0 1710 + jiti: '>=1.21.0' 1711 + less: ^4.0.0 1712 + lightningcss: ^1.21.0 1713 + sass: ^1.70.0 1714 + sass-embedded: ^1.70.0 1715 + stylus: '>=0.54.8' 1716 + sugarss: ^5.0.0 1717 + terser: ^5.16.0 1718 + tsx: ^4.8.1 1719 + yaml: ^2.4.2 1720 + peerDependenciesMeta: 1721 + '@types/node': 1722 + optional: true 1723 + jiti: 1724 + optional: true 1725 + less: 1726 + optional: true 1727 + lightningcss: 1728 + optional: true 1729 + sass: 1730 + optional: true 1731 + sass-embedded: 1732 + optional: true 1733 + stylus: 1734 + optional: true 1735 + sugarss: 1736 + optional: true 1737 + terser: 1738 + optional: true 1739 + tsx: 1740 + optional: true 1741 + yaml: 1742 + optional: true 1743 + 1744 + vitefu@1.1.1: 1745 + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} 1746 + peerDependencies: 1747 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 1748 + peerDependenciesMeta: 1749 + vite: 1750 + optional: true 1751 + 1752 + vitest@4.0.17: 1753 + resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==} 1754 + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} 1755 + hasBin: true 1756 + peerDependencies: 1757 + '@edge-runtime/vm': '*' 1758 + '@opentelemetry/api': ^1.9.0 1759 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 1760 + '@vitest/browser-playwright': 4.0.17 1761 + '@vitest/browser-preview': 4.0.17 1762 + '@vitest/browser-webdriverio': 4.0.17 1763 + '@vitest/ui': 4.0.17 1764 + happy-dom: '*' 1765 + jsdom: '*' 1766 + peerDependenciesMeta: 1767 + '@edge-runtime/vm': 1768 + optional: true 1769 + '@opentelemetry/api': 1770 + optional: true 1771 + '@types/node': 1772 + optional: true 1773 + '@vitest/browser-playwright': 1774 + optional: true 1775 + '@vitest/browser-preview': 1776 + optional: true 1777 + '@vitest/browser-webdriverio': 1778 + optional: true 1779 + '@vitest/ui': 1780 + optional: true 1781 + happy-dom: 1782 + optional: true 1783 + jsdom: 1784 + optional: true 1785 + 1786 + why-is-node-running@2.3.0: 1787 + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1788 + engines: {node: '>=8'} 1789 + hasBin: true 1790 + 1791 + workerd@1.20260120.0: 1792 + resolution: {integrity: sha512-R6X/VQOkwLTBGLp4VRUwLQZZVxZ9T9J8pGiJ6GQUMaRkY7TVWrCSkVfoNMM1/YyFsY5UYhhPoQe5IehnhZ3Pdw==} 1793 + engines: {node: '>=16'} 1794 + hasBin: true 1795 + 1796 + wrangler@4.60.0: 1797 + resolution: {integrity: sha512-n4kibm/xY0Qd5G2K/CbAQeVeOIlwPNVglmFjlDRCCYk3hZh8IggO/rg8AXt/vByK2Sxsugl5Z7yvgWxrUbmS6g==} 1798 + engines: {node: '>=20.0.0'} 1799 + hasBin: true 1800 + peerDependencies: 1801 + '@cloudflare/workers-types': ^4.20260120.0 1802 + peerDependenciesMeta: 1803 + '@cloudflare/workers-types': 1804 + optional: true 1805 + 1806 + ws@8.18.0: 1807 + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 1808 + engines: {node: '>=10.0.0'} 1809 + peerDependencies: 1810 + bufferutil: ^4.0.1 1811 + utf-8-validate: '>=5.0.2' 1812 + peerDependenciesMeta: 1813 + bufferutil: 1814 + optional: true 1815 + utf-8-validate: 1816 + optional: true 1817 + 1818 + yallist@3.1.1: 1819 + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1820 + 1821 + youch-core@0.3.3: 1822 + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} 1823 + 1824 + youch@4.1.0-beta.10: 1825 + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} 1826 + 1827 + zod@3.25.76: 1828 + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 1829 + 1830 + snapshots: 1831 + 1832 + '@atcute/uint8array@1.0.6': {} 1833 + 1834 + '@babel/code-frame@7.28.6': 1835 + dependencies: 1836 + '@babel/helper-validator-identifier': 7.28.5 1837 + js-tokens: 4.0.0 1838 + picocolors: 1.1.1 1839 + 1840 + '@babel/compat-data@7.28.6': {} 1841 + 1842 + '@babel/core@7.28.6': 1843 + dependencies: 1844 + '@babel/code-frame': 7.28.6 1845 + '@babel/generator': 7.28.6 1846 + '@babel/helper-compilation-targets': 7.28.6 1847 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) 1848 + '@babel/helpers': 7.28.6 1849 + '@babel/parser': 7.28.6 1850 + '@babel/template': 7.28.6 1851 + '@babel/traverse': 7.28.6 1852 + '@babel/types': 7.28.6 1853 + '@jridgewell/remapping': 2.3.5 1854 + convert-source-map: 2.0.0 1855 + debug: 4.4.3 1856 + gensync: 1.0.0-beta.2 1857 + json5: 2.2.3 1858 + semver: 6.3.1 1859 + transitivePeerDependencies: 1860 + - supports-color 1861 + 1862 + '@babel/generator@7.28.6': 1863 + dependencies: 1864 + '@babel/parser': 7.28.6 1865 + '@babel/types': 7.28.6 1866 + '@jridgewell/gen-mapping': 0.3.13 1867 + '@jridgewell/trace-mapping': 0.3.31 1868 + jsesc: 3.1.0 1869 + 1870 + '@babel/helper-compilation-targets@7.28.6': 1871 + dependencies: 1872 + '@babel/compat-data': 7.28.6 1873 + '@babel/helper-validator-option': 7.27.1 1874 + browserslist: 4.28.1 1875 + lru-cache: 5.1.1 1876 + semver: 6.3.1 1877 + 1878 + '@babel/helper-globals@7.28.0': {} 1879 + 1880 + '@babel/helper-module-imports@7.18.6': 1881 + dependencies: 1882 + '@babel/types': 7.28.6 1883 + 1884 + '@babel/helper-module-imports@7.28.6': 1885 + dependencies: 1886 + '@babel/traverse': 7.28.6 1887 + '@babel/types': 7.28.6 1888 + transitivePeerDependencies: 1889 + - supports-color 1890 + 1891 + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)': 1892 + dependencies: 1893 + '@babel/core': 7.28.6 1894 + '@babel/helper-module-imports': 7.28.6 1895 + '@babel/helper-validator-identifier': 7.28.5 1896 + '@babel/traverse': 7.28.6 1897 + transitivePeerDependencies: 1898 + - supports-color 1899 + 1900 + '@babel/helper-plugin-utils@7.28.6': {} 1901 + 1902 + '@babel/helper-string-parser@7.27.1': {} 1903 + 1904 + '@babel/helper-validator-identifier@7.28.5': {} 1905 + 1906 + '@babel/helper-validator-option@7.27.1': {} 1907 + 1908 + '@babel/helpers@7.28.6': 1909 + dependencies: 1910 + '@babel/template': 7.28.6 1911 + '@babel/types': 7.28.6 1912 + 1913 + '@babel/parser@7.28.6': 1914 + dependencies: 1915 + '@babel/types': 7.28.6 1916 + 1917 + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.6)': 1918 + dependencies: 1919 + '@babel/core': 7.28.6 1920 + '@babel/helper-plugin-utils': 7.28.6 1921 + 1922 + '@babel/template@7.28.6': 1923 + dependencies: 1924 + '@babel/code-frame': 7.28.6 1925 + '@babel/parser': 7.28.6 1926 + '@babel/types': 7.28.6 1927 + 1928 + '@babel/traverse@7.28.6': 1929 + dependencies: 1930 + '@babel/code-frame': 7.28.6 1931 + '@babel/generator': 7.28.6 1932 + '@babel/helper-globals': 7.28.0 1933 + '@babel/parser': 7.28.6 1934 + '@babel/template': 7.28.6 1935 + '@babel/types': 7.28.6 1936 + debug: 4.4.3 1937 + transitivePeerDependencies: 1938 + - supports-color 1939 + 1940 + '@babel/types@7.28.6': 1941 + dependencies: 1942 + '@babel/helper-string-parser': 7.27.1 1943 + '@babel/helper-validator-identifier': 7.28.5 1944 + 1945 + '@cloudflare/kv-asset-handler@0.4.2': {} 1946 + 1947 + '@cloudflare/unenv-preset@2.11.0(unenv@2.0.0-rc.24)(workerd@1.20260120.0)': 1948 + dependencies: 1949 + unenv: 2.0.0-rc.24 1950 + optionalDependencies: 1951 + workerd: 1.20260120.0 1952 + 1953 + '@cloudflare/workerd-darwin-64@1.20260120.0': 1954 + optional: true 1955 + 1956 + '@cloudflare/workerd-darwin-arm64@1.20260120.0': 1957 + optional: true 1958 + 1959 + '@cloudflare/workerd-linux-64@1.20260120.0': 1960 + optional: true 1961 + 1962 + '@cloudflare/workerd-linux-arm64@1.20260120.0': 1963 + optional: true 1964 + 1965 + '@cloudflare/workerd-windows-64@1.20260120.0': 1966 + optional: true 1967 + 1968 + '@cspotcode/source-map-support@0.8.1': 1969 + dependencies: 1970 + '@jridgewell/trace-mapping': 0.3.9 1971 + 1972 + '@emnapi/core@1.8.1': 1973 + dependencies: 1974 + '@emnapi/wasi-threads': 1.1.0 1975 + tslib: 2.8.1 1976 + 1977 + '@emnapi/runtime@1.8.1': 1978 + dependencies: 1979 + tslib: 2.8.1 1980 + 1981 + '@emnapi/wasi-threads@1.1.0': 1982 + dependencies: 1983 + tslib: 2.8.1 1984 + 1985 + '@esbuild/aix-ppc64@0.27.0': 1986 + optional: true 1987 + 1988 + '@esbuild/aix-ppc64@0.27.2': 1989 + optional: true 1990 + 1991 + '@esbuild/android-arm64@0.27.0': 1992 + optional: true 1993 + 1994 + '@esbuild/android-arm64@0.27.2': 1995 + optional: true 1996 + 1997 + '@esbuild/android-arm@0.27.0': 1998 + optional: true 1999 + 2000 + '@esbuild/android-arm@0.27.2': 2001 + optional: true 2002 + 2003 + '@esbuild/android-x64@0.27.0': 2004 + optional: true 2005 + 2006 + '@esbuild/android-x64@0.27.2': 2007 + optional: true 2008 + 2009 + '@esbuild/darwin-arm64@0.27.0': 2010 + optional: true 2011 + 2012 + '@esbuild/darwin-arm64@0.27.2': 2013 + optional: true 2014 + 2015 + '@esbuild/darwin-x64@0.27.0': 2016 + optional: true 2017 + 2018 + '@esbuild/darwin-x64@0.27.2': 2019 + optional: true 2020 + 2021 + '@esbuild/freebsd-arm64@0.27.0': 2022 + optional: true 2023 + 2024 + '@esbuild/freebsd-arm64@0.27.2': 2025 + optional: true 2026 + 2027 + '@esbuild/freebsd-x64@0.27.0': 2028 + optional: true 2029 + 2030 + '@esbuild/freebsd-x64@0.27.2': 2031 + optional: true 2032 + 2033 + '@esbuild/linux-arm64@0.27.0': 2034 + optional: true 2035 + 2036 + '@esbuild/linux-arm64@0.27.2': 2037 + optional: true 2038 + 2039 + '@esbuild/linux-arm@0.27.0': 2040 + optional: true 2041 + 2042 + '@esbuild/linux-arm@0.27.2': 2043 + optional: true 2044 + 2045 + '@esbuild/linux-ia32@0.27.0': 2046 + optional: true 2047 + 2048 + '@esbuild/linux-ia32@0.27.2': 2049 + optional: true 2050 + 2051 + '@esbuild/linux-loong64@0.27.0': 2052 + optional: true 2053 + 2054 + '@esbuild/linux-loong64@0.27.2': 2055 + optional: true 2056 + 2057 + '@esbuild/linux-mips64el@0.27.0': 2058 + optional: true 2059 + 2060 + '@esbuild/linux-mips64el@0.27.2': 2061 + optional: true 2062 + 2063 + '@esbuild/linux-ppc64@0.27.0': 2064 + optional: true 2065 + 2066 + '@esbuild/linux-ppc64@0.27.2': 2067 + optional: true 2068 + 2069 + '@esbuild/linux-riscv64@0.27.0': 2070 + optional: true 2071 + 2072 + '@esbuild/linux-riscv64@0.27.2': 2073 + optional: true 2074 + 2075 + '@esbuild/linux-s390x@0.27.0': 2076 + optional: true 2077 + 2078 + '@esbuild/linux-s390x@0.27.2': 2079 + optional: true 2080 + 2081 + '@esbuild/linux-x64@0.27.0': 2082 + optional: true 2083 + 2084 + '@esbuild/linux-x64@0.27.2': 2085 + optional: true 2086 + 2087 + '@esbuild/netbsd-arm64@0.27.0': 2088 + optional: true 2089 + 2090 + '@esbuild/netbsd-arm64@0.27.2': 2091 + optional: true 2092 + 2093 + '@esbuild/netbsd-x64@0.27.0': 2094 + optional: true 2095 + 2096 + '@esbuild/netbsd-x64@0.27.2': 2097 + optional: true 2098 + 2099 + '@esbuild/openbsd-arm64@0.27.0': 2100 + optional: true 2101 + 2102 + '@esbuild/openbsd-arm64@0.27.2': 2103 + optional: true 2104 + 2105 + '@esbuild/openbsd-x64@0.27.0': 2106 + optional: true 2107 + 2108 + '@esbuild/openbsd-x64@0.27.2': 2109 + optional: true 2110 + 2111 + '@esbuild/openharmony-arm64@0.27.0': 2112 + optional: true 2113 + 2114 + '@esbuild/openharmony-arm64@0.27.2': 2115 + optional: true 2116 + 2117 + '@esbuild/sunos-x64@0.27.0': 2118 + optional: true 2119 + 2120 + '@esbuild/sunos-x64@0.27.2': 2121 + optional: true 2122 + 2123 + '@esbuild/win32-arm64@0.27.0': 2124 + optional: true 2125 + 2126 + '@esbuild/win32-arm64@0.27.2': 2127 + optional: true 2128 + 2129 + '@esbuild/win32-ia32@0.27.0': 2130 + optional: true 2131 + 2132 + '@esbuild/win32-ia32@0.27.2': 2133 + optional: true 2134 + 2135 + '@esbuild/win32-x64@0.27.0': 2136 + optional: true 2137 + 2138 + '@esbuild/win32-x64@0.27.2': 2139 + optional: true 2140 + 2141 + '@floating-ui/core@1.7.3': 2142 + dependencies: 2143 + '@floating-ui/utils': 0.2.10 2144 + 2145 + '@floating-ui/dom@1.7.4': 2146 + dependencies: 2147 + '@floating-ui/core': 1.7.3 2148 + '@floating-ui/utils': 0.2.10 2149 + 2150 + '@floating-ui/utils@0.2.10': {} 2151 + 2152 + '@img/colour@1.0.0': {} 2153 + 2154 + '@img/sharp-darwin-arm64@0.34.5': 2155 + optionalDependencies: 2156 + '@img/sharp-libvips-darwin-arm64': 1.2.4 2157 + optional: true 2158 + 2159 + '@img/sharp-darwin-x64@0.34.5': 2160 + optionalDependencies: 2161 + '@img/sharp-libvips-darwin-x64': 1.2.4 2162 + optional: true 2163 + 2164 + '@img/sharp-libvips-darwin-arm64@1.2.4': 2165 + optional: true 2166 + 2167 + '@img/sharp-libvips-darwin-x64@1.2.4': 2168 + optional: true 2169 + 2170 + '@img/sharp-libvips-linux-arm64@1.2.4': 2171 + optional: true 2172 + 2173 + '@img/sharp-libvips-linux-arm@1.2.4': 2174 + optional: true 2175 + 2176 + '@img/sharp-libvips-linux-ppc64@1.2.4': 2177 + optional: true 2178 + 2179 + '@img/sharp-libvips-linux-riscv64@1.2.4': 2180 + optional: true 2181 + 2182 + '@img/sharp-libvips-linux-s390x@1.2.4': 2183 + optional: true 2184 + 2185 + '@img/sharp-libvips-linux-x64@1.2.4': 2186 + optional: true 2187 + 2188 + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': 2189 + optional: true 2190 + 2191 + '@img/sharp-libvips-linuxmusl-x64@1.2.4': 2192 + optional: true 2193 + 2194 + '@img/sharp-linux-arm64@0.34.5': 2195 + optionalDependencies: 2196 + '@img/sharp-libvips-linux-arm64': 1.2.4 2197 + optional: true 2198 + 2199 + '@img/sharp-linux-arm@0.34.5': 2200 + optionalDependencies: 2201 + '@img/sharp-libvips-linux-arm': 1.2.4 2202 + optional: true 2203 + 2204 + '@img/sharp-linux-ppc64@0.34.5': 2205 + optionalDependencies: 2206 + '@img/sharp-libvips-linux-ppc64': 1.2.4 2207 + optional: true 2208 + 2209 + '@img/sharp-linux-riscv64@0.34.5': 2210 + optionalDependencies: 2211 + '@img/sharp-libvips-linux-riscv64': 1.2.4 2212 + optional: true 2213 + 2214 + '@img/sharp-linux-s390x@0.34.5': 2215 + optionalDependencies: 2216 + '@img/sharp-libvips-linux-s390x': 1.2.4 2217 + optional: true 2218 + 2219 + '@img/sharp-linux-x64@0.34.5': 2220 + optionalDependencies: 2221 + '@img/sharp-libvips-linux-x64': 1.2.4 2222 + optional: true 2223 + 2224 + '@img/sharp-linuxmusl-arm64@0.34.5': 2225 + optionalDependencies: 2226 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 2227 + optional: true 2228 + 2229 + '@img/sharp-linuxmusl-x64@0.34.5': 2230 + optionalDependencies: 2231 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 2232 + optional: true 2233 + 2234 + '@img/sharp-wasm32@0.34.5': 2235 + dependencies: 2236 + '@emnapi/runtime': 1.8.1 2237 + optional: true 2238 + 2239 + '@img/sharp-win32-arm64@0.34.5': 2240 + optional: true 2241 + 2242 + '@img/sharp-win32-ia32@0.34.5': 2243 + optional: true 2244 + 2245 + '@img/sharp-win32-x64@0.34.5': 2246 + optional: true 2247 + 2248 + '@jridgewell/gen-mapping@0.3.13': 2249 + dependencies: 2250 + '@jridgewell/sourcemap-codec': 1.5.5 2251 + '@jridgewell/trace-mapping': 0.3.31 2252 + 2253 + '@jridgewell/remapping@2.3.5': 2254 + dependencies: 2255 + '@jridgewell/gen-mapping': 0.3.13 2256 + '@jridgewell/trace-mapping': 0.3.31 2257 + 2258 + '@jridgewell/resolve-uri@3.1.2': {} 2259 + 2260 + '@jridgewell/sourcemap-codec@1.5.5': {} 2261 + 2262 + '@jridgewell/trace-mapping@0.3.31': 2263 + dependencies: 2264 + '@jridgewell/resolve-uri': 3.1.2 2265 + '@jridgewell/sourcemap-codec': 1.5.5 2266 + 2267 + '@jridgewell/trace-mapping@0.3.9': 2268 + dependencies: 2269 + '@jridgewell/resolve-uri': 3.1.2 2270 + '@jridgewell/sourcemap-codec': 1.5.5 2271 + 2272 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': 2273 + dependencies: 2274 + tslib: 2.8.1 2275 + 2276 + '@jsonjoy.com/base64@17.65.0(tslib@2.8.1)': 2277 + dependencies: 2278 + tslib: 2.8.1 2279 + 2280 + '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': 2281 + dependencies: 2282 + tslib: 2.8.1 2283 + 2284 + '@jsonjoy.com/buffers@17.65.0(tslib@2.8.1)': 2285 + dependencies: 2286 + tslib: 2.8.1 2287 + 2288 + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': 2289 + dependencies: 2290 + tslib: 2.8.1 2291 + 2292 + '@jsonjoy.com/codegen@17.65.0(tslib@2.8.1)': 2293 + dependencies: 2294 + tslib: 2.8.1 2295 + 2296 + '@jsonjoy.com/fs-core@4.56.9(tslib@2.8.1)': 2297 + dependencies: 2298 + '@jsonjoy.com/fs-node-builtins': 4.56.9(tslib@2.8.1) 2299 + '@jsonjoy.com/fs-node-utils': 4.56.9(tslib@2.8.1) 2300 + thingies: 2.5.0(tslib@2.8.1) 2301 + tslib: 2.8.1 2302 + 2303 + '@jsonjoy.com/fs-fsa@4.56.9(tslib@2.8.1)': 2304 + dependencies: 2305 + '@jsonjoy.com/fs-core': 4.56.9(tslib@2.8.1) 2306 + '@jsonjoy.com/fs-node-builtins': 4.56.9(tslib@2.8.1) 2307 + '@jsonjoy.com/fs-node-utils': 4.56.9(tslib@2.8.1) 2308 + thingies: 2.5.0(tslib@2.8.1) 2309 + tslib: 2.8.1 2310 + 2311 + '@jsonjoy.com/fs-node-builtins@4.56.9(tslib@2.8.1)': 2312 + dependencies: 2313 + tslib: 2.8.1 2314 + 2315 + '@jsonjoy.com/fs-node-to-fsa@4.56.9(tslib@2.8.1)': 2316 + dependencies: 2317 + '@jsonjoy.com/fs-fsa': 4.56.9(tslib@2.8.1) 2318 + '@jsonjoy.com/fs-node-builtins': 4.56.9(tslib@2.8.1) 2319 + '@jsonjoy.com/fs-node-utils': 4.56.9(tslib@2.8.1) 2320 + tslib: 2.8.1 2321 + 2322 + '@jsonjoy.com/fs-node-utils@4.56.9(tslib@2.8.1)': 2323 + dependencies: 2324 + '@jsonjoy.com/fs-node-builtins': 4.56.9(tslib@2.8.1) 2325 + tslib: 2.8.1 2326 + 2327 + '@jsonjoy.com/fs-node@4.56.9(tslib@2.8.1)': 2328 + dependencies: 2329 + '@jsonjoy.com/fs-core': 4.56.9(tslib@2.8.1) 2330 + '@jsonjoy.com/fs-node-builtins': 4.56.9(tslib@2.8.1) 2331 + '@jsonjoy.com/fs-node-utils': 4.56.9(tslib@2.8.1) 2332 + '@jsonjoy.com/fs-print': 4.56.9(tslib@2.8.1) 2333 + glob-to-regex.js: 1.2.0(tslib@2.8.1) 2334 + thingies: 2.5.0(tslib@2.8.1) 2335 + tslib: 2.8.1 2336 + 2337 + '@jsonjoy.com/fs-print@4.56.9(tslib@2.8.1)': 2338 + dependencies: 2339 + '@jsonjoy.com/fs-node-utils': 4.56.9(tslib@2.8.1) 2340 + tree-dump: 1.1.0(tslib@2.8.1) 2341 + tslib: 2.8.1 2342 + 2343 + '@jsonjoy.com/fs-snapshot@4.56.9(tslib@2.8.1)': 2344 + dependencies: 2345 + '@jsonjoy.com/buffers': 17.65.0(tslib@2.8.1) 2346 + '@jsonjoy.com/fs-node-utils': 4.56.9(tslib@2.8.1) 2347 + '@jsonjoy.com/json-pack': 17.65.0(tslib@2.8.1) 2348 + '@jsonjoy.com/util': 17.65.0(tslib@2.8.1) 2349 + tslib: 2.8.1 2350 + 2351 + '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': 2352 + dependencies: 2353 + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) 2354 + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) 2355 + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) 2356 + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) 2357 + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) 2358 + hyperdyperid: 1.2.0 2359 + thingies: 2.5.0(tslib@2.8.1) 2360 + tree-dump: 1.1.0(tslib@2.8.1) 2361 + tslib: 2.8.1 2362 + 2363 + '@jsonjoy.com/json-pack@17.65.0(tslib@2.8.1)': 2364 + dependencies: 2365 + '@jsonjoy.com/base64': 17.65.0(tslib@2.8.1) 2366 + '@jsonjoy.com/buffers': 17.65.0(tslib@2.8.1) 2367 + '@jsonjoy.com/codegen': 17.65.0(tslib@2.8.1) 2368 + '@jsonjoy.com/json-pointer': 17.65.0(tslib@2.8.1) 2369 + '@jsonjoy.com/util': 17.65.0(tslib@2.8.1) 2370 + hyperdyperid: 1.2.0 2371 + thingies: 2.5.0(tslib@2.8.1) 2372 + tree-dump: 1.1.0(tslib@2.8.1) 2373 + tslib: 2.8.1 2374 + 2375 + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': 2376 + dependencies: 2377 + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) 2378 + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) 2379 + tslib: 2.8.1 2380 + 2381 + '@jsonjoy.com/json-pointer@17.65.0(tslib@2.8.1)': 2382 + dependencies: 2383 + '@jsonjoy.com/util': 17.65.0(tslib@2.8.1) 2384 + tslib: 2.8.1 2385 + 2386 + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': 2387 + dependencies: 2388 + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) 2389 + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) 2390 + tslib: 2.8.1 2391 + 2392 + '@jsonjoy.com/util@17.65.0(tslib@2.8.1)': 2393 + dependencies: 2394 + '@jsonjoy.com/buffers': 17.65.0(tslib@2.8.1) 2395 + '@jsonjoy.com/codegen': 17.65.0(tslib@2.8.1) 2396 + tslib: 2.8.1 2397 + 2398 + '@jsr/mary__array-fns@0.1.5': {} 2399 + 2400 + '@jsr/mary__tar@0.3.1': {} 2401 + 2402 + '@napi-rs/wasm-runtime@1.1.1': 2403 + dependencies: 2404 + '@emnapi/core': 1.8.1 2405 + '@emnapi/runtime': 1.8.1 2406 + '@tybys/wasm-util': 0.10.1 2407 + 2408 + '@oxfmt/darwin-arm64@0.26.0': 2409 + optional: true 2410 + 2411 + '@oxfmt/darwin-x64@0.26.0': 2412 + optional: true 2413 + 2414 + '@oxfmt/linux-arm64-gnu@0.26.0': 2415 + optional: true 2416 + 2417 + '@oxfmt/linux-arm64-musl@0.26.0': 2418 + optional: true 2419 + 2420 + '@oxfmt/linux-x64-gnu@0.26.0': 2421 + optional: true 2422 + 2423 + '@oxfmt/linux-x64-musl@0.26.0': 2424 + optional: true 2425 + 2426 + '@oxfmt/win32-arm64@0.26.0': 2427 + optional: true 2428 + 2429 + '@oxfmt/win32-x64@0.26.0': 2430 + optional: true 2431 + 2432 + '@oxlint/darwin-arm64@1.41.0': 2433 + optional: true 2434 + 2435 + '@oxlint/darwin-x64@1.41.0': 2436 + optional: true 2437 + 2438 + '@oxlint/linux-arm64-gnu@1.41.0': 2439 + optional: true 2440 + 2441 + '@oxlint/linux-arm64-musl@1.41.0': 2442 + optional: true 2443 + 2444 + '@oxlint/linux-x64-gnu@1.41.0': 2445 + optional: true 2446 + 2447 + '@oxlint/linux-x64-musl@1.41.0': 2448 + optional: true 2449 + 2450 + '@oxlint/win32-arm64@1.41.0': 2451 + optional: true 2452 + 2453 + '@oxlint/win32-x64@1.41.0': 2454 + optional: true 2455 + 2456 + '@poppinss/colors@4.1.6': 2457 + dependencies: 2458 + kleur: 4.1.5 2459 + 2460 + '@poppinss/dumper@0.6.5': 2461 + dependencies: 2462 + '@poppinss/colors': 4.1.6 2463 + '@sindresorhus/is': 7.2.0 2464 + supports-color: 10.2.2 2465 + 2466 + '@poppinss/exception@1.2.3': {} 2467 + 2468 + '@rolldown/browser@1.0.0-rc.1': 2469 + dependencies: 2470 + '@napi-rs/wasm-runtime': 1.1.1 2471 + 2472 + '@rollup/rollup-android-arm-eabi@4.55.3': 2473 + optional: true 2474 + 2475 + '@rollup/rollup-android-arm64@4.55.3': 2476 + optional: true 2477 + 2478 + '@rollup/rollup-darwin-arm64@4.55.3': 2479 + optional: true 2480 + 2481 + '@rollup/rollup-darwin-x64@4.55.3': 2482 + optional: true 2483 + 2484 + '@rollup/rollup-freebsd-arm64@4.55.3': 2485 + optional: true 2486 + 2487 + '@rollup/rollup-freebsd-x64@4.55.3': 2488 + optional: true 2489 + 2490 + '@rollup/rollup-linux-arm-gnueabihf@4.55.3': 2491 + optional: true 2492 + 2493 + '@rollup/rollup-linux-arm-musleabihf@4.55.3': 2494 + optional: true 2495 + 2496 + '@rollup/rollup-linux-arm64-gnu@4.55.3': 2497 + optional: true 2498 + 2499 + '@rollup/rollup-linux-arm64-musl@4.55.3': 2500 + optional: true 2501 + 2502 + '@rollup/rollup-linux-loong64-gnu@4.55.3': 2503 + optional: true 2504 + 2505 + '@rollup/rollup-linux-loong64-musl@4.55.3': 2506 + optional: true 2507 + 2508 + '@rollup/rollup-linux-ppc64-gnu@4.55.3': 2509 + optional: true 2510 + 2511 + '@rollup/rollup-linux-ppc64-musl@4.55.3': 2512 + optional: true 2513 + 2514 + '@rollup/rollup-linux-riscv64-gnu@4.55.3': 2515 + optional: true 2516 + 2517 + '@rollup/rollup-linux-riscv64-musl@4.55.3': 2518 + optional: true 2519 + 2520 + '@rollup/rollup-linux-s390x-gnu@4.55.3': 2521 + optional: true 2522 + 2523 + '@rollup/rollup-linux-x64-gnu@4.55.3': 2524 + optional: true 2525 + 2526 + '@rollup/rollup-linux-x64-musl@4.55.3': 2527 + optional: true 2528 + 2529 + '@rollup/rollup-openbsd-x64@4.55.3': 2530 + optional: true 2531 + 2532 + '@rollup/rollup-openharmony-arm64@4.55.3': 2533 + optional: true 2534 + 2535 + '@rollup/rollup-win32-arm64-msvc@4.55.3': 2536 + optional: true 2537 + 2538 + '@rollup/rollup-win32-ia32-msvc@4.55.3': 2539 + optional: true 2540 + 2541 + '@rollup/rollup-win32-x64-gnu@4.55.3': 2542 + optional: true 2543 + 2544 + '@rollup/rollup-win32-x64-msvc@4.55.3': 2545 + optional: true 2546 + 2547 + '@sindresorhus/is@7.2.0': {} 2548 + 2549 + '@speed-highlight/core@1.2.14': {} 2550 + 2551 + '@standard-schema/spec@1.1.0': {} 2552 + 2553 + '@tailwindcss/node@4.1.18': 2554 + dependencies: 2555 + '@jridgewell/remapping': 2.3.5 2556 + enhanced-resolve: 5.18.4 2557 + jiti: 2.6.1 2558 + lightningcss: 1.30.2 2559 + magic-string: 0.30.21 2560 + source-map-js: 1.2.1 2561 + tailwindcss: 4.1.18 2562 + 2563 + '@tailwindcss/oxide-android-arm64@4.1.18': 2564 + optional: true 2565 + 2566 + '@tailwindcss/oxide-darwin-arm64@4.1.18': 2567 + optional: true 2568 + 2569 + '@tailwindcss/oxide-darwin-x64@4.1.18': 2570 + optional: true 2571 + 2572 + '@tailwindcss/oxide-freebsd-x64@4.1.18': 2573 + optional: true 2574 + 2575 + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': 2576 + optional: true 2577 + 2578 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': 2579 + optional: true 2580 + 2581 + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': 2582 + optional: true 2583 + 2584 + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': 2585 + optional: true 2586 + 2587 + '@tailwindcss/oxide-linux-x64-musl@4.1.18': 2588 + optional: true 2589 + 2590 + '@tailwindcss/oxide-wasm32-wasi@4.1.18': 2591 + optional: true 2592 + 2593 + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': 2594 + optional: true 2595 + 2596 + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': 2597 + optional: true 2598 + 2599 + '@tailwindcss/oxide@4.1.18': 2600 + optionalDependencies: 2601 + '@tailwindcss/oxide-android-arm64': 4.1.18 2602 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 2603 + '@tailwindcss/oxide-darwin-x64': 4.1.18 2604 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 2605 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 2606 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 2607 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 2608 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 2609 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 2610 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 2611 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 2612 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 2613 + 2614 + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1))': 2615 + dependencies: 2616 + '@tailwindcss/node': 4.1.18 2617 + '@tailwindcss/oxide': 4.1.18 2618 + tailwindcss: 4.1.18 2619 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1) 2620 + 2621 + '@tybys/wasm-util@0.10.1': 2622 + dependencies: 2623 + tslib: 2.8.1 2624 + 2625 + '@types/babel__core@7.20.5': 2626 + dependencies: 2627 + '@babel/parser': 7.28.6 2628 + '@babel/types': 7.28.6 2629 + '@types/babel__generator': 7.27.0 2630 + '@types/babel__template': 7.4.4 2631 + '@types/babel__traverse': 7.28.0 2632 + 2633 + '@types/babel__generator@7.27.0': 2634 + dependencies: 2635 + '@babel/types': 7.28.6 2636 + 2637 + '@types/babel__template@7.4.4': 2638 + dependencies: 2639 + '@babel/parser': 7.28.6 2640 + '@babel/types': 7.28.6 2641 + 2642 + '@types/babel__traverse@7.28.0': 2643 + dependencies: 2644 + '@babel/types': 7.28.6 2645 + 2646 + '@types/chai@5.2.3': 2647 + dependencies: 2648 + '@types/deep-eql': 4.0.2 2649 + assertion-error: 2.0.1 2650 + 2651 + '@types/deep-eql@4.0.2': {} 2652 + 2653 + '@types/estree@1.0.8': {} 2654 + 2655 + '@types/node@24.10.9': 2656 + dependencies: 2657 + undici-types: 7.16.0 2658 + 2659 + '@types/semver@7.7.1': {} 2660 + 2661 + '@vitest/expect@4.0.17': 2662 + dependencies: 2663 + '@standard-schema/spec': 1.1.0 2664 + '@types/chai': 5.2.3 2665 + '@vitest/spy': 4.0.17 2666 + '@vitest/utils': 4.0.17 2667 + chai: 6.2.2 2668 + tinyrainbow: 3.0.3 2669 + 2670 + '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1))': 2671 + dependencies: 2672 + '@vitest/spy': 4.0.17 2673 + estree-walker: 3.0.3 2674 + magic-string: 0.30.21 2675 + optionalDependencies: 2676 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1) 2677 + 2678 + '@vitest/pretty-format@4.0.17': 2679 + dependencies: 2680 + tinyrainbow: 3.0.3 2681 + 2682 + '@vitest/runner@4.0.17': 2683 + dependencies: 2684 + '@vitest/utils': 4.0.17 2685 + pathe: 2.0.3 2686 + 2687 + '@vitest/snapshot@4.0.17': 2688 + dependencies: 2689 + '@vitest/pretty-format': 4.0.17 2690 + magic-string: 0.30.21 2691 + pathe: 2.0.3 2692 + 2693 + '@vitest/spy@4.0.17': {} 2694 + 2695 + '@vitest/utils@4.0.17': 2696 + dependencies: 2697 + '@vitest/pretty-format': 4.0.17 2698 + tinyrainbow: 3.0.3 2699 + 2700 + assertion-error@2.0.1: {} 2701 + 2702 + babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.6): 2703 + dependencies: 2704 + '@babel/core': 7.28.6 2705 + '@babel/helper-module-imports': 7.18.6 2706 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6) 2707 + '@babel/types': 7.28.6 2708 + html-entities: 2.3.3 2709 + parse5: 7.3.0 2710 + 2711 + babel-preset-solid@1.9.10(@babel/core@7.28.6)(solid-js@1.9.10): 2712 + dependencies: 2713 + '@babel/core': 7.28.6 2714 + babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.28.6) 2715 + optionalDependencies: 2716 + solid-js: 1.9.10 2717 + 2718 + baseline-browser-mapping@2.9.17: {} 2719 + 2720 + blake3-wasm@2.1.5: {} 2721 + 2722 + browserslist@4.28.1: 2723 + dependencies: 2724 + baseline-browser-mapping: 2.9.17 2725 + caniuse-lite: 1.0.30001765 2726 + electron-to-chromium: 1.5.267 2727 + node-releases: 2.0.27 2728 + update-browserslist-db: 1.2.3(browserslist@4.28.1) 2729 + 2730 + caniuse-lite@1.0.30001765: {} 2731 + 2732 + chai@6.2.2: {} 2733 + 2734 + convert-source-map@2.0.0: {} 2735 + 2736 + cookie@1.1.1: {} 2737 + 2738 + csstype@3.2.3: {} 2739 + 2740 + debug@4.4.3: 2741 + dependencies: 2742 + ms: 2.1.3 2743 + 2744 + dequal@2.0.3: {} 2745 + 2746 + detect-libc@2.1.2: {} 2747 + 2748 + electron-to-chromium@1.5.267: {} 2749 + 2750 + enhanced-resolve@5.18.4: 2751 + dependencies: 2752 + graceful-fs: 4.2.11 2753 + tapable: 2.3.0 2754 + 2755 + entities@6.0.1: {} 2756 + 2757 + error-stack-parser-es@1.0.5: {} 2758 + 2759 + es-module-lexer@1.7.0: {} 2760 + 2761 + esbuild@0.27.0: 2762 + optionalDependencies: 2763 + '@esbuild/aix-ppc64': 0.27.0 2764 + '@esbuild/android-arm': 0.27.0 2765 + '@esbuild/android-arm64': 0.27.0 2766 + '@esbuild/android-x64': 0.27.0 2767 + '@esbuild/darwin-arm64': 0.27.0 2768 + '@esbuild/darwin-x64': 0.27.0 2769 + '@esbuild/freebsd-arm64': 0.27.0 2770 + '@esbuild/freebsd-x64': 0.27.0 2771 + '@esbuild/linux-arm': 0.27.0 2772 + '@esbuild/linux-arm64': 0.27.0 2773 + '@esbuild/linux-ia32': 0.27.0 2774 + '@esbuild/linux-loong64': 0.27.0 2775 + '@esbuild/linux-mips64el': 0.27.0 2776 + '@esbuild/linux-ppc64': 0.27.0 2777 + '@esbuild/linux-riscv64': 0.27.0 2778 + '@esbuild/linux-s390x': 0.27.0 2779 + '@esbuild/linux-x64': 0.27.0 2780 + '@esbuild/netbsd-arm64': 0.27.0 2781 + '@esbuild/netbsd-x64': 0.27.0 2782 + '@esbuild/openbsd-arm64': 0.27.0 2783 + '@esbuild/openbsd-x64': 0.27.0 2784 + '@esbuild/openharmony-arm64': 0.27.0 2785 + '@esbuild/sunos-x64': 0.27.0 2786 + '@esbuild/win32-arm64': 0.27.0 2787 + '@esbuild/win32-ia32': 0.27.0 2788 + '@esbuild/win32-x64': 0.27.0 2789 + 2790 + esbuild@0.27.2: 2791 + optionalDependencies: 2792 + '@esbuild/aix-ppc64': 0.27.2 2793 + '@esbuild/android-arm': 0.27.2 2794 + '@esbuild/android-arm64': 0.27.2 2795 + '@esbuild/android-x64': 0.27.2 2796 + '@esbuild/darwin-arm64': 0.27.2 2797 + '@esbuild/darwin-x64': 0.27.2 2798 + '@esbuild/freebsd-arm64': 0.27.2 2799 + '@esbuild/freebsd-x64': 0.27.2 2800 + '@esbuild/linux-arm': 0.27.2 2801 + '@esbuild/linux-arm64': 0.27.2 2802 + '@esbuild/linux-ia32': 0.27.2 2803 + '@esbuild/linux-loong64': 0.27.2 2804 + '@esbuild/linux-mips64el': 0.27.2 2805 + '@esbuild/linux-ppc64': 0.27.2 2806 + '@esbuild/linux-riscv64': 0.27.2 2807 + '@esbuild/linux-s390x': 0.27.2 2808 + '@esbuild/linux-x64': 0.27.2 2809 + '@esbuild/netbsd-arm64': 0.27.2 2810 + '@esbuild/netbsd-x64': 0.27.2 2811 + '@esbuild/openbsd-arm64': 0.27.2 2812 + '@esbuild/openbsd-x64': 0.27.2 2813 + '@esbuild/openharmony-arm64': 0.27.2 2814 + '@esbuild/sunos-x64': 0.27.2 2815 + '@esbuild/win32-arm64': 0.27.2 2816 + '@esbuild/win32-ia32': 0.27.2 2817 + '@esbuild/win32-x64': 0.27.2 2818 + 2819 + escalade@3.2.0: {} 2820 + 2821 + estree-walker@3.0.3: 2822 + dependencies: 2823 + '@types/estree': 1.0.8 2824 + 2825 + expect-type@1.3.0: {} 2826 + 2827 + fdir@6.5.0(picomatch@4.0.3): 2828 + optionalDependencies: 2829 + picomatch: 4.0.3 2830 + 2831 + fsevents@2.3.3: 2832 + optional: true 2833 + 2834 + gensync@1.0.0-beta.2: {} 2835 + 2836 + glob-to-regex.js@1.2.0(tslib@2.8.1): 2837 + dependencies: 2838 + tslib: 2.8.1 2839 + 2840 + graceful-fs@4.2.11: {} 2841 + 2842 + html-entities@2.3.3: {} 2843 + 2844 + hyperdyperid@1.2.0: {} 2845 + 2846 + is-what@4.1.16: {} 2847 + 2848 + jiti@2.6.1: {} 2849 + 2850 + js-tokens@4.0.0: {} 2851 + 2852 + jsesc@3.1.0: {} 2853 + 2854 + json5@2.2.3: {} 2855 + 2856 + kleur@4.1.5: {} 2857 + 2858 + lightningcss-android-arm64@1.30.2: 2859 + optional: true 2860 + 2861 + lightningcss-android-arm64@1.31.1: 2862 + optional: true 2863 + 2864 + lightningcss-darwin-arm64@1.30.2: 2865 + optional: true 2866 + 2867 + lightningcss-darwin-arm64@1.31.1: 2868 + optional: true 2869 + 2870 + lightningcss-darwin-x64@1.30.2: 2871 + optional: true 2872 + 2873 + lightningcss-darwin-x64@1.31.1: 2874 + optional: true 2875 + 2876 + lightningcss-freebsd-x64@1.30.2: 2877 + optional: true 2878 + 2879 + lightningcss-freebsd-x64@1.31.1: 2880 + optional: true 2881 + 2882 + lightningcss-linux-arm-gnueabihf@1.30.2: 2883 + optional: true 2884 + 2885 + lightningcss-linux-arm-gnueabihf@1.31.1: 2886 + optional: true 2887 + 2888 + lightningcss-linux-arm64-gnu@1.30.2: 2889 + optional: true 2890 + 2891 + lightningcss-linux-arm64-gnu@1.31.1: 2892 + optional: true 2893 + 2894 + lightningcss-linux-arm64-musl@1.30.2: 2895 + optional: true 2896 + 2897 + lightningcss-linux-arm64-musl@1.31.1: 2898 + optional: true 2899 + 2900 + lightningcss-linux-x64-gnu@1.30.2: 2901 + optional: true 2902 + 2903 + lightningcss-linux-x64-gnu@1.31.1: 2904 + optional: true 2905 + 2906 + lightningcss-linux-x64-musl@1.30.2: 2907 + optional: true 2908 + 2909 + lightningcss-linux-x64-musl@1.31.1: 2910 + optional: true 2911 + 2912 + lightningcss-win32-arm64-msvc@1.30.2: 2913 + optional: true 2914 + 2915 + lightningcss-win32-arm64-msvc@1.31.1: 2916 + optional: true 2917 + 2918 + lightningcss-win32-x64-msvc@1.30.2: 2919 + optional: true 2920 + 2921 + lightningcss-win32-x64-msvc@1.31.1: 2922 + optional: true 2923 + 2924 + lightningcss@1.30.2: 2925 + dependencies: 2926 + detect-libc: 2.1.2 2927 + optionalDependencies: 2928 + lightningcss-android-arm64: 1.30.2 2929 + lightningcss-darwin-arm64: 1.30.2 2930 + lightningcss-darwin-x64: 1.30.2 2931 + lightningcss-freebsd-x64: 1.30.2 2932 + lightningcss-linux-arm-gnueabihf: 1.30.2 2933 + lightningcss-linux-arm64-gnu: 1.30.2 2934 + lightningcss-linux-arm64-musl: 1.30.2 2935 + lightningcss-linux-x64-gnu: 1.30.2 2936 + lightningcss-linux-x64-musl: 1.30.2 2937 + lightningcss-win32-arm64-msvc: 1.30.2 2938 + lightningcss-win32-x64-msvc: 1.30.2 2939 + 2940 + lightningcss@1.31.1: 2941 + dependencies: 2942 + detect-libc: 2.1.2 2943 + optionalDependencies: 2944 + lightningcss-android-arm64: 1.31.1 2945 + lightningcss-darwin-arm64: 1.31.1 2946 + lightningcss-darwin-x64: 1.31.1 2947 + lightningcss-freebsd-x64: 1.31.1 2948 + lightningcss-linux-arm-gnueabihf: 1.31.1 2949 + lightningcss-linux-arm64-gnu: 1.31.1 2950 + lightningcss-linux-arm64-musl: 1.31.1 2951 + lightningcss-linux-x64-gnu: 1.31.1 2952 + lightningcss-linux-x64-musl: 1.31.1 2953 + lightningcss-win32-arm64-msvc: 1.31.1 2954 + lightningcss-win32-x64-msvc: 1.31.1 2955 + optional: true 2956 + 2957 + lru-cache@5.1.1: 2958 + dependencies: 2959 + yallist: 3.1.1 2960 + 2961 + magic-string@0.30.21: 2962 + dependencies: 2963 + '@jridgewell/sourcemap-codec': 1.5.5 2964 + 2965 + memfs@4.56.9(tslib@2.8.1): 2966 + dependencies: 2967 + '@jsonjoy.com/fs-core': 4.56.9(tslib@2.8.1) 2968 + '@jsonjoy.com/fs-fsa': 4.56.9(tslib@2.8.1) 2969 + '@jsonjoy.com/fs-node': 4.56.9(tslib@2.8.1) 2970 + '@jsonjoy.com/fs-node-builtins': 4.56.9(tslib@2.8.1) 2971 + '@jsonjoy.com/fs-node-to-fsa': 4.56.9(tslib@2.8.1) 2972 + '@jsonjoy.com/fs-node-utils': 4.56.9(tslib@2.8.1) 2973 + '@jsonjoy.com/fs-print': 4.56.9(tslib@2.8.1) 2974 + '@jsonjoy.com/fs-snapshot': 4.56.9(tslib@2.8.1) 2975 + '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) 2976 + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) 2977 + glob-to-regex.js: 1.2.0(tslib@2.8.1) 2978 + thingies: 2.5.0(tslib@2.8.1) 2979 + tree-dump: 1.1.0(tslib@2.8.1) 2980 + tslib: 2.8.1 2981 + 2982 + merge-anything@5.1.7: 2983 + dependencies: 2984 + is-what: 4.1.16 2985 + 2986 + miniflare@4.20260120.0: 2987 + dependencies: 2988 + '@cspotcode/source-map-support': 0.8.1 2989 + sharp: 0.34.5 2990 + undici: 7.18.2 2991 + workerd: 1.20260120.0 2992 + ws: 8.18.0 2993 + youch: 4.1.0-beta.10 2994 + zod: 3.25.76 2995 + transitivePeerDependencies: 2996 + - bufferutil 2997 + - utf-8-validate 2998 + 2999 + ms@2.1.3: {} 3000 + 3001 + nanoid@3.3.11: {} 3002 + 3003 + node-releases@2.0.27: {} 3004 + 3005 + obug@2.1.1: {} 3006 + 3007 + oxfmt@0.26.0: 3008 + dependencies: 3009 + tinypool: 2.0.0 3010 + optionalDependencies: 3011 + '@oxfmt/darwin-arm64': 0.26.0 3012 + '@oxfmt/darwin-x64': 0.26.0 3013 + '@oxfmt/linux-arm64-gnu': 0.26.0 3014 + '@oxfmt/linux-arm64-musl': 0.26.0 3015 + '@oxfmt/linux-x64-gnu': 0.26.0 3016 + '@oxfmt/linux-x64-musl': 0.26.0 3017 + '@oxfmt/win32-arm64': 0.26.0 3018 + '@oxfmt/win32-x64': 0.26.0 3019 + 3020 + oxlint@1.41.0: 3021 + optionalDependencies: 3022 + '@oxlint/darwin-arm64': 1.41.0 3023 + '@oxlint/darwin-x64': 1.41.0 3024 + '@oxlint/linux-arm64-gnu': 1.41.0 3025 + '@oxlint/linux-arm64-musl': 1.41.0 3026 + '@oxlint/linux-x64-gnu': 1.41.0 3027 + '@oxlint/linux-x64-musl': 1.41.0 3028 + '@oxlint/win32-arm64': 1.41.0 3029 + '@oxlint/win32-x64': 1.41.0 3030 + 3031 + parse5@7.3.0: 3032 + dependencies: 3033 + entities: 6.0.1 3034 + 3035 + path-to-regexp@6.3.0: {} 3036 + 3037 + pathe@2.0.3: {} 3038 + 3039 + picocolors@1.1.1: {} 3040 + 3041 + picomatch@4.0.3: {} 3042 + 3043 + postcss@8.5.6: 3044 + dependencies: 3045 + nanoid: 3.3.11 3046 + picocolors: 1.1.1 3047 + source-map-js: 1.2.1 3048 + 3049 + rollup@4.55.3: 3050 + dependencies: 3051 + '@types/estree': 1.0.8 3052 + optionalDependencies: 3053 + '@rollup/rollup-android-arm-eabi': 4.55.3 3054 + '@rollup/rollup-android-arm64': 4.55.3 3055 + '@rollup/rollup-darwin-arm64': 4.55.3 3056 + '@rollup/rollup-darwin-x64': 4.55.3 3057 + '@rollup/rollup-freebsd-arm64': 4.55.3 3058 + '@rollup/rollup-freebsd-x64': 4.55.3 3059 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.3 3060 + '@rollup/rollup-linux-arm-musleabihf': 4.55.3 3061 + '@rollup/rollup-linux-arm64-gnu': 4.55.3 3062 + '@rollup/rollup-linux-arm64-musl': 4.55.3 3063 + '@rollup/rollup-linux-loong64-gnu': 4.55.3 3064 + '@rollup/rollup-linux-loong64-musl': 4.55.3 3065 + '@rollup/rollup-linux-ppc64-gnu': 4.55.3 3066 + '@rollup/rollup-linux-ppc64-musl': 4.55.3 3067 + '@rollup/rollup-linux-riscv64-gnu': 4.55.3 3068 + '@rollup/rollup-linux-riscv64-musl': 4.55.3 3069 + '@rollup/rollup-linux-s390x-gnu': 4.55.3 3070 + '@rollup/rollup-linux-x64-gnu': 4.55.3 3071 + '@rollup/rollup-linux-x64-musl': 4.55.3 3072 + '@rollup/rollup-openbsd-x64': 4.55.3 3073 + '@rollup/rollup-openharmony-arm64': 4.55.3 3074 + '@rollup/rollup-win32-arm64-msvc': 4.55.3 3075 + '@rollup/rollup-win32-ia32-msvc': 4.55.3 3076 + '@rollup/rollup-win32-x64-gnu': 4.55.3 3077 + '@rollup/rollup-win32-x64-msvc': 4.55.3 3078 + fsevents: 2.3.3 3079 + 3080 + semver@6.3.1: {} 3081 + 3082 + semver@7.7.3: {} 3083 + 3084 + seroval-plugins@1.3.3(seroval@1.3.2): 3085 + dependencies: 3086 + seroval: 1.3.2 3087 + 3088 + seroval@1.3.2: {} 3089 + 3090 + sharp@0.34.5: 3091 + dependencies: 3092 + '@img/colour': 1.0.0 3093 + detect-libc: 2.1.2 3094 + semver: 7.7.3 3095 + optionalDependencies: 3096 + '@img/sharp-darwin-arm64': 0.34.5 3097 + '@img/sharp-darwin-x64': 0.34.5 3098 + '@img/sharp-libvips-darwin-arm64': 1.2.4 3099 + '@img/sharp-libvips-darwin-x64': 1.2.4 3100 + '@img/sharp-libvips-linux-arm': 1.2.4 3101 + '@img/sharp-libvips-linux-arm64': 1.2.4 3102 + '@img/sharp-libvips-linux-ppc64': 1.2.4 3103 + '@img/sharp-libvips-linux-riscv64': 1.2.4 3104 + '@img/sharp-libvips-linux-s390x': 1.2.4 3105 + '@img/sharp-libvips-linux-x64': 1.2.4 3106 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 3107 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 3108 + '@img/sharp-linux-arm': 0.34.5 3109 + '@img/sharp-linux-arm64': 0.34.5 3110 + '@img/sharp-linux-ppc64': 0.34.5 3111 + '@img/sharp-linux-riscv64': 0.34.5 3112 + '@img/sharp-linux-s390x': 0.34.5 3113 + '@img/sharp-linux-x64': 0.34.5 3114 + '@img/sharp-linuxmusl-arm64': 0.34.5 3115 + '@img/sharp-linuxmusl-x64': 0.34.5 3116 + '@img/sharp-wasm32': 0.34.5 3117 + '@img/sharp-win32-arm64': 0.34.5 3118 + '@img/sharp-win32-ia32': 0.34.5 3119 + '@img/sharp-win32-x64': 0.34.5 3120 + 3121 + siginfo@2.0.0: {} 3122 + 3123 + solid-js@1.9.10: 3124 + dependencies: 3125 + csstype: 3.2.3 3126 + seroval: 1.3.2 3127 + seroval-plugins: 1.3.3(seroval@1.3.2) 3128 + 3129 + solid-refresh@0.6.3(solid-js@1.9.10): 3130 + dependencies: 3131 + '@babel/generator': 7.28.6 3132 + '@babel/helper-module-imports': 7.28.6 3133 + '@babel/types': 7.28.6 3134 + solid-js: 1.9.10 3135 + transitivePeerDependencies: 3136 + - supports-color 3137 + 3138 + source-map-js@1.2.1: {} 3139 + 3140 + stackback@0.0.2: {} 3141 + 3142 + std-env@3.10.0: {} 3143 + 3144 + supports-color@10.2.2: {} 3145 + 3146 + tailwindcss@4.1.18: {} 3147 + 3148 + tapable@2.3.0: {} 3149 + 3150 + thingies@2.5.0(tslib@2.8.1): 3151 + dependencies: 3152 + tslib: 2.8.1 3153 + 3154 + tinybench@2.9.0: {} 3155 + 3156 + tinyexec@1.0.2: {} 3157 + 3158 + tinyglobby@0.2.15: 3159 + dependencies: 3160 + fdir: 6.5.0(picomatch@4.0.3) 3161 + picomatch: 4.0.3 3162 + 3163 + tinypool@2.0.0: {} 3164 + 3165 + tinyrainbow@3.0.3: {} 3166 + 3167 + tree-dump@1.1.0(tslib@2.8.1): 3168 + dependencies: 3169 + tslib: 2.8.1 3170 + 3171 + tslib@2.8.1: {} 3172 + 3173 + typescript@5.9.3: {} 3174 + 3175 + undici-types@7.16.0: {} 3176 + 3177 + undici@7.18.2: {} 3178 + 3179 + unenv@2.0.0-rc.24: 3180 + dependencies: 3181 + pathe: 2.0.3 3182 + 3183 + update-browserslist-db@1.2.3(browserslist@4.28.1): 3184 + dependencies: 3185 + browserslist: 4.28.1 3186 + escalade: 3.2.0 3187 + picocolors: 1.1.1 3188 + 3189 + valibot@1.2.0(typescript@5.9.3): 3190 + optionalDependencies: 3191 + typescript: 5.9.3 3192 + 3193 + vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)): 3194 + dependencies: 3195 + '@babel/core': 7.28.6 3196 + '@types/babel__core': 7.20.5 3197 + babel-preset-solid: 1.9.10(@babel/core@7.28.6)(solid-js@1.9.10) 3198 + merge-anything: 5.1.7 3199 + solid-js: 1.9.10 3200 + solid-refresh: 0.6.3(solid-js@1.9.10) 3201 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1) 3202 + vitefu: 1.1.1(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)) 3203 + transitivePeerDependencies: 3204 + - supports-color 3205 + 3206 + vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1): 3207 + dependencies: 3208 + esbuild: 0.27.2 3209 + fdir: 6.5.0(picomatch@4.0.3) 3210 + picomatch: 4.0.3 3211 + postcss: 8.5.6 3212 + rollup: 4.55.3 3213 + tinyglobby: 0.2.15 3214 + optionalDependencies: 3215 + '@types/node': 24.10.9 3216 + fsevents: 2.3.3 3217 + jiti: 2.6.1 3218 + lightningcss: 1.31.1 3219 + 3220 + vitefu@1.1.1(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)): 3221 + optionalDependencies: 3222 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1) 3223 + 3224 + vitest@4.0.17(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1): 3225 + dependencies: 3226 + '@vitest/expect': 4.0.17 3227 + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)) 3228 + '@vitest/pretty-format': 4.0.17 3229 + '@vitest/runner': 4.0.17 3230 + '@vitest/snapshot': 4.0.17 3231 + '@vitest/spy': 4.0.17 3232 + '@vitest/utils': 4.0.17 3233 + es-module-lexer: 1.7.0 3234 + expect-type: 1.3.0 3235 + magic-string: 0.30.21 3236 + obug: 2.1.1 3237 + pathe: 2.0.3 3238 + picomatch: 4.0.3 3239 + std-env: 3.10.0 3240 + tinybench: 2.9.0 3241 + tinyexec: 1.0.2 3242 + tinyglobby: 0.2.15 3243 + tinyrainbow: 3.0.3 3244 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1) 3245 + why-is-node-running: 2.3.0 3246 + optionalDependencies: 3247 + '@types/node': 24.10.9 3248 + transitivePeerDependencies: 3249 + - jiti 3250 + - less 3251 + - lightningcss 3252 + - msw 3253 + - sass 3254 + - sass-embedded 3255 + - stylus 3256 + - sugarss 3257 + - terser 3258 + - tsx 3259 + - yaml 3260 + 3261 + why-is-node-running@2.3.0: 3262 + dependencies: 3263 + siginfo: 2.0.0 3264 + stackback: 0.0.2 3265 + 3266 + workerd@1.20260120.0: 3267 + optionalDependencies: 3268 + '@cloudflare/workerd-darwin-64': 1.20260120.0 3269 + '@cloudflare/workerd-darwin-arm64': 1.20260120.0 3270 + '@cloudflare/workerd-linux-64': 1.20260120.0 3271 + '@cloudflare/workerd-linux-arm64': 1.20260120.0 3272 + '@cloudflare/workerd-windows-64': 1.20260120.0 3273 + 3274 + wrangler@4.60.0: 3275 + dependencies: 3276 + '@cloudflare/kv-asset-handler': 0.4.2 3277 + '@cloudflare/unenv-preset': 2.11.0(unenv@2.0.0-rc.24)(workerd@1.20260120.0) 3278 + blake3-wasm: 2.1.5 3279 + esbuild: 0.27.0 3280 + miniflare: 4.20260120.0 3281 + path-to-regexp: 6.3.0 3282 + unenv: 2.0.0-rc.24 3283 + workerd: 1.20260120.0 3284 + optionalDependencies: 3285 + fsevents: 2.3.3 3286 + transitivePeerDependencies: 3287 + - bufferutil 3288 + - utf-8-validate 3289 + 3290 + ws@8.18.0: {} 3291 + 3292 + yallist@3.1.1: {} 3293 + 3294 + youch-core@0.3.3: 3295 + dependencies: 3296 + '@poppinss/exception': 1.2.3 3297 + error-stack-parser-es: 1.0.5 3298 + 3299 + youch@4.1.0-beta.10: 3300 + dependencies: 3301 + '@poppinss/colors': 4.1.6 3302 + '@poppinss/dumper': 0.6.5 3303 + '@speed-highlight/core': 1.2.14 3304 + cookie: 1.1.1 3305 + youch-core: 0.3.3 3306 + 3307 + zod@3.25.76: {}
+365
src/app.tsx
··· 1 + import { sample, sampleOne } from '@mary/array-fns'; 2 + import { createEffect, createMemo, createSignal, Match, onCleanup, Switch } from 'solid-js'; 3 + import * as v from 'valibot'; 4 + 5 + import PackageResult from './components/package-result'; 6 + import PackageSearchInput from './components/package-search-input'; 7 + import { 8 + LucideArrowDown, 9 + LucideCircleAlert, 10 + LucideHandHeart, 11 + LucideLoader, 12 + LucideScissorsLineDashed, 13 + } from './icons/lucide'; 14 + import { TangledDolly } from './icons/tangled'; 15 + import { PACKAGE_SPECIFIER_RE } from './lib/package-name'; 16 + import { createQuery } from './lib/query'; 17 + import { createDerivedSignal } from './lib/signals'; 18 + import { useSearchParams } from './lib/use-search-params'; 19 + import { progress } from './npm/events'; 20 + import { initPackage } from './npm/worker-client'; 21 + import type { ProgressMessage } from './npm/worker-protocol'; 22 + import Button from './primitives/button'; 23 + import Tooltip from './primitives/tooltip'; 24 + 25 + const RECOMMENDATIONS: (string | string[])[] = [ 26 + // UI frameworks 27 + 'preact', 28 + 'react', 29 + 'solid-js', 30 + 'svelte', 31 + 'vue', 32 + 33 + // routing 34 + '@solidjs/router', 35 + '@tanstack/react-router', 36 + 'navaid', 37 + 'path-to-regexp', 38 + 'react-router', 39 + 'regexparam', 40 + 'trouter', 41 + 'vue-router', 42 + 'wouter', 43 + 44 + // state management 45 + 'immer', 46 + 'jotai', 47 + 'nanostores', 48 + 'zustand', 49 + 'valtio', 50 + 51 + // data fetched 52 + ['@tanstack/react-query', '@tanstack/vue-query', '@tanstack/solid-query'], 53 + 'swr', 54 + 55 + // HTTP clients 56 + 'axios', 57 + 'ky', 58 + 'ofetch', 59 + 'redaxios', 60 + 'wretch', 61 + 62 + // validation/schema 63 + 'zod', 64 + 'valibot', 65 + 'yup', 66 + 'ajv', 67 + 'arktype', 68 + 'superstruct', 69 + '@badrap/valita', 70 + 71 + // date/time 72 + ['date-fns', '@date-fns/tz', '@date-fns/utc'], 73 + 'dayjs', 74 + 'luxon', 75 + 'moment', 76 + 'tinydate', 77 + 78 + // class names/styling 79 + 'clsx', 80 + 'classnames', 81 + 'tailwind-merge', 82 + 83 + // ID generation 84 + 'nanoid', 85 + 'uuid', 86 + 'hexoid', 87 + 'uid', 88 + 89 + // deep clone/equality 90 + 'klona', 91 + 'dequal', 92 + 'fast-deep-equal', 93 + 'rfdc', 94 + 95 + // query strings 96 + 'qs', 97 + 'qss', 98 + 'query-string', 99 + 100 + // i18n 101 + 'i18next', 102 + 'rosetta', 103 + 104 + // markdown 105 + 'marked', 106 + ['markdown-it', 'markdown-exit'], 107 + 108 + // search 109 + 'fuse.js', 110 + 'minisearch', 111 + 112 + // animation 113 + ['framer-motion', 'motion-v', 'motion'], 114 + 115 + // forms 116 + 'formik', 117 + 'react-hook-form', 118 + [ 119 + '@formisch/preact', 120 + '@formisch/qwik', 121 + '@formisch/react', 122 + '@formisch/solid', 123 + '@formisch/svelte', 124 + '@formisch/vue', 125 + ], 126 + [ 127 + '@tanstack/angular-form', 128 + '@tanstack/lit-form', 129 + '@tanstack/react-form', 130 + '@tanstack/solid-form', 131 + '@tanstack/svelte-form', 132 + '@tanstack/vue-form', 133 + ], 134 + 135 + // tables 136 + [ 137 + '@tanstack/angular-table', 138 + '@tanstack/lit-table', 139 + '@tanstack/qwik-table', 140 + '@tanstack/react-table', 141 + '@tanstack/solid-table', 142 + '@tanstack/svelte-table', 143 + '@tanstack/vue-table', 144 + ], 145 + 146 + // virtualization 147 + 'rc-virtual-list', 148 + 'virtua', 149 + [ 150 + '@tanstack/angular-virtual', 151 + '@tanstack/lit-virtual', 152 + '@tanstack/react-virtual', 153 + '@tanstack/solid-virtual', 154 + '@tanstack/svelte-virtual', 155 + '@tanstack/vue-virtual', 156 + ], 157 + 158 + // storage 159 + 'idb-keyval', 160 + 'idb', 161 + 162 + // CSV 163 + 'papaparse', 164 + 165 + // sanitization 166 + 'dompurify', 167 + 168 + // functional/result types 169 + 'effect', 170 + 'neverthrow', 171 + 'true-myth', 172 + 173 + // promise utilities 174 + 'p-all', 175 + 'p-limit', 176 + 'p-map', 177 + 'p-queue', 178 + 'p-series', 179 + 180 + // utilities 181 + 'es-toolkit', 182 + 'lodash-es', 183 + 'ramda', 184 + 'underscore', 185 + 186 + // UI component libraries 187 + '@base-ui/react', 188 + '@chakra-ui/react', 189 + '@fluentui/react-components', 190 + 'antd', 191 + 'corvu', 192 + 'tamagui', 193 + ['@mui/material', '@mui/joy'], 194 + ['radix-ui', '@radix-ui/themes'], 195 + ['radix-vue', 'reka-ui'], 196 + 197 + // CSS-in-JS / styling 198 + ['@emotion/react', '@emotion/styled'], 199 + 'styled-components', 200 + 'styled-jsx', 201 + 202 + // positioning/floating 203 + 'nanopop', 204 + ['@floating-ui/dom', '@floating-ui/react', '@floating-ui/react-dom', '@floating-ui/vue'], 205 + ]; 206 + 207 + function App() { 208 + const [params, setParams] = useSearchParams({ 209 + q: v.pipe(v.string(), v.regex(PACKAGE_SPECIFIER_RE)), 210 + }); 211 + 212 + const packageName = createMemo(() => params().q); 213 + 214 + const [query, setQuery] = createDerivedSignal(() => packageName() ?? ''); 215 + 216 + const [result, { refetch }] = createQuery(packageName, (name) => initPackage(name), { 217 + keepPreviousData: false, 218 + }); 219 + 220 + createEffect(() => { 221 + const $result = result(); 222 + if (!$result) { 223 + return; 224 + } 225 + 226 + onCleanup(() => $result.worker.terminate()); 227 + }); 228 + 229 + const recs = sample(RECOMMENDATIONS, 6) 230 + .flatMap((v) => (Array.isArray(v) ? sampleOne(v) : v)) 231 + .sort(); 232 + 233 + return ( 234 + <div class="mx-auto flex max-w-xl flex-col gap-6 p-4"> 235 + <div class="flex h-12 items-center justify-between gap-1 rounded-lg border border-neutral-stroke-3 bg-neutral-background-1 px-4"> 236 + <div class="flex shrink-0 items-center gap-2"> 237 + <LucideScissorsLineDashed class="size-5 text-brand-foreground-2" /> 238 + <h1 class="text-base-400 font-medium">teardown</h1> 239 + </div> 240 + 241 + <div class="-mr-2 flex items-center gap-1"> 242 + <Tooltip content="Donate!" relationship="label" placement="bottom"> 243 + {(triggerProps) => ( 244 + <a 245 + {...triggerProps} 246 + target="_blank" 247 + href="https://github.com/sponsors/mary-ext" 248 + class="grid h-8 w-8 shrink-0 place-items-center rounded-md border border-transparent bg-subtle-background text-neutral-foreground-2 outline-2 -outline-offset-2 outline-transparent transition duration-100 hover:bg-subtle-background-hover focus-visible:outline-compound-brand-stroke active:bg-subtle-background-pressed" 249 + > 250 + <LucideHandHeart class="size-4" /> 251 + </a> 252 + )} 253 + </Tooltip> 254 + 255 + <Tooltip content="Source code on tangled.org" relationship="label" placement="bottom"> 256 + {(triggerProps) => ( 257 + <a 258 + {...triggerProps} 259 + target="_blank" 260 + href="https://tangled.org/did:plc:ia76kvnndjutgedggx2ibrem/teardown" 261 + class="grid h-8 w-8 shrink-0 place-items-center rounded-md border border-transparent bg-subtle-background text-neutral-foreground-2 outline-2 -outline-offset-2 outline-transparent transition duration-100 hover:bg-subtle-background-hover focus-visible:outline-compound-brand-stroke active:bg-subtle-background-pressed" 262 + > 263 + <TangledDolly class="size-4" /> 264 + </a> 265 + )} 266 + </Tooltip> 267 + </div> 268 + </div> 269 + 270 + <div class="flex min-h-0 grow flex-col gap-4 sm:px-4"> 271 + <PackageSearchInput 272 + autofocus={/* @once */ !query()} 273 + value={query()} 274 + onChange={setQuery} 275 + onSelect={(specifier) => setParams({ q: specifier })} 276 + /> 277 + 278 + <Switch> 279 + <Match when={result()} keyed> 280 + {(result) => <PackageResult result={result} />} 281 + </Match> 282 + 283 + <Match when={result.state === 'errored'}> 284 + <div class="flex flex-col items-center justify-center gap-3 py-12"> 285 + <LucideCircleAlert class="text-danger-foreground-1 size-5" /> 286 + <span class="text-base-300 text-neutral-foreground-2">{result.error?.message}</span> 287 + <Button appearance="subtle" onClick={() => refetch()}> 288 + Retry 289 + </Button> 290 + </div> 291 + </Match> 292 + 293 + <Match when={result.state === 'pending' || result.state === 'refreshing'} keyed> 294 + {(_) => { 295 + const [progressState, setProgressState] = createSignal<ProgressMessage | null>(null); 296 + 297 + onCleanup(progress.listen((msg) => setProgressState(msg))); 298 + 299 + return ( 300 + <div class="flex flex-col items-center justify-center gap-3 py-12"> 301 + <LucideLoader class="size-5 animate-spin-linear text-neutral-foreground-3" /> 302 + 303 + {(() => { 304 + const p = progressState(); 305 + 306 + switch (p?.kind) { 307 + case 'resolve': 308 + return ( 309 + <span class="text-base-300 text-neutral-foreground-2"> 310 + Resolved {p.name}@{p.version} 311 + </span> 312 + ); 313 + case 'fetch': 314 + return ( 315 + <div class="flex flex-col items-center gap-1"> 316 + <span class="text-base-300 text-neutral-foreground-2">Downloaded {p.name}</span> 317 + <span class="text-base-200 text-neutral-foreground-3"> 318 + {p.current} / {p.total} 319 + </span> 320 + </div> 321 + ); 322 + default: 323 + return <span class="text-base-300 text-neutral-foreground-2">Loading...</span>; 324 + } 325 + })()} 326 + </div> 327 + ); 328 + }} 329 + </Match> 330 + 331 + <Match when> 332 + <div class="flex flex-col gap-4"> 333 + <div> 334 + <p class="text-base-300 text-neutral-foreground-2"> 335 + Find the cost of adding an npm package to your app's bundle size. <br /> 336 + Makes use of <a>Rolldown</a> to bundle packages in your browser. 337 + </p> 338 + </div> 339 + 340 + <div class="flex flex-col gap-3"> 341 + <p class="text-base-200 font-bold text-neutral-foreground-4 uppercase">Example packages</p> 342 + 343 + {recs.map((name) => { 344 + return ( 345 + <div class="flex items-center gap-3"> 346 + <LucideArrowDown class="size-4 rotate-270 text-neutral-foreground-3" /> 347 + <button 348 + onClick={() => setParams({ q: `npm:${name}` })} 349 + class="cursor-pointer text-base-300 text-brand-foreground-2 transition hover:text-brand-foreground-2-hover hover:underline active:text-brand-foreground-2-pressed" 350 + > 351 + {name} 352 + </button> 353 + </div> 354 + ); 355 + })} 356 + </div> 357 + </div> 358 + </Match> 359 + </Switch> 360 + </div> 361 + </div> 362 + ); 363 + } 364 + 365 + export default App;
+305
src/components/package-bundle.tsx
··· 1 + import { createSignal, For, Match, onCleanup, Show, Switch } from 'solid-js'; 2 + 3 + import { LucideCheck, LucideCircleAlert, LucideLoader } from '../icons/lucide'; 4 + import { formatBytes } from '../lib/format'; 5 + import { LRUCache } from '../lib/lru'; 6 + import { createQuery } from '../lib/query'; 7 + import { createDerivedSignal } from '../lib/signals'; 8 + import type { BundleResult } from '../npm/bundler'; 9 + import { progress } from '../npm/events'; 10 + import type { DiscoveredSubpaths } from '../npm/subpaths'; 11 + import type { BundlerWorker } from '../npm/worker-client'; 12 + import type { ProgressMessage } from '../npm/worker-protocol'; 13 + import Button from '../primitives/button'; 14 + import * as Dropdown from '../primitives/dropdown'; 15 + import * as Field from '../primitives/field'; 16 + 17 + // #region helpers 18 + 19 + function serializeCacheKey(subpath: string, exports: string[] | null): string { 20 + if (exports === null) { 21 + return subpath; 22 + } 23 + return `${subpath}\0${exports.join('\0')}`; 24 + } 25 + 26 + function arraysEqual(a: string[], b: string[]): boolean { 27 + if (a.length !== b.length) { 28 + return false; 29 + } 30 + for (let i = 0; i < a.length; i++) { 31 + if (a[i] !== b[i]) { 32 + return false; 33 + } 34 + } 35 + return true; 36 + } 37 + 38 + // #endregion 39 + 40 + // #region component 41 + 42 + interface PackageBundleProps { 43 + packageName: string; 44 + subpaths: DiscoveredSubpaths; 45 + worker: BundlerWorker; 46 + } 47 + 48 + const PackageBundle = (props: PackageBundleProps) => { 49 + const packageName = props.packageName; 50 + const subpaths = props.subpaths; 51 + const worker = props.worker; 52 + 53 + /** formats a subpath for display, replacing `.` and `./` with the package name */ 54 + const formatSubpath = (subpath: string) => { 55 + if (subpath === '.') { 56 + return packageName; 57 + } 58 + if (subpath.startsWith('./')) { 59 + return packageName + subpath.slice(1); 60 + } 61 + 62 + return subpath; 63 + }; 64 + 65 + const bundleCache = new LRUCache<string, BundleResult>(16); 66 + 67 + const [subpath, setSubpath] = createSignal(subpaths.defaultSubpath!); 68 + 69 + // initial bundle query - fetches all exports to discover what's available 70 + const [initialBundle, { refetch: refetchInitial }] = createQuery( 71 + subpath, 72 + async (subpath) => { 73 + const cacheKey = serializeCacheKey(subpath, null); 74 + const cached = bundleCache.peek(cacheKey); 75 + if (cached) { 76 + return cached; 77 + } 78 + 79 + const res = await worker.bundle(subpath, null); 80 + bundleCache.put(cacheKey, res); 81 + return res; 82 + }, 83 + { keepPreviousData: false }, 84 + ); 85 + 86 + // selectedExports derives from initialBundle exports, resets when they change 87 + const [selectedExports, setSelectedExports] = createDerivedSignal(() => { 88 + const $initialBundle = initialBundle.state === 'ready' && initialBundle(); 89 + return $initialBundle ? $initialBundle.exports : []; 90 + }); 91 + 92 + // bundle query - runs when initial bundle is ready, uses cache when all exports selected 93 + const [bundle, { refetch: refetchBundle }] = createQuery( 94 + () => { 95 + const $subpath = subpath(); 96 + if (!$subpath) { 97 + return null; 98 + } 99 + 100 + const $initialBundle = initialBundle.state === 'ready' && initialBundle(); 101 + if (!$initialBundle) { 102 + return null; 103 + } 104 + 105 + const exports = selectedExports(); 106 + // if selection equals all exports, pass null to reuse LRU cache 107 + const exportsParam = arraysEqual(exports, $initialBundle.exports) ? null : exports; 108 + 109 + return { subpath: $subpath, exports: exportsParam }; 110 + }, 111 + async ({ subpath, exports }) => { 112 + const cacheKey = serializeCacheKey(subpath, exports); 113 + const cached = bundleCache.get(cacheKey); 114 + if (cached) { 115 + return cached; 116 + } 117 + 118 + const res = await worker.bundle(subpath, exports); 119 + bundleCache.put(cacheKey, res); 120 + return res; 121 + }, 122 + ); 123 + 124 + const toggleExport = (exportName: string) => { 125 + const current = selectedExports(); 126 + if (current.includes(exportName)) { 127 + setSelectedExports(current.filter((e) => e !== exportName)); 128 + } else { 129 + setSelectedExports([...current, exportName]); 130 + } 131 + }; 132 + 133 + const selectAll = () => { 134 + const $initialBundle = initialBundle.state === 'ready' && initialBundle(); 135 + if ($initialBundle) { 136 + setSelectedExports($initialBundle.exports); 137 + } 138 + }; 139 + 140 + const selectNone = () => setSelectedExports([]); 141 + 142 + const isExportSelected = (exportName: string) => { 143 + return selectedExports().includes(exportName); 144 + }; 145 + 146 + return ( 147 + <div class="flex flex-col gap-5"> 148 + {/* section header */} 149 + <h3 class="text-base-400 font-semibold text-neutral-foreground-1">Bundle size</h3> 150 + 151 + {/* subpath selector */} 152 + <Show when={subpaths.subpaths.length > 1}> 153 + <Field.Root label="Subpath"> 154 + <Dropdown.Root value={subpath()} onValueChange={setSubpath}> 155 + <Dropdown.Trigger>{formatSubpath(subpath())}</Dropdown.Trigger> 156 + <Dropdown.Listbox> 157 + <For each={subpaths.subpaths}> 158 + {(sp) => <Dropdown.Option value={sp.subpath}>{formatSubpath(sp.subpath)}</Dropdown.Option>} 159 + </For> 160 + </Dropdown.Listbox> 161 + </Dropdown.Root> 162 + </Field.Root> 163 + </Show> 164 + 165 + {/* bundle results */} 166 + <Switch> 167 + <Match when={initialBundle.state === 'errored'}> 168 + <div class="flex flex-col items-center justify-center gap-3 py-12"> 169 + <LucideCircleAlert class="text-danger-foreground-1 size-5" /> 170 + <span class="text-base-300 text-neutral-foreground-2">{initialBundle.error?.message}</span> 171 + <Button appearance="subtle" onClick={() => refetchInitial()}> 172 + Retry 173 + </Button> 174 + </div> 175 + </Match> 176 + 177 + <Match when={bundle.state === 'errored'}> 178 + <div class="flex flex-col items-center justify-center gap-3 py-12"> 179 + <LucideCircleAlert class="text-danger-foreground-1 size-5" /> 180 + <span class="text-base-300 text-neutral-foreground-2">{bundle.error?.message}</span> 181 + <Button appearance="subtle" onClick={() => refetchBundle()}> 182 + Retry 183 + </Button> 184 + </div> 185 + </Match> 186 + 187 + <Match when={bundle()}> 188 + {(bundleData) => ( 189 + <div class="flex flex-col gap-5"> 190 + {/* size display card */} 191 + <div class="flex items-stretch gap-6 rounded-lg border border-neutral-stroke-3 bg-neutral-background-1 p-4"> 192 + <div class="flex flex-col gap-1"> 193 + <span class="text-base-300 text-neutral-foreground-3">Minified</span> 194 + <span class="text-base-500 font-semibold text-neutral-foreground-1"> 195 + {formatBytes(bundleData().size)} 196 + </span> 197 + </div> 198 + <div class="w-px bg-neutral-stroke-3" /> 199 + <div class="flex flex-col gap-1"> 200 + <span class="text-base-300 text-neutral-foreground-3">Gzipped</span> 201 + <span class="text-base-500 font-semibold text-neutral-foreground-1"> 202 + {formatBytes(bundleData().gzipSize)} 203 + </span> 204 + </div> 205 + 206 + <Show when={bundleData().brotliSize !== undefined}> 207 + <div class="w-px bg-neutral-stroke-3" /> 208 + <div class="flex flex-col gap-1"> 209 + <span class="text-base-300 text-neutral-foreground-3">Brotli</span> 210 + <span class="text-base-500 font-semibold text-neutral-foreground-1"> 211 + {formatBytes(bundleData().brotliSize!)} 212 + </span> 213 + </div> 214 + </Show> 215 + <Show when={bundle.state === 'refreshing'}> 216 + <div class="flex items-center"> 217 + <LucideLoader class="size-5 animate-spin-linear text-neutral-foreground-3" /> 218 + </div> 219 + </Show> 220 + </div> 221 + 222 + {/* export selection */} 223 + <Show when={initialBundle()?.exports} keyed> 224 + {(allExports) => ( 225 + <div class="flex flex-col gap-3"> 226 + <div class="flex items-center justify-between"> 227 + <span class="text-base-300 font-medium text-neutral-foreground-2"> 228 + Exports ({allExports.length}) 229 + </span> 230 + <div class="flex gap-1"> 231 + <Button appearance="subtle" size="small" onClick={selectAll}> 232 + All 233 + </Button> 234 + <Button appearance="subtle" size="small" onClick={selectNone}> 235 + None 236 + </Button> 237 + </div> 238 + </div> 239 + <div class="flex flex-wrap gap-1.5"> 240 + <For each={allExports}> 241 + {(exp) => { 242 + const selected = () => isExportSelected(exp); 243 + return ( 244 + <button 245 + onClick={() => toggleExport(exp)} 246 + class="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-base-300 transition duration-100" 247 + classList={{ 248 + 'border-brand-stroke-1 bg-brand-background-2 text-brand-foreground-2 hover:bg-brand-background-2-hover hover:text-brand-foreground-2-hover active:bg-brand-background-2-pressed active:text-brand-foreground-2-pressed': 249 + selected(), 250 + 'border-neutral-stroke-1 bg-neutral-background-1 text-neutral-foreground-2 hover:bg-neutral-background-1-hover hover:text-neutral-foreground-2-hover active:bg-neutral-background-1-pressed active:text-neutral-foreground-2-pressed': 251 + !selected(), 252 + }} 253 + > 254 + <LucideCheck 255 + class="duration-fast size-3.5 transition" 256 + classList={{ 257 + 'opacity-100': selected(), 258 + 'opacity-0': !selected(), 259 + }} 260 + /> 261 + <span>{exp}</span> 262 + </button> 263 + ); 264 + }} 265 + </For> 266 + </div> 267 + </div> 268 + )} 269 + </Show> 270 + </div> 271 + )} 272 + </Match> 273 + 274 + <Match when> 275 + {(() => { 276 + const [progressState, setProgressState] = createSignal<ProgressMessage | null>(null); 277 + 278 + onCleanup(progress.listen((msg) => setProgressState(msg))); 279 + 280 + return ( 281 + <div class="flex flex-col items-center justify-center gap-3 py-12"> 282 + <LucideLoader class="size-5 animate-spin-linear text-neutral-foreground-3" /> 283 + 284 + {(() => { 285 + const p = progressState(); 286 + 287 + switch (p?.kind) { 288 + case 'compress': 289 + return <span class="text-base-300 text-neutral-foreground-2">Compressing</span>; 290 + default: 291 + return <span class="text-base-300 text-neutral-foreground-2">Bundling...</span>; 292 + } 293 + })()} 294 + </div> 295 + ); 296 + })()} 297 + </Match> 298 + </Switch> 299 + </div> 300 + ); 301 + }; 302 + 303 + export default PackageBundle; 304 + 305 + // #endregion
+254
src/components/package-dependencies.tsx
··· 1 + import { createMemo, createSignal, For, Show } from 'solid-js'; 2 + 3 + import { LucideSearch } from '../icons/lucide'; 4 + import { tw } from '../lib/classes'; 5 + import { formatBytes } from '../lib/format'; 6 + import type { InstalledPackage } from '../npm/worker-protocol'; 7 + import * as Dropdown from '../primitives/dropdown'; 8 + import Input from '../primitives/input'; 9 + import Tooltip from '../primitives/tooltip'; 10 + 11 + // #region types 12 + 13 + type SortOption = 'level' | 'size' | 'installedBy' | 'dependencies' | 'name'; 14 + 15 + interface SortConfig { 16 + label: string; 17 + compare: (a: InstalledPackage, b: InstalledPackage) => number; 18 + } 19 + 20 + // #endregion 21 + 22 + // #region constants 23 + 24 + const SORT_OPTIONS: Record<SortOption, SortConfig> = { 25 + level: { 26 + label: 'Dependency level', 27 + compare: (a, b) => a.level - b.level || a.name.localeCompare(b.name), 28 + }, 29 + size: { 30 + label: 'Package size', 31 + compare: (a, b) => b.size - a.size || a.name.localeCompare(b.name), 32 + }, 33 + installedBy: { 34 + label: 'Installed by count', 35 + compare: (a, b) => b.installedBy - a.installedBy || a.name.localeCompare(b.name), 36 + }, 37 + dependencies: { 38 + label: 'Dependencies count', 39 + compare: (a, b) => b.dependencyCount - a.dependencyCount || a.name.localeCompare(b.name), 40 + }, 41 + name: { 42 + label: 'Name', 43 + compare: (a, b) => a.name.localeCompare(b.name), 44 + }, 45 + }; 46 + 47 + /** colors for the size breakdown bar segments */ 48 + const SEGMENT_COLORS = [ 49 + tw`bg-[#f97316]`, // orange 50 + tw`bg-[#eab308]`, // yellow 51 + tw`bg-[#22c55e]`, // green 52 + tw`bg-[#06b6d4]`, // cyan 53 + tw`bg-[#3b82f6]`, // blue 54 + tw`bg-[#8b5cf6]`, // violet 55 + tw`bg-[#ec4899]`, // pink 56 + tw`bg-[#f43f5e]`, // rose 57 + ]; 58 + 59 + // #endregion 60 + 61 + // #region size breakdown bar 62 + 63 + interface SizeBreakdownBarProps { 64 + packages: InstalledPackage[]; 65 + installSize: number; 66 + } 67 + 68 + const SizeBreakdownBar = (props: SizeBreakdownBarProps) => { 69 + const segments = createMemo(() => { 70 + // sort by size descending for the bar 71 + const sorted = [...props.packages].sort((a, b) => b.size - a.size); 72 + return sorted.map((pkg, i) => ({ 73 + pkg, 74 + percent: (pkg.size / props.installSize) * 100, 75 + color: SEGMENT_COLORS[i % SEGMENT_COLORS.length], 76 + })); 77 + }); 78 + 79 + return ( 80 + <div class="flex h-8 w-full overflow-hidden rounded-lg"> 81 + <For each={segments()}> 82 + {(segment) => ( 83 + <Tooltip 84 + content={ 85 + <> 86 + <span class="font-medium">{segment.pkg.name}</span> 87 + <span class="text-neutral-foreground-3"> 88 + {' '} 89 + — {formatBytes(segment.pkg.size)} ({segment.percent.toFixed(1)}%) 90 + </span> 91 + </> 92 + } 93 + relationship="label" 94 + placement="bottom" 95 + > 96 + {(triggerProps) => ( 97 + <div 98 + {...triggerProps} 99 + class={`${segment.color} duration-fast min-w-0 transition-opacity hover:opacity-80`} 100 + style={{ width: `${segment.percent}%` }} 101 + tabIndex={0} 102 + /> 103 + )} 104 + </Tooltip> 105 + )} 106 + </For> 107 + </div> 108 + ); 109 + }; 110 + 111 + // #endregion 112 + 113 + // #region package card 114 + 115 + interface PackageCardProps { 116 + pkg: InstalledPackage; 117 + percent: number; 118 + } 119 + 120 + const PackageCard = (props: PackageCardProps) => { 121 + return ( 122 + <div class="group duration-fast flex gap-4 rounded-lg border border-transparent px-3 py-4 transition hover:border-neutral-stroke-3 hover:bg-neutral-background-1"> 123 + {/* left side: percentage and size */} 124 + <div class="flex w-16 shrink-0 flex-col items-end text-right"> 125 + <span class="text-base-400 font-semibold text-neutral-foreground-1">{props.percent.toFixed(0)}%</span> 126 + <span class="text-base-300 text-neutral-foreground-3">{formatBytes(props.pkg.size)}</span> 127 + </div> 128 + 129 + {/* main content */} 130 + <div class="flex min-w-0 flex-1 flex-col gap-1.5"> 131 + {/* name, version, and level */} 132 + <div class="flex flex-wrap items-center gap-2"> 133 + <span class="text-base-400 font-semibold text-neutral-foreground-1">{props.pkg.name}</span> 134 + <span class="my-px text-base-300 text-neutral-foreground-3">{props.pkg.version}</span> 135 + <Show when={props.pkg.level > 0}> 136 + <span class="rounded-md bg-neutral-background-3 px-1.5 py-0.5 text-base-200 font-medium text-neutral-foreground-3"> 137 + Level {props.pkg.level} 138 + </span> 139 + </Show> 140 + </div> 141 + 142 + {/* description */} 143 + <Show when={props.pkg.description}> 144 + <p class="line-clamp-2 text-base-300 text-neutral-foreground-2">{props.pkg.description}</p> 145 + </Show> 146 + 147 + {/* stats */} 148 + <div class="flex flex-wrap gap-4 text-base-300"> 149 + <span class="text-neutral-foreground-3"> 150 + <span class="font-medium text-neutral-foreground-2">Installed by:</span> {props.pkg.installedBy} 151 + </span> 152 + <span class="text-neutral-foreground-3"> 153 + <span class="font-medium text-neutral-foreground-2">Dependencies:</span>{' '} 154 + {props.pkg.dependencyCount} 155 + </span> 156 + </div> 157 + </div> 158 + </div> 159 + ); 160 + }; 161 + 162 + // #endregion 163 + 164 + // #region component 165 + 166 + interface PackageDependenciesProps { 167 + packages: InstalledPackage[]; 168 + installSize: number; 169 + } 170 + 171 + const PackageDependencies = (props: PackageDependenciesProps) => { 172 + const [filter, setFilter] = createSignal(''); 173 + const [sortBy, setSortBy] = createSignal<SortOption>('level'); 174 + 175 + const filteredAndSorted = createMemo(() => { 176 + const filterText = filter().toLowerCase(); 177 + const sortConfig = SORT_OPTIONS[sortBy()]; 178 + 179 + let result = props.packages; 180 + 181 + if (filterText) { 182 + result = result.filter( 183 + (pkg) => 184 + pkg.name.toLowerCase().includes(filterText) || pkg.description?.toLowerCase().includes(filterText), 185 + ); 186 + } 187 + 188 + return [...result].sort(sortConfig.compare); 189 + }); 190 + 191 + return ( 192 + <div class="flex flex-col gap-5"> 193 + {/* header with total */} 194 + <div class="flex flex-wrap items-baseline justify-between gap-4"> 195 + <h3 class="text-base-400 font-semibold text-neutral-foreground-1">Install size</h3> 196 + <div class="text-base-300 text-neutral-foreground-2"> 197 + <span class="text-base-400 font-semibold text-neutral-foreground-1"> 198 + {formatBytes(props.installSize)} 199 + </span> 200 + <span class="text-neutral-foreground-3"> across {props.packages.length} packages</span> 201 + </div> 202 + </div> 203 + 204 + {/* size breakdown bar */} 205 + <SizeBreakdownBar packages={props.packages} installSize={props.installSize} /> 206 + 207 + {/* filter and sort controls */} 208 + <div class="flex flex-col gap-3 sm:flex-row sm:items-center"> 209 + {/* filter input */} 210 + <Input 211 + class="sm:flex-1" 212 + type="text" 213 + placeholder="Filter packages..." 214 + value={filter()} 215 + onInput={(e) => setFilter(e.currentTarget.value)} 216 + contentBefore={<LucideSearch class="size-4" />} 217 + /> 218 + 219 + {/* sort dropdown */} 220 + <Dropdown.Root value={sortBy()} onValueChange={(v) => setSortBy(v as SortOption)}> 221 + <Dropdown.Trigger class="w-auto"> 222 + <span class="mr-1 text-neutral-foreground-3">Sort:</span> 223 + <span>{SORT_OPTIONS[sortBy()].label}</span> 224 + </Dropdown.Trigger> 225 + <Dropdown.Listbox> 226 + <For each={Object.entries(SORT_OPTIONS) as [SortOption, SortConfig][]}> 227 + {([key, config]) => <Dropdown.Option value={key}>{config.label}</Dropdown.Option>} 228 + </For> 229 + </Dropdown.Listbox> 230 + </Dropdown.Root> 231 + </div> 232 + 233 + {/* package list */} 234 + <div class="-mx-3 flex flex-col"> 235 + <For each={filteredAndSorted()}> 236 + {(pkg) => { 237 + const percent = (pkg.size / props.installSize) * 100; 238 + return <PackageCard pkg={pkg} percent={percent} />; 239 + }} 240 + </For> 241 + 242 + <Show when={filteredAndSorted().length === 0}> 243 + <div class="py-12 text-center text-base-300 text-neutral-foreground-3"> 244 + No packages match your filter 245 + </div> 246 + </Show> 247 + </div> 248 + </div> 249 + ); 250 + }; 251 + 252 + export default PackageDependencies; 253 + 254 + // #endregion
+49
src/components/package-result.tsx
··· 1 + import type { PackageSession } from '../npm/worker-client'; 2 + 3 + import PackageBundle from './package-bundle'; 4 + import PackageDependencies from './package-dependencies'; 5 + 6 + // #region component 7 + 8 + interface PackageResultProps { 9 + result: PackageSession; 10 + } 11 + 12 + const PackageResult = (props: PackageResultProps) => { 13 + const result = props.result; 14 + 15 + return ( 16 + <div class="flex flex-col gap-8"> 17 + {/* package header */} 18 + <div class="flex flex-wrap gap-3"> 19 + <h2 class="min-w-0 text-base-600 font-bold wrap-break-word text-neutral-foreground-1"> 20 + {result.name} 21 + </h2> 22 + 23 + <span class="my-1.25 text-base-400 text-neutral-foreground-3">{result.version}</span> 24 + </div> 25 + 26 + {/* bundle size section */} 27 + {result.subpaths.defaultSubpath !== null && ( 28 + <> 29 + <PackageBundle 30 + packageName={/* @once */ result.name} 31 + subpaths={/* @once */ result.subpaths} 32 + worker={/* @once */ result.worker} 33 + /> 34 + <hr class="border-neutral-stroke-3" /> 35 + </> 36 + )} 37 + 38 + {/* install size section */} 39 + <PackageDependencies 40 + packages={/* @once */ result.packages} 41 + installSize={/* @once */ result.installSize} 42 + /> 43 + </div> 44 + ); 45 + }; 46 + 47 + export default PackageResult; 48 + 49 + // #endregion
+320
src/components/package-search-input.tsx
··· 1 + import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js'; 2 + 3 + import { LucideLoader, LucidePackage, LucideSearch } from '../icons/lucide'; 4 + import { modality } from '../lib/modality'; 5 + import { formatPackageSpecifier, parsePackageSpecifier, type Registry } from '../lib/package-name'; 6 + import { createQuery } from '../lib/query'; 7 + import { createTrailingThrottle, makeAbortable } from '../lib/signals'; 8 + import { normalizeWhitespace } from '../lib/strings'; 9 + import Input from '../primitives/input'; 10 + 11 + // #region types 12 + 13 + interface SearchResult { 14 + name: string; 15 + version: string; 16 + description?: string; 17 + registry: Registry; 18 + } 19 + 20 + interface NpmSearchResponse { 21 + objects: Array<{ 22 + package: { 23 + name: string; 24 + version: string; 25 + description?: string; 26 + }; 27 + }>; 28 + } 29 + 30 + interface JsrSearchResponse { 31 + items: Array<{ 32 + scope: string; 33 + name: string; 34 + latestVersion: string; 35 + description?: string; 36 + }>; 37 + } 38 + 39 + // #endregion 40 + 41 + // #region search API 42 + 43 + async function searchNpm(query: string, signal: AbortSignal): Promise<SearchResult[]> { 44 + const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=10`; 45 + const response = await fetch(url, { signal }); 46 + if (!response.ok) { 47 + return []; 48 + } 49 + 50 + const data = (await response.json()) as NpmSearchResponse; 51 + 52 + return data.objects.map((obj) => ({ 53 + name: obj.package.name, 54 + version: obj.package.version, 55 + description: obj.package.description, 56 + registry: 'npm' as const, 57 + })); 58 + } 59 + 60 + async function searchJsr(query: string, signal: AbortSignal): Promise<SearchResult[]> { 61 + const url = `https://api.jsr.io/packages?query=${encodeURIComponent(query)}&limit=10`; 62 + const response = await fetch(url, { signal }); 63 + if (!response.ok) { 64 + return []; 65 + } 66 + 67 + const data = (await response.json()) as JsrSearchResponse; 68 + 69 + return data.items.map((item) => ({ 70 + name: `@${item.scope}/${item.name}`, 71 + version: item.latestVersion, 72 + description: item.description, 73 + registry: 'jsr' as const, 74 + })); 75 + } 76 + 77 + interface ParsedQuery { 78 + registry: Registry | null; 79 + query: string; 80 + } 81 + 82 + function parseQuery(input: string): ParsedQuery { 83 + const trimmed = input.trim(); 84 + if (trimmed.startsWith('npm:')) { 85 + return { registry: 'npm', query: trimmed.slice(4).trim() }; 86 + } 87 + if (trimmed.startsWith('jsr:')) { 88 + return { registry: 'jsr', query: trimmed.slice(4).trim() }; 89 + } 90 + return { registry: null, query: trimmed }; 91 + } 92 + 93 + // #endregion 94 + 95 + // #region component 96 + 97 + interface PackageSearchInputProps { 98 + value: string; 99 + onChange: (next: string) => void; 100 + onSelect?: (specifier: string) => void; 101 + autofocus?: boolean; 102 + disabled?: boolean; 103 + } 104 + 105 + const PackageSearchInput = (props: PackageSearchInputProps) => { 106 + let listboxRef: HTMLDivElement | undefined; 107 + 108 + const [createAbortSignal] = makeAbortable(); 109 + 110 + const [open, setOpen] = createSignal(false); 111 + const [activeIndex, setActiveIndex] = createSignal(-1); 112 + 113 + const throttledValue = createTrailingThrottle(() => normalizeWhitespace(props.value), 500); 114 + const parsed = createMemo(() => parseQuery(props.value)); 115 + 116 + const [results] = createQuery( 117 + () => { 118 + const { registry, query } = parseQuery(throttledValue()); 119 + if (query.length < 3) { 120 + return null; 121 + } 122 + 123 + return { registry, query }; 124 + }, 125 + async ({ registry, query }) => { 126 + const signal = createAbortSignal(); 127 + if (registry === 'npm') { 128 + return searchNpm(query, signal); 129 + } 130 + if (registry === 'jsr') { 131 + return searchJsr(query, signal); 132 + } 133 + // default to npm when no prefix 134 + return searchNpm(query, signal); 135 + }, 136 + ); 137 + 138 + const showPopover = () => !props.disabled && open() && parsed().query.length >= 2; 139 + 140 + createEffect(() => { 141 + if (props.disabled) { 142 + setOpen(false); 143 + setActiveIndex(-1); 144 + } 145 + }); 146 + 147 + const handleSelect = (result: SearchResult) => { 148 + const specifier = formatPackageSpecifier({ 149 + registry: result.registry, 150 + name: result.name, 151 + range: 'latest', 152 + }); 153 + props.onChange(specifier); 154 + props.onSelect?.(specifier); 155 + setOpen(false); 156 + setActiveIndex(-1); 157 + }; 158 + 159 + const handleKeyDown = (ev: KeyboardEvent) => { 160 + if (props.disabled) { 161 + return; 162 + } 163 + 164 + const items = results() ?? []; 165 + 166 + switch (ev.key) { 167 + case 'ArrowDown': { 168 + if (items.length === 0) { 169 + return; 170 + } 171 + ev.preventDefault(); 172 + setActiveIndex((i) => (i + 1) % items.length); 173 + break; 174 + } 175 + case 'ArrowUp': { 176 + if (items.length === 0) { 177 + return; 178 + } 179 + ev.preventDefault(); 180 + setActiveIndex((i) => (i <= 0 ? items.length - 1 : i - 1)); 181 + break; 182 + } 183 + case 'Enter': { 184 + ev.preventDefault(); 185 + const idx = activeIndex(); 186 + if (idx >= 0 && idx < items.length) { 187 + handleSelect(items[idx]); 188 + } else { 189 + const parsed = parsePackageSpecifier(props.value.trim()); 190 + if (parsed) { 191 + props.onSelect?.(formatPackageSpecifier(parsed)); 192 + setOpen(false); 193 + setActiveIndex(-1); 194 + } 195 + } 196 + break; 197 + } 198 + case 'Escape': { 199 + ev.preventDefault(); 200 + setOpen(false); 201 + setActiveIndex(-1); 202 + break; 203 + } 204 + } 205 + }; 206 + 207 + return ( 208 + <div class="relative flex flex-col"> 209 + <Input 210 + inputRef={(node) => { 211 + onMount(() => { 212 + if (props.autofocus) { 213 + node.focus(); 214 + } 215 + }); 216 + }} 217 + disabled={props.disabled} 218 + value={props.value} 219 + onInput={(ev) => { 220 + props.onChange(ev.currentTarget.value); 221 + setOpen(true); 222 + setActiveIndex(-1); 223 + }} 224 + onFocus={() => setOpen(true)} 225 + onBlur={() => setOpen(false)} 226 + onKeyDown={handleKeyDown} 227 + placeholder="Search packages (npm: or jsr:)" 228 + role="combobox" 229 + aria-expanded={showPopover()} 230 + aria-autocomplete="list" 231 + contentBefore={ 232 + <Show 233 + when={results.state === 'pending' || results.state === 'refreshing'} 234 + fallback={<LucideSearch />} 235 + > 236 + <LucideLoader class="animate-spin-linear" /> 237 + </Show> 238 + } 239 + /> 240 + 241 + {/* listbox */} 242 + <Show when={showPopover()}> 243 + <div 244 + ref={(el) => (listboxRef = el)} 245 + class="absolute top-full right-0 left-0 z-10 mt-0.5 flex max-h-80 flex-col gap-0.5 overflow-y-auto rounded-md bg-neutral-background-1 p-1 text-base-300 shadow-16" 246 + role="listbox" 247 + tabindex={-1} 248 + onMouseDown={(e) => e.preventDefault()} 249 + > 250 + <Show 251 + when={results() && results()!.length > 0} 252 + fallback={ 253 + <div class="px-2 py-1.5 text-base-300 text-neutral-foreground-3"> 254 + {results.loading ? 'Searching...' : 'No packages found'} 255 + </div> 256 + } 257 + > 258 + <For each={results()}> 259 + {(result, index) => ( 260 + <div 261 + ref={(el) => { 262 + createEffect(() => { 263 + if (activeIndex() === index() && modality() === 'keyboard' && listboxRef) { 264 + const padding = 4; // matches p-1 265 + const listboxRect = listboxRef.getBoundingClientRect(); 266 + const optionRect = el.getBoundingClientRect(); 267 + 268 + if (optionRect.top < listboxRect.top + padding) { 269 + listboxRef.scrollTop -= listboxRect.top + padding - optionRect.top; 270 + } else if (optionRect.bottom > listboxRect.bottom - padding) { 271 + listboxRef.scrollTop += optionRect.bottom - (listboxRect.bottom - padding); 272 + } 273 + } 274 + }); 275 + }} 276 + role="option" 277 + aria-selected={activeIndex() === index()} 278 + class="flex gap-2 rounded-md px-2 py-1.5 text-base-300 text-neutral-foreground-1 select-none" 279 + classList={{ 280 + 'bg-neutral-background-1-hover': activeIndex() === index(), 281 + 'hover:bg-neutral-background-1-hover active:bg-neutral-background-1-pressed': 282 + modality() === 'pointer', 283 + }} 284 + onMouseOver={() => modality() === 'pointer' && setActiveIndex(index())} 285 + onClick={() => handleSelect(result)} 286 + > 287 + <div class="grid size-5 shrink-0 place-items-center text-neutral-foreground-3"> 288 + <LucidePackage class="size-4" /> 289 + </div> 290 + 291 + <div class="flex min-w-0 grow flex-col"> 292 + <div class="flex gap-1"> 293 + {result.registry === 'jsr' && ( 294 + <span class="font-medium text-neutral-foreground-3">jsr:</span> 295 + )} 296 + 297 + <span class="min-w-0 wrap-break-word">{result.name}</span> 298 + 299 + <span class="my-0.5 shrink-0 text-base-200 text-neutral-foreground-3"> 300 + {result.version} 301 + </span> 302 + </div> 303 + 304 + <span class="line-clamp-2 text-base-200 text-neutral-foreground-3 empty:hidden"> 305 + {result.description ?? ''} 306 + </span> 307 + </div> 308 + </div> 309 + )} 310 + </For> 311 + </Show> 312 + </div> 313 + </Show> 314 + </div> 315 + ); 316 + }; 317 + 318 + export default PackageSearchInput; 319 + 320 + // #endregion
+43
src/icons/central.tsx
··· 1 + // from Central Icons - https://centralicons.com 2 + // stroke: 2px - corner: 2px 3 + 4 + import type { JSX } from 'solid-js/jsx-runtime'; 5 + 6 + export function CentralCircleCheckSolid(props: JSX.IntrinsicElements['svg']) { 7 + return ( 8 + <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" {...props}> 9 + <path 10 + fill-rule="evenodd" 11 + clip-rule="evenodd" 12 + d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z" 13 + fill="currentColor" 14 + /> 15 + </svg> 16 + ); 17 + } 18 + 19 + export function CentralCircleXSolid(props: JSX.IntrinsicElements['svg']) { 20 + return ( 21 + <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" {...props}> 22 + <path 23 + fill-rule="evenodd" 24 + clip-rule="evenodd" 25 + d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM9.70711 8.29289C9.31658 7.90237 8.68342 7.90237 8.29289 8.29289C7.90237 8.68342 7.90237 9.31658 8.29289 9.70711L10.5858 12L8.29289 14.2929C7.90237 14.6834 7.90237 15.3166 8.29289 15.7071C8.68342 16.0976 9.31658 16.0976 9.70711 15.7071L12 13.4142L14.2929 15.7071C14.6834 16.0976 15.3166 16.0976 15.7071 15.7071C16.0976 15.3166 16.0976 14.6834 15.7071 14.2929L13.4142 12L15.7071 9.70711C16.0976 9.31658 16.0976 8.68342 15.7071 8.29289C15.3166 7.90237 14.6834 7.90237 14.2929 8.29289L12 10.5858L9.70711 8.29289Z" 26 + fill="currentColor" 27 + /> 28 + </svg> 29 + ); 30 + } 31 + 32 + export function CentralExclamationTriangleSolid(props: JSX.IntrinsicElements['svg']) { 33 + return ( 34 + <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" {...props}> 35 + <path 36 + fill-rule="evenodd" 37 + clip-rule="evenodd" 38 + d="M9.40767 3.45763C10.5653 1.47311 13.4327 1.47311 14.5903 3.45762L21.6083 15.4884C22.7749 17.4883 21.3323 20 19.0169 20H4.98108C2.66571 20 1.22309 17.4883 2.38974 15.4884L9.40767 3.45763ZM12 8C12.5523 8 13 8.44772 13 9V12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12V9C11 8.44772 11.4477 8 12 8ZM10.75 15C10.75 14.3096 11.3096 13.75 12 13.75C12.6904 13.75 13.25 14.3096 13.25 15C13.25 15.6904 12.6904 16.25 12 16.25C11.3096 16.25 10.75 15.6904 10.75 15Z" 39 + fill="currentColor" 40 + /> 41 + </svg> 42 + ); 43 + }
+261
src/icons/lucide.tsx
··· 1 + // from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE 2 + 3 + import type { JSX } from 'solid-js/jsx-runtime'; 4 + 5 + export function LucideArrowDown(props: JSX.IntrinsicElements['svg']) { 6 + return ( 7 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 8 + <path 9 + fill="none" 10 + stroke="currentColor" 11 + stroke-linecap="round" 12 + stroke-linejoin="round" 13 + stroke-width="2" 14 + d="M12 5v14m7-7l-7 7l-7-7" 15 + /> 16 + </svg> 17 + ); 18 + } 19 + 20 + export function LucideBox(props: JSX.IntrinsicElements['svg']) { 21 + return ( 22 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 23 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 24 + <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" /> 25 + <path d="m3.3 7l8.7 5l8.7-5M12 22V12" /> 26 + </g> 27 + </svg> 28 + ); 29 + } 30 + 31 + export function LucideCheck(props: JSX.IntrinsicElements['svg']) { 32 + return ( 33 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 34 + <path 35 + fill="none" 36 + stroke="currentColor" 37 + stroke-linecap="round" 38 + stroke-linejoin="round" 39 + stroke-width="2" 40 + d="M20 6L9 17l-5-5" 41 + /> 42 + </svg> 43 + ); 44 + } 45 + 46 + export function LucideChevronDown(props: JSX.IntrinsicElements['svg']) { 47 + return ( 48 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 49 + <path 50 + fill="none" 51 + stroke="currentColor" 52 + stroke-linecap="round" 53 + stroke-linejoin="round" 54 + stroke-width="2" 55 + d="m6 9l6 6l6-6" 56 + /> 57 + </svg> 58 + ); 59 + } 60 + 61 + export function LucideCircleAlert(props: JSX.IntrinsicElements['svg']) { 62 + return ( 63 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 64 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 65 + <circle cx="12" cy="12" r="10" /> 66 + <path d="M12 8v4m0 4h.01" /> 67 + </g> 68 + </svg> 69 + ); 70 + } 71 + 72 + export function LucideFileCode(props: JSX.IntrinsicElements['svg']) { 73 + return ( 74 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 75 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 76 + <path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> 77 + <path d="M14 2v5a1 1 0 0 0 1 1h5m-10 4.5L8 15l2 2.5m4-5l2 2.5l-2 2.5" /> 78 + </g> 79 + </svg> 80 + ); 81 + } 82 + 83 + export function LucideHandHeart(props: JSX.IntrinsicElements['svg']) { 84 + return ( 85 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 86 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 87 + <path d="M11 14h2a2 2 0 0 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 16" /> 88 + <path d="m14.45 13.39l5.05-4.694C20.196 8 21 6.85 21 5.75a2.75 2.75 0 0 0-4.797-1.837a.276.276 0 0 1-.406 0A2.75 2.75 0 0 0 11 5.75c0 1.2.802 2.248 1.5 2.946L16 11.95M2 15l6 6" /> 89 + <path d="m7 20l1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a1 1 0 0 0-2.75-2.91" /> 90 + </g> 91 + </svg> 92 + ); 93 + } 94 + 95 + export function LucideHardDrive(props: JSX.IntrinsicElements['svg']) { 96 + return ( 97 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 98 + <path 99 + fill="none" 100 + stroke="currentColor" 101 + stroke-linecap="round" 102 + stroke-linejoin="round" 103 + stroke-width="2" 104 + d="M22 12H2m3.45-6.89L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11M6 16h.01M10 16h.01" 105 + /> 106 + </svg> 107 + ); 108 + } 109 + 110 + export function LucideLoader(props: JSX.IntrinsicElements['svg']) { 111 + return ( 112 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 113 + <path 114 + fill="none" 115 + stroke="currentColor" 116 + stroke-linecap="round" 117 + stroke-linejoin="round" 118 + stroke-width="2" 119 + d="M12 2v4m4.2 1.8l2.9-2.9M18 12h4m-5.8 4.2l2.9 2.9M12 18v4m-7.1-2.9l2.9-2.9M2 12h4M4.9 4.9l2.9 2.9" 120 + /> 121 + </svg> 122 + ); 123 + } 124 + 125 + export function LucideMoon(props: JSX.IntrinsicElements['svg']) { 126 + return ( 127 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 128 + <path 129 + fill="none" 130 + stroke="currentColor" 131 + stroke-linecap="round" 132 + stroke-linejoin="round" 133 + stroke-width="2" 134 + d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" 135 + /> 136 + </svg> 137 + ); 138 + } 139 + 140 + export function LucidePackage(props: JSX.IntrinsicElements['svg']) { 141 + return ( 142 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 143 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 144 + <path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73zm1 .27V12" /> 145 + <path d="M3.29 7L12 12l8.71-5M7.5 4.27l9 5.15" /> 146 + </g> 147 + </svg> 148 + ); 149 + } 150 + 151 + export function LucidePackageSearch(props: JSX.IntrinsicElements['svg']) { 152 + return ( 153 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 154 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 155 + <path d="M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14M7.5 4.27l9 5.15" /> 156 + <path d="M3.29 7L12 12l8.71-5M12 22V12" /> 157 + <circle cx="18.5" cy="15.5" r="2.5" /> 158 + <path d="M20.27 17.27L22 19" /> 159 + </g> 160 + </svg> 161 + ); 162 + } 163 + 164 + export function LucideScale(props: JSX.IntrinsicElements['svg']) { 165 + return ( 166 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 167 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 168 + <path d="M12 3v18m7-13l3 8a5 5 0 0 1-6 0zV7" /> 169 + <path d="M3 7h1a17 17 0 0 0 8-2a17 17 0 0 0 8 2h1M5 8l3 8a5 5 0 0 1-6 0zV7m2 14h10" /> 170 + </g> 171 + </svg> 172 + ); 173 + } 174 + 175 + export function LucideScissorsLineDashed(props: JSX.IntrinsicElements['svg']) { 176 + return ( 177 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 178 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 179 + <path d="M5.42 9.42L8 12" /> 180 + <circle cx="4" cy="8" r="2" /> 181 + <path d="m14 6-8.58 8.58" /> 182 + <circle cx="4" cy="16" r="2" /> 183 + <path d="M10.8 14.8L14 18m2-6h-2m8 0h-2" /> 184 + </g> 185 + </svg> 186 + ); 187 + } 188 + 189 + export function LucideSettings(props: JSX.IntrinsicElements['svg']) { 190 + return ( 191 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 192 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 193 + <path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0a2.34 2.34 0 0 0 3.319 1.915a2.34 2.34 0 0 1 2.33 4.033a2.34 2.34 0 0 0 0 3.831a2.34 2.34 0 0 1-2.33 4.033a2.34 2.34 0 0 0-3.319 1.915a2.34 2.34 0 0 1-4.659 0a2.34 2.34 0 0 0-3.32-1.915a2.34 2.34 0 0 1-2.33-4.033a2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> 194 + <circle cx="12" cy="12" r="3" /> 195 + </g> 196 + </svg> 197 + ); 198 + } 199 + 200 + export function LucideSearch(props: JSX.IntrinsicElements['svg']) { 201 + return ( 202 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 203 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 204 + <path d="m21 21l-4.34-4.34" /> 205 + <circle cx="11" cy="11" r="8" /> 206 + </g> 207 + </svg> 208 + ); 209 + } 210 + 211 + export function LucideSun(props: JSX.IntrinsicElements['svg']) { 212 + return ( 213 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 214 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 215 + <circle cx="12" cy="12" r="4" /> 216 + <path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" /> 217 + </g> 218 + </svg> 219 + ); 220 + } 221 + 222 + export function LucideWeight(props: JSX.IntrinsicElements['svg']) { 223 + return ( 224 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 225 + <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 226 + <circle cx="12" cy="5" r="3" /> 227 + <path d="M6.5 8a2 2 0 0 0-1.905 1.46L2.1 18.5A2 2 0 0 0 4 21h16a2 2 0 0 0 1.925-2.54L19.4 9.5A2 2 0 0 0 17.48 8Z" /> 228 + </g> 229 + </svg> 230 + ); 231 + } 232 + 233 + export function LucideX(props: JSX.IntrinsicElements['svg']) { 234 + return ( 235 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 236 + <path 237 + fill="none" 238 + stroke="currentColor" 239 + stroke-linecap="round" 240 + stroke-linejoin="round" 241 + stroke-width="2" 242 + d="M18 6L6 18M6 6l12 12" 243 + /> 244 + </svg> 245 + ); 246 + } 247 + 248 + export function LucideZap(props: JSX.IntrinsicElements['svg']) { 249 + return ( 250 + <svg width="1em" height="1em" viewBox="0 0 24 24" {...props}> 251 + <path 252 + fill="none" 253 + stroke="currentColor" 254 + stroke-linecap="round" 255 + stroke-linejoin="round" 256 + stroke-width="2" 257 + d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" 258 + /> 259 + </svg> 260 + ); 261 + }
+13
src/icons/tangled.tsx
··· 1 + import type { JSX } from 'solid-js/jsx-runtime'; 2 + 3 + export function TangledDolly(props: JSX.IntrinsicElements['svg']) { 4 + return ( 5 + <svg width="1em" height="1em" viewBox="0 0 25 25" fill="none" {...props}> 6 + <path 7 + fill="currentColor" 8 + style="stroke-width:.111183" 9 + d="m16.349 24.1-.065-.038-.202-.01-.202-.011-.276-.026-.275-.026v-.053l-.205-.04-.205-.04-.167-.081-.168-.08-.001-.042-.002-.041-.266-.144-.266-.144-.277-.203-.276-.203-.262-.252-.262-.252-.22-.285-.222-.285-.17-.284-.17-.285-.014-.014-.013-.015-.142.162-.142.161-.223.214-.223.215-.186.146-.186.146-.253.163-.252.163-.25.116-.248.115.005.032.005.033-.171.046-.172.046-.338.1-.338.102-.178.045-.178.045-.391.026-.392.026-.355-.035-.356-.035-.038-.03-.037-.03-.077.02-.077.02-.05-.051-.05-.05-.21-.047-.208-.046-.297-.103-.298-.104-.325-.163-.326-.163-.327-.228-.327-.228-.304-.288-.305-.289-.224-.29-.225-.289-.127-.213-.127-.214-.106-.213-.107-.214-.125-.338-.126-.337-.083-.392-.084-.391v-.694l.001-.694.064-.319.065-.319.108-.339.11-.34.157-.319.157-.319.07-.113.07-.114-.098-.068-.099-.067-.178-.102-.178-.101-.267-.196-.267-.195-.262-.252-.262-.252-.189-.235-.188-.235-.16-.247-.16-.246-.129-.266-.129-.266-.12-.338-.12-.338-.083-.391-.083-.391.002-.694.002-.694.1-.426.099-.426.132-.342.133-.341.167-.307.167-.306.218-.296.219-.295.252-.263.252-.262.231-.185.232-.185.231-.151.231-.151.321-.156.321-.155.177-.065.177-.065.178-.338.178-.337.213-.303.212-.302.314-.325.314-.326.257-.195.256-.196.304-.179.305-.179.316-.13.316-.132.21-.067.21-.067.397-.079.397-.08.587.004.587.003.444.092.445.093.303.11.302.11.33.165.33.165.24-.232.239-.231.16-.126.16-.126.16-.102.16-.102.142-.083.143-.082.23-.109.232-.109.267-.099.267-.098.32-.074.32-.073.356-.042.356-.042.427.024.427.024.355.07.356.072.285.093.284.092.286.131.285.131.238.145.238.145.26.195.259.196.29.297.291.296.152.195.152.194.135.215.136.215.154.32.155.32.094.268.094.268.07.331.07.332.01.008.011.009.445.217.445.217.31.216.309.216.31.293.31.294.187.234.187.235.167.258.166.257.153.326.153.326.09.267.09.267.082.391.083.392v.658l-.001.658-.064.316-.063.315-.09.29-.091.289-.123.281-.123.281-.146.253-.147.252-.19.259-.19.258-.256.269-.255.268-.287.223-.286.223-.32.188-.32.188-.044.035-.044.035.057.13.056.13.087.213.088.214.19.73.19.729.064.302.065.302-.001.676-.002.676-.08.374-.08.373-.09.267-.09.267-.19.392-.191.39-.223.321-.223.32-.304.316-.304.315-.284.22-.285.221-.22.133-.22.132-.243.107-.242.106-.089.048-.089.047-.249.072-.249.073-.322.057-.322.058-.283-.003-.283-.003-.07-.003-.072-.003-.178-.004-.178-.003-.124.025-.125.026zm-4.47-5.35.215-.018.206-.068.207-.068.244-.117.245-.118.274-.207.275-.207.229-.257.23-.257.218-.285.22-.285.188-.284.189-.285.214-.373.215-.374.134-.312.134-.312.028-.018.029-.017.197.262.197.262.164.15.164.152.202.092.201.093.303.014.302.014.214-.08.213-.08.2-.205.201-.204.093-.28.092-.278.058-.303.058-.302-.019-.427-.018-.427-.077-.426-.076-.426-.086-.321-.086-.321-.141-.402-.141-.403-.167-.309-.166-.31-.118-.16-.117-.16-.124-.12-.125-.119.019-.183.019-.182-.061-.25-.062-.248-.134-.285-.133-.285-.183-.202-.183-.201-.173-.128-.174-.127-.204.123-.204.123-.267.06-.267.06-.206-.022-.206-.022-.235-.088-.235-.089-.118-.09-.119-.09h-.079l-.055.116-.055.117-.159.181-.159.182-.17.108-.17.108-.221.074-.221.074h-.56l-.196-.067-.195-.067-.114-.059-.113-.058-.24-.222-.24-.22-.095-.085-.096-.085-.219.198-.219.198-.165.079-.165.078-.178.048-.178.048h-.439l-.224-.07-.225-.07-.102.097-.101.097-.121.164-.121.164-.17.063-.17.063-.115.086-.115.086-.11.114-.109.114-.355.529-.355.528-.216.45-.216.45-.222.462-.222.463-.145.338-.146.338-.056.22-.055.22-.016.207-.016.207.034.243.034.243.097.196.096.197.144.125.143.125.188.088.187.087.275.002.275.002.232-.098.23-.097.108-.076.106-.076.368-.294.368-.294.027.017.027.016.023.467.024.467.088.513.089.513.089.365.089.364.131.302.132.303.105.16.105.16.11.119.111.119.285.205.284.206.145.073.144.073.215.056.215.055.245.031.246.03.204-.012.205-.012zm.686-3.498-.113-.06-.106-.135-.106-.134-.044-.184-.044-.183.024-.554.024-.554.035-.427.036-.427.072-.374.072-.373.054-.211.054-.212.068-.132.067-.132.133-.11.132-.108.188-.042.187-.042.17.064.17.065.115.124.114.124.042.185.041.185-.111.46-.111.46-.034.266-.034.266-.04.818-.04.818-.037.152-.038.151-.111.111-.111.11-.115.05-.114.049-.188-.002-.188-.001zm-2.809-.358-.146-.069-.088-.12-.088-.119-.039-.106-.038-.107-.023-.135-.022-.135-.032-.47-.032-.47.036-.444.037-.445.048-.215.05-.216.075-.203.076-.203.094-.112.094-.11.143-.066.144-.066h.285l.142.066.142.066.093.103.093.102.04.12.041.122v.305l-.033.088-.034.088-.057.275-.056.276v.86l.043.393.043.393-.092.2-.092.201-.149.099-.148.098-.202.012-.201.012z" 10 + /> 11 + </svg> 12 + ); 13 + }
+920
src/index.css
··· 1 + @import 'tailwindcss'; 2 + 3 + @theme { 4 + --text-*: initial; 5 + --text-base-100: 10px; 6 + --text-base-100--line-height: 14px; 7 + --text-base-200: 12px; 8 + --text-base-200--line-height: 16px; 9 + --text-base-300: 14px; 10 + --text-base-300--line-height: 20px; 11 + --text-base-400: 16px; 12 + --text-base-400--line-height: 22px; 13 + --text-base-500: 20px; 14 + --text-base-500--line-height: 28px; 15 + --text-base-600: 24px; 16 + --text-base-600--line-height: 32px; 17 + --text-hero-700: 28px; 18 + --text-hero-700--line-height: 36px; 19 + --text-hero-800: 32px; 20 + --text-hero-800--line-height: 40px; 21 + --text-hero-900: 40px; 22 + --text-hero-900--line-height: 52px; 23 + --text-hero-1000: 68px; 24 + --text-hero-1000--line-height: 92px; 25 + } 26 + 27 + @theme inline { 28 + --color-*: initial; 29 + 30 + --color-background-overlay: var(--color-background-overlay); 31 + --color-brand-background: var(--color-brand-background); 32 + --color-brand-background-2: var(--color-brand-background-2); 33 + --color-brand-background-2-hover: var(--color-brand-background-2-hover); 34 + --color-brand-background-2-pressed: var(--color-brand-background-2-pressed); 35 + --color-brand-background-3-static: var(--color-brand-background-3-static); 36 + --color-brand-background-4-static: var(--color-brand-background-4-static); 37 + --color-brand-background-hover: var(--color-brand-background-hover); 38 + --color-brand-background-inverted: var(--color-brand-background-inverted); 39 + --color-brand-background-inverted-hover: var(--color-brand-background-inverted-hover); 40 + --color-brand-background-inverted-pressed: var(--color-brand-background-inverted-pressed); 41 + --color-brand-background-inverted-selected: var(--color-brand-background-inverted-selected); 42 + --color-brand-background-pressed: var(--color-brand-background-pressed); 43 + --color-brand-background-selected: var(--color-brand-background-selected); 44 + --color-brand-background-static: var(--color-brand-background-static); 45 + --color-brand-foreground-1: var(--color-brand-foreground-1); 46 + --color-brand-foreground-2: var(--color-brand-foreground-2); 47 + --color-brand-foreground-2-hover: var(--color-brand-foreground-2-hover); 48 + --color-brand-foreground-2-pressed: var(--color-brand-foreground-2-pressed); 49 + --color-brand-foreground-inverted: var(--color-brand-foreground-inverted); 50 + --color-brand-foreground-inverted-hover: var(--color-brand-foreground-inverted-hover); 51 + --color-brand-foreground-inverted-pressed: var(--color-brand-foreground-inverted-pressed); 52 + --color-brand-foreground-link: var(--color-brand-foreground-link); 53 + --color-brand-foreground-link-hover: var(--color-brand-foreground-link-hover); 54 + --color-brand-foreground-link-pressed: var(--color-brand-foreground-link-pressed); 55 + --color-brand-foreground-link-selected: var(--color-brand-foreground-link-selected); 56 + --color-brand-foreground-on-light: var(--color-brand-foreground-on-light); 57 + --color-brand-foreground-on-light-hover: var(--color-brand-foreground-on-light-hover); 58 + --color-brand-foreground-on-light-pressed: var(--color-brand-foreground-on-light-pressed); 59 + --color-brand-foreground-on-light-selected: var(--color-brand-foreground-on-light-selected); 60 + --color-brand-stroke-1: var(--color-brand-stroke-1); 61 + --color-brand-stroke-2: var(--color-brand-stroke-2); 62 + --color-brand-stroke-2-contrast: var(--color-brand-stroke-2-contrast); 63 + --color-brand-stroke-2-hover: var(--color-brand-stroke-2-hover); 64 + --color-brand-stroke-2-pressed: var(--color-brand-stroke-2-pressed); 65 + --color-compound-brand-background: var(--color-compound-brand-background); 66 + --color-compound-brand-background-hover: var(--color-compound-brand-background-hover); 67 + --color-compound-brand-background-pressed: var(--color-compound-brand-background-pressed); 68 + --color-compound-brand-foreground-1: var(--color-compound-brand-foreground-1); 69 + --color-compound-brand-foreground-1-hover: var(--color-compound-brand-foreground-1-hover); 70 + --color-compound-brand-foreground-1-pressed: var(--color-compound-brand-foreground-1-pressed); 71 + --color-compound-brand-stroke: var(--color-compound-brand-stroke); 72 + --color-compound-brand-stroke-hover: var(--color-compound-brand-stroke-hover); 73 + --color-compound-brand-stroke-pressed: var(--color-compound-brand-stroke-pressed); 74 + --color-neutral-background-1: var(--color-neutral-background-1); 75 + --color-neutral-background-1-hover: var(--color-neutral-background-1-hover); 76 + --color-neutral-background-1-pressed: var(--color-neutral-background-1-pressed); 77 + --color-neutral-background-1-selected: var(--color-neutral-background-1-selected); 78 + --color-neutral-background-2: var(--color-neutral-background-2); 79 + --color-neutral-background-2-hover: var(--color-neutral-background-2-hover); 80 + --color-neutral-background-2-pressed: var(--color-neutral-background-2-pressed); 81 + --color-neutral-background-2-selected: var(--color-neutral-background-2-selected); 82 + --color-neutral-background-3: var(--color-neutral-background-3); 83 + --color-neutral-background-3-hover: var(--color-neutral-background-3-hover); 84 + --color-neutral-background-3-pressed: var(--color-neutral-background-3-pressed); 85 + --color-neutral-background-3-selected: var(--color-neutral-background-3-selected); 86 + --color-neutral-background-4: var(--color-neutral-background-4); 87 + --color-neutral-background-4-hover: var(--color-neutral-background-4-hover); 88 + --color-neutral-background-4-pressed: var(--color-neutral-background-4-pressed); 89 + --color-neutral-background-4-selected: var(--color-neutral-background-4-selected); 90 + --color-neutral-background-5: var(--color-neutral-background-5); 91 + --color-neutral-background-5-hover: var(--color-neutral-background-5-hover); 92 + --color-neutral-background-5-pressed: var(--color-neutral-background-5-pressed); 93 + --color-neutral-background-5-selected: var(--color-neutral-background-5-selected); 94 + --color-neutral-background-6: var(--color-neutral-background-6); 95 + --color-neutral-background-alpha: var(--color-neutral-background-alpha); 96 + --color-neutral-background-alpha-2: var(--color-neutral-background-alpha-2); 97 + --color-neutral-background-disabled: var(--color-neutral-background-disabled); 98 + --color-neutral-background-inverted: var(--color-neutral-background-inverted); 99 + --color-neutral-background-inverted-hover: var(--color-neutral-background-inverted-hover); 100 + --color-neutral-background-inverted-pressed: var(--color-neutral-background-inverted-pressed); 101 + --color-neutral-background-inverted-selected: var(--color-neutral-background-inverted-selected); 102 + --color-neutral-background-static: var(--color-neutral-background-static); 103 + --color-neutral-foreground-1: var(--color-neutral-foreground-1); 104 + --color-neutral-foreground-1-hover: var(--color-neutral-foreground-1-hover); 105 + --color-neutral-foreground-1-pressed: var(--color-neutral-foreground-1-pressed); 106 + --color-neutral-foreground-1-selected: var(--color-neutral-foreground-1-selected); 107 + --color-neutral-foreground-1-static: var(--color-neutral-foreground-1-static); 108 + --color-neutral-foreground-2: var(--color-neutral-foreground-2); 109 + --color-neutral-foreground-2-brand-hover: var(--color-neutral-foreground-2-brand-hover); 110 + --color-neutral-foreground-2-brand-pressed: var(--color-neutral-foreground-2-brand-pressed); 111 + --color-neutral-foreground-2-brand-selected: var(--color-neutral-foreground-2-brand-selected); 112 + --color-neutral-foreground-2-hover: var(--color-neutral-foreground-2-hover); 113 + --color-neutral-foreground-2-pressed: var(--color-neutral-foreground-2-pressed); 114 + --color-neutral-foreground-2-selected: var(--color-neutral-foreground-2-selected); 115 + --color-neutral-foreground-3: var(--color-neutral-foreground-3); 116 + --color-neutral-foreground-3-hover: var(--color-neutral-foreground-3-hover); 117 + --color-neutral-foreground-3-pressed: var(--color-neutral-foreground-3-pressed); 118 + --color-neutral-foreground-3-selected: var(--color-neutral-foreground-3-selected); 119 + --color-neutral-foreground-4: var(--color-neutral-foreground-4); 120 + --color-neutral-foreground-5: var(--color-neutral-foreground-5); 121 + --color-neutral-foreground-5-hover: var(--color-neutral-foreground-5-hover); 122 + --color-neutral-foreground-5-pressed: var(--color-neutral-foreground-5-pressed); 123 + --color-neutral-foreground-5-selected: var(--color-neutral-foreground-5-selected); 124 + --color-neutral-foreground-disabled: var(--color-neutral-foreground-disabled); 125 + --color-neutral-foreground-inverted: var(--color-neutral-foreground-inverted); 126 + --color-neutral-foreground-inverted-2: var(--color-neutral-foreground-inverted-2); 127 + --color-neutral-foreground-inverted-hover: var(--color-neutral-foreground-inverted-hover); 128 + --color-neutral-foreground-inverted-pressed: var(--color-neutral-foreground-inverted-pressed); 129 + --color-neutral-foreground-inverted-selected: var(--color-neutral-foreground-inverted-selected); 130 + --color-neutral-foreground-on-brand: var(--color-neutral-foreground-on-brand); 131 + --color-neutral-foreground-static-inverted: var(--color-neutral-foreground-static-inverted); 132 + --color-neutral-shadow-ambient: var(--color-neutral-shadow-ambient); 133 + --color-neutral-shadow-ambient-darker: var(--color-neutral-shadow-ambient-darker); 134 + --color-neutral-shadow-ambient-lighter: var(--color-neutral-shadow-ambient-lighter); 135 + --color-neutral-shadow-key: var(--color-neutral-shadow-key); 136 + --color-neutral-shadow-key-darker: var(--color-neutral-shadow-key-darker); 137 + --color-neutral-shadow-key-lighter: var(--color-neutral-shadow-key-lighter); 138 + --color-neutral-stroke-1: var(--color-neutral-stroke-1); 139 + --color-neutral-stroke-1-hover: var(--color-neutral-stroke-1-hover); 140 + --color-neutral-stroke-1-pressed: var(--color-neutral-stroke-1-pressed); 141 + --color-neutral-stroke-1-selected: var(--color-neutral-stroke-1-selected); 142 + --color-neutral-stroke-2: var(--color-neutral-stroke-2); 143 + --color-neutral-stroke-3: var(--color-neutral-stroke-3); 144 + --color-neutral-stroke-accessible: var(--color-neutral-stroke-accessible); 145 + --color-neutral-stroke-accessible-hover: var(--color-neutral-stroke-accessible-hover); 146 + --color-neutral-stroke-accessible-pressed: var(--color-neutral-stroke-accessible-pressed); 147 + --color-neutral-stroke-accessible-selected: var(--color-neutral-stroke-accessible-selected); 148 + --color-neutral-stroke-alpha: var(--color-neutral-stroke-alpha); 149 + --color-neutral-stroke-alpha-2: var(--color-neutral-stroke-alpha-2); 150 + --color-neutral-stroke-disabled: var(--color-neutral-stroke-disabled); 151 + --color-neutral-stroke-inverted-disabled: var(--color-neutral-stroke-inverted-disabled); 152 + --color-neutral-stroke-on-brand: var(--color-neutral-stroke-on-brand); 153 + --color-neutral-stroke-on-brand-2: var(--color-neutral-stroke-on-brand-2); 154 + --color-neutral-stroke-on-brand-2-hover: var(--color-neutral-stroke-on-brand-2-hover); 155 + --color-neutral-stroke-on-brand-2-pressed: var(--color-neutral-stroke-on-brand-2-pressed); 156 + --color-neutral-stroke-on-brand-2-selected: var(--color-neutral-stroke-on-brand-2-selected); 157 + --color-neutral-stroke-subtle: var(--color-neutral-stroke-subtle); 158 + --color-scrollbar-overlay: var(--color-scrollbar-overlay); 159 + --color-status-danger-background-1: var(--color-status-danger-background-1); 160 + --color-status-danger-background-2: var(--color-status-danger-background-2); 161 + --color-status-danger-background-3: var(--color-status-danger-background-3); 162 + --color-status-danger-background-3-hover: var(--color-status-danger-background-3-hover); 163 + --color-status-danger-background-3-pressed: var(--color-status-danger-background-3-pressed); 164 + --color-status-danger-border-1: var(--color-status-danger-border-1); 165 + --color-status-danger-border-2: var(--color-status-danger-border-2); 166 + --color-status-danger-border-active: var(--color-status-danger-border-active); 167 + --color-status-danger-foreground-1: var(--color-status-danger-foreground-1); 168 + --color-status-danger-foreground-2: var(--color-status-danger-foreground-2); 169 + --color-status-danger-foreground-3: var(--color-status-danger-foreground-3); 170 + --color-status-danger-foreground-inverted: var(--color-status-danger-foreground-inverted); 171 + --color-status-success-background-1: var(--color-status-success-background-1); 172 + --color-status-success-background-2: var(--color-status-success-background-2); 173 + --color-status-success-background-3: var(--color-status-success-background-3); 174 + --color-status-success-border-1: var(--color-status-success-border-1); 175 + --color-status-success-border-2: var(--color-status-success-border-2); 176 + --color-status-success-border-active: var(--color-status-success-border-active); 177 + --color-status-success-foreground-1: var(--color-status-success-foreground-1); 178 + --color-status-success-foreground-2: var(--color-status-success-foreground-2); 179 + --color-status-success-foreground-3: var(--color-status-success-foreground-3); 180 + --color-status-success-foreground-inverted: var(--color-status-success-foreground-inverted); 181 + --color-status-warning-background-1: var(--color-status-warning-background-1); 182 + --color-status-warning-background-2: var(--color-status-warning-background-2); 183 + --color-status-warning-background-3: var(--color-status-warning-background-3); 184 + --color-status-warning-border-1: var(--color-status-warning-border-1); 185 + --color-status-warning-border-2: var(--color-status-warning-border-2); 186 + --color-status-warning-border-active: var(--color-status-warning-border-active); 187 + --color-status-warning-foreground-1: var(--color-status-warning-foreground-1); 188 + --color-status-warning-foreground-2: var(--color-status-warning-foreground-2); 189 + --color-status-warning-foreground-3: var(--color-status-warning-foreground-3); 190 + --color-status-warning-foreground-inverted: var(--color-status-warning-foreground-inverted); 191 + --color-stroke-focus-1: var(--color-stroke-focus-1); 192 + --color-stroke-focus-2: var(--color-stroke-focus-2); 193 + --color-subtle-background: var(--color-subtle-background); 194 + --color-subtle-background-hover: var(--color-subtle-background-hover); 195 + --color-subtle-background-inverted: var(--color-subtle-background-inverted); 196 + --color-subtle-background-inverted-hover: var(--color-subtle-background-inverted-hover); 197 + --color-subtle-background-inverted-pressed: var(--color-subtle-background-inverted-pressed); 198 + --color-subtle-background-inverted-selected: var(--color-subtle-background-inverted-selected); 199 + --color-subtle-background-light-alpha-hover: var(--color-subtle-background-light-alpha-hover); 200 + --color-subtle-background-light-alpha-pressed: var(--color-subtle-background-light-alpha-pressed); 201 + --color-subtle-background-light-alpha-selected: var(--color-subtle-background-light-alpha-selected); 202 + --color-subtle-background-pressed: var(--color-subtle-background-pressed); 203 + --color-subtle-background-selected: var(--color-subtle-background-selected); 204 + --color-transparent-background: var(--color-transparent-background); 205 + --color-transparent-background-hover: var(--color-transparent-background-hover); 206 + --color-transparent-background-pressed: var(--color-transparent-background-pressed); 207 + --color-transparent-background-selected: var(--color-transparent-background-selected); 208 + --color-transparent-stroke: var(--color-transparent-stroke); 209 + --color-transparent-stroke-disabled: var(--color-transparent-stroke-disabled); 210 + --color-transparent-stroke-interactive: var(--color-transparent-stroke-interactive); 211 + 212 + --ease-*: initial; 213 + --ease-fluent: cubic-bezier(0.33, 0, 0.67, 1); 214 + --ease-accelerate-min: cubic-bezier(0.8, 0, 0.78, 1); 215 + --ease-decelerate-mid: cubic-bezier(0, 0, 0, 1); 216 + 217 + --duration-*: initial; 218 + --duration-faster: 100ms; 219 + --duration-fast: 150ms; 220 + --duration-normal: 200ms; 221 + --duration-gentle: 250ms; 222 + --duration-slow: 300ms; 223 + --duration-slower: 400ms; 224 + 225 + --animate-*: initial; 226 + --animate-spin-linear: spin-linear 1.5s linear infinite; 227 + --animate-spin-swing: spin-swing 1.5s var(--ease-fluent) infinite; 228 + --animate-spin-start: spin-start 1.5s var(--ease-fluent) infinite; 229 + --animate-spin-end: spin-end 1.5s var(--ease-fluent) infinite; 230 + 231 + --radius-*: initial; 232 + --radius-none: 0; 233 + --radius-sm: 2px; 234 + --radius-md: 4px; 235 + --radius-lg: 6px; 236 + --radius-xl: 8px; 237 + --radius-full: 10000px; 238 + 239 + --shadow-*: initial; 240 + --shadow-2: var(--shadow-2); 241 + --shadow-4: var(--shadow-4); 242 + --shadow-8: var(--shadow-8); 243 + --shadow-16: var(--shadow-16); 244 + --shadow-28: var(--shadow-28); 245 + --shadow-64: var(--shadow-64); 246 + } 247 + 248 + @keyframes spin-linear { 249 + 100% { 250 + transform: rotate(360deg); 251 + } 252 + } 253 + 254 + @keyframes spin-swing { 255 + 0% { 256 + transform: rotate(-135deg); 257 + } 258 + 50% { 259 + transform: rotate(0deg); 260 + } 261 + 100% { 262 + transform: rotate(225deg); 263 + } 264 + } 265 + 266 + @keyframes spin-start { 267 + 0%, 268 + 100% { 269 + transform: rotate(0deg); 270 + } 271 + 50% { 272 + transform: rotate(-80deg); 273 + } 274 + } 275 + 276 + @keyframes spin-end { 277 + 0%, 278 + 100% { 279 + transform: rotate(0deg); 280 + } 281 + 50% { 282 + transform: rotate(70deg); 283 + } 284 + } 285 + 286 + :root { 287 + & { 288 + --color-background-overlay: rgba(0, 0, 0, 0.4); 289 + --color-brand-background: #ad46ff; 290 + --color-brand-background-2: #fcf8ff; 291 + --color-brand-background-2-hover: #f2e2ff; 292 + --color-brand-background-2-pressed: #d8a8ff; 293 + --color-brand-background-3-static: #8335c7; 294 + --color-brand-background-4-static: #53227e; 295 + --color-brand-background-hover: #983ee6; 296 + --color-brand-background-inverted: #ffffff; 297 + --color-brand-background-inverted-hover: #fcf8ff; 298 + --color-brand-background-inverted-pressed: #e5c5ff; 299 + --color-brand-background-inverted-selected: #f2e2ff; 300 + --color-brand-background-pressed: #53227e; 301 + --color-brand-background-selected: #8335c7; 302 + --color-brand-background-static: #ad46ff; 303 + --color-brand-foreground-1: #ad46ff; 304 + --color-brand-foreground-2: #983ee6; 305 + --color-brand-foreground-2-hover: #8335c7; 306 + --color-brand-foreground-2-pressed: #34154f; 307 + --color-brand-foreground-inverted: #ba65ff; 308 + --color-brand-foreground-inverted-hover: #c47bff; 309 + --color-brand-foreground-inverted-pressed: #ba65ff; 310 + --color-brand-foreground-link: #983ee6; 311 + --color-brand-foreground-link-hover: #8335c7; 312 + --color-brand-foreground-link-pressed: #53227e; 313 + --color-brand-foreground-link-selected: #983ee6; 314 + --color-brand-foreground-on-light: #ad46ff; 315 + --color-brand-foreground-on-light-hover: #983ee6; 316 + --color-brand-foreground-on-light-pressed: #6f2da8; 317 + --color-brand-foreground-on-light-selected: #8335c7; 318 + --color-brand-stroke-1: #ad46ff; 319 + --color-brand-stroke-2: #e5c5ff; 320 + --color-brand-stroke-2-contrast: #e5c5ff; 321 + --color-brand-stroke-2-hover: #ce91ff; 322 + --color-brand-stroke-2-pressed: #ad46ff; 323 + --color-compound-brand-background: #ad46ff; 324 + --color-compound-brand-background-hover: #983ee6; 325 + --color-compound-brand-background-pressed: #8335c7; 326 + --color-compound-brand-foreground-1: #ad46ff; 327 + --color-compound-brand-foreground-1-hover: #983ee6; 328 + --color-compound-brand-foreground-1-pressed: #8335c7; 329 + --color-compound-brand-stroke: #ad46ff; 330 + --color-compound-brand-stroke-hover: #983ee6; 331 + --color-compound-brand-stroke-pressed: #8335c7; 332 + --color-neutral-background-1: #ffffff; 333 + --color-neutral-background-1-hover: #f5f5f5; 334 + --color-neutral-background-1-pressed: #e0e0e0; 335 + --color-neutral-background-1-selected: #ebebeb; 336 + --color-neutral-background-2: #fafafa; 337 + --color-neutral-background-2-hover: #f0f0f0; 338 + --color-neutral-background-2-pressed: #dbdbdb; 339 + --color-neutral-background-2-selected: #e6e6e6; 340 + --color-neutral-background-3: #f5f5f5; 341 + --color-neutral-background-3-hover: #ebebeb; 342 + --color-neutral-background-3-pressed: #d6d6d6; 343 + --color-neutral-background-3-selected: #e0e0e0; 344 + --color-neutral-background-4: #f0f0f0; 345 + --color-neutral-background-4-hover: #fafafa; 346 + --color-neutral-background-4-pressed: #f5f5f5; 347 + --color-neutral-background-4-selected: #ffffff; 348 + --color-neutral-background-5: #ebebeb; 349 + --color-neutral-background-5-hover: #f5f5f5; 350 + --color-neutral-background-5-pressed: #f0f0f0; 351 + --color-neutral-background-5-selected: #fafafa; 352 + --color-neutral-background-6: #e6e6e6; 353 + --color-neutral-background-alpha: rgba(255, 255, 255, 0.5); 354 + --color-neutral-background-alpha-2: rgba(255, 255, 255, 0.8); 355 + --color-neutral-background-disabled: #f0f0f0; 356 + --color-neutral-background-inverted: #292929; 357 + --color-neutral-background-inverted-hover: #3d3d3d; 358 + --color-neutral-background-inverted-pressed: #1f1f1f; 359 + --color-neutral-background-inverted-selected: #383838; 360 + --color-neutral-background-static: #333333; 361 + --color-neutral-foreground-1: #242424; 362 + --color-neutral-foreground-1-hover: #242424; 363 + --color-neutral-foreground-1-pressed: #242424; 364 + --color-neutral-foreground-1-selected: #242424; 365 + --color-neutral-foreground-1-static: #242424; 366 + --color-neutral-foreground-2: #424242; 367 + --color-neutral-foreground-2-brand-hover: #ad46ff; 368 + --color-neutral-foreground-2-brand-pressed: #983ee6; 369 + --color-neutral-foreground-2-brand-selected: #ad46ff; 370 + --color-neutral-foreground-2-hover: #242424; 371 + --color-neutral-foreground-2-pressed: #242424; 372 + --color-neutral-foreground-2-selected: #242424; 373 + --color-neutral-foreground-3: #616161; 374 + --color-neutral-foreground-3-hover: #424242; 375 + --color-neutral-foreground-3-pressed: #424242; 376 + --color-neutral-foreground-3-selected: #424242; 377 + --color-neutral-foreground-4: #707070; 378 + --color-neutral-foreground-5: #616161; 379 + --color-neutral-foreground-5-hover: #242424; 380 + --color-neutral-foreground-5-pressed: #242424; 381 + --color-neutral-foreground-5-selected: #242424; 382 + --color-neutral-foreground-disabled: #bdbdbd; 383 + --color-neutral-foreground-inverted: #ffffff; 384 + --color-neutral-foreground-inverted-2: #ffffff; 385 + --color-neutral-foreground-inverted-hover: #ffffff; 386 + --color-neutral-foreground-inverted-pressed: #ffffff; 387 + --color-neutral-foreground-inverted-selected: #ffffff; 388 + --color-neutral-foreground-on-brand: #ffffff; 389 + --color-neutral-foreground-static-inverted: #ffffff; 390 + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.12); 391 + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.2); 392 + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.06); 393 + --color-neutral-shadow-key: rgba(0, 0, 0, 0.14); 394 + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.24); 395 + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.07); 396 + --color-neutral-stroke-1: #d1d1d1; 397 + --color-neutral-stroke-1-hover: #c7c7c7; 398 + --color-neutral-stroke-1-pressed: #b3b3b3; 399 + --color-neutral-stroke-1-selected: #bdbdbd; 400 + --color-neutral-stroke-2: #e0e0e0; 401 + --color-neutral-stroke-3: #f0f0f0; 402 + --color-neutral-stroke-accessible: #616161; 403 + --color-neutral-stroke-accessible-hover: #575757; 404 + --color-neutral-stroke-accessible-pressed: #4d4d4d; 405 + --color-neutral-stroke-accessible-selected: #ad46ff; 406 + --color-neutral-stroke-alpha: rgba(0, 0, 0, 0.05); 407 + --color-neutral-stroke-alpha-2: rgba(255, 255, 255, 0.2); 408 + --color-neutral-stroke-disabled: #e0e0e0; 409 + --color-neutral-stroke-inverted-disabled: rgba(255, 255, 255, 0.4); 410 + --color-neutral-stroke-on-brand: #ffffff; 411 + --color-neutral-stroke-on-brand-2: #ffffff; 412 + --color-neutral-stroke-on-brand-2-hover: #ffffff; 413 + --color-neutral-stroke-on-brand-2-pressed: #ffffff; 414 + --color-neutral-stroke-on-brand-2-selected: #ffffff; 415 + --color-neutral-stroke-subtle: #e0e0e0; 416 + --color-scrollbar-overlay: rgba(0, 0, 0, 0.5); 417 + --color-status-danger-background-1: #fdf3f4; 418 + --color-status-danger-background-2: #eeacb2; 419 + --color-status-danger-background-3: #c50f1f; 420 + --color-status-danger-background-3-hover: #b10e1c; 421 + --color-status-danger-background-3-pressed: #960b18; 422 + --color-status-danger-border-1: #eeacb2; 423 + --color-status-danger-border-2: #c50f1f; 424 + --color-status-danger-border-active: #c50f1f; 425 + --color-status-danger-foreground-1: #6e0811; 426 + --color-status-danger-foreground-2: #b10e1c; 427 + --color-status-danger-foreground-3: #c50f1f; 428 + --color-status-danger-foreground-inverted: #dc626d; 429 + --color-status-success-background-1: #f1faf1; 430 + --color-status-success-background-2: #9fd89f; 431 + --color-status-success-background-3: #107c10; 432 + --color-status-success-border-1: #9fd89f; 433 + --color-status-success-border-2: #359b35; 434 + --color-status-success-border-active: #107c10; 435 + --color-status-success-foreground-1: #094509; 436 + --color-status-success-foreground-2: #0e700e; 437 + --color-status-success-foreground-3: #359b35; 438 + --color-status-success-foreground-inverted: #54b054; 439 + --color-status-warning-background-1: #fff9f5; 440 + --color-status-warning-background-2: #fdcfb5; 441 + --color-status-warning-background-3: #f7630c; 442 + --color-status-warning-border-1: #fdcfb5; 443 + --color-status-warning-border-2: #8a3707; 444 + --color-status-warning-border-active: #f7630c; 445 + --color-status-warning-foreground-1: #8a3707; 446 + --color-status-warning-foreground-2: #de590b; 447 + --color-status-warning-foreground-3: #8a3707; 448 + --color-status-warning-foreground-inverted: #faa06b; 449 + --color-stroke-focus-1: #ffffff; 450 + --color-stroke-focus-2: #000000; 451 + --color-subtle-background: transparent; 452 + --color-subtle-background-hover: #f5f5f5; 453 + --color-subtle-background-inverted: transparent; 454 + --color-subtle-background-inverted-hover: rgba(0, 0, 0, 0.1); 455 + --color-subtle-background-inverted-pressed: rgba(0, 0, 0, 0.3); 456 + --color-subtle-background-inverted-selected: rgba(0, 0, 0, 0.2); 457 + --color-subtle-background-light-alpha-hover: rgba(255, 255, 255, 0.7); 458 + --color-subtle-background-light-alpha-pressed: rgba(255, 255, 255, 0.5); 459 + --color-subtle-background-light-alpha-selected: transparent; 460 + --color-subtle-background-pressed: #e0e0e0; 461 + --color-subtle-background-selected: #ebebeb; 462 + --color-transparent-background: transparent; 463 + --color-transparent-background-hover: transparent; 464 + --color-transparent-background-pressed: transparent; 465 + --color-transparent-background-selected: transparent; 466 + --color-transparent-stroke: transparent; 467 + --color-transparent-stroke-disabled: transparent; 468 + --color-transparent-stroke-interactive: transparent; 469 + 470 + --shadow-2: 0 0 2px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.14); 471 + --shadow-4: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); 472 + --shadow-8: 0 0 2px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.14); 473 + --shadow-16: 0 0 2px rgba(0, 0, 0, 0.12), 0 8px 16px rgba(0, 0, 0, 0.14); 474 + --shadow-28: 0 0 8px rgba(0, 0, 0, 0.12), 0 14px 28px rgba(0, 0, 0, 0.14); 475 + --shadow-64: 0 0 8px rgba(0, 0, 0, 0.12), 0 32px 64px rgba(0, 0, 0, 0.14); 476 + } 477 + 478 + @media (prefers-color-scheme: dark) { 479 + --color-background-overlay: rgba(0, 0, 0, 0.5); 480 + --color-brand-background: #983ee6; 481 + --color-brand-background-2: #1c0b2a; 482 + --color-brand-background-2-hover: #53227e; 483 + --color-brand-background-2-pressed: #07030a; 484 + --color-brand-background-3-static: #8335c7; 485 + --color-brand-background-4-static: #53227e; 486 + --color-brand-background-hover: #ad46ff; 487 + --color-brand-background-inverted: #ffffff; 488 + --color-brand-background-inverted-hover: #fcf8ff; 489 + --color-brand-background-inverted-pressed: #e5c5ff; 490 + --color-brand-background-inverted-selected: #f2e2ff; 491 + --color-brand-background-pressed: #53227e; 492 + --color-brand-background-selected: #8335c7; 493 + --color-brand-background-static: #ad46ff; 494 + --color-brand-foreground-1: #ba65ff; 495 + --color-brand-foreground-2: #c47bff; 496 + --color-brand-foreground-2-hover: #d8a8ff; 497 + --color-brand-foreground-2-pressed: #fcf8ff; 498 + --color-brand-foreground-inverted: #ad46ff; 499 + --color-brand-foreground-inverted-hover: #983ee6; 500 + --color-brand-foreground-inverted-pressed: #8335c7; 501 + --color-brand-foreground-link: #ba65ff; 502 + --color-brand-foreground-link-hover: #c47bff; 503 + --color-brand-foreground-link-pressed: #b456ff; 504 + --color-brand-foreground-link-selected: #ba65ff; 505 + --color-brand-foreground-on-light: #ad46ff; 506 + --color-brand-foreground-on-light-hover: #983ee6; 507 + --color-brand-foreground-on-light-pressed: #6f2da8; 508 + --color-brand-foreground-on-light-selected: #8335c7; 509 + --color-brand-stroke-1: #ba65ff; 510 + --color-brand-stroke-2: #6f2da8; 511 + --color-brand-stroke-2-contrast: #6f2da8; 512 + --color-brand-stroke-2-hover: #6f2da8; 513 + --color-brand-stroke-2-pressed: #34154f; 514 + --color-compound-brand-background: #ba65ff; 515 + --color-compound-brand-background-hover: #c47bff; 516 + --color-compound-brand-background-pressed: #b456ff; 517 + --color-compound-brand-foreground-1: #ba65ff; 518 + --color-compound-brand-foreground-1-hover: #c47bff; 519 + --color-compound-brand-foreground-1-pressed: #b456ff; 520 + --color-compound-brand-stroke: #ba65ff; 521 + --color-compound-brand-stroke-hover: #c47bff; 522 + --color-compound-brand-stroke-pressed: #b456ff; 523 + --color-neutral-background-1: #292929; 524 + --color-neutral-background-1-hover: #3d3d3d; 525 + --color-neutral-background-1-pressed: #1f1f1f; 526 + --color-neutral-background-1-selected: #383838; 527 + --color-neutral-background-2: #1f1f1f; 528 + --color-neutral-background-2-hover: #333333; 529 + --color-neutral-background-2-pressed: #141414; 530 + --color-neutral-background-2-selected: #2e2e2e; 531 + --color-neutral-background-3: #141414; 532 + --color-neutral-background-3-hover: #292929; 533 + --color-neutral-background-3-pressed: #0a0a0a; 534 + --color-neutral-background-3-selected: #242424; 535 + --color-neutral-background-4: #0a0a0a; 536 + --color-neutral-background-4-hover: #1f1f1f; 537 + --color-neutral-background-4-pressed: #000000; 538 + --color-neutral-background-4-selected: #1a1a1a; 539 + --color-neutral-background-5: #000000; 540 + --color-neutral-background-5-hover: #141414; 541 + --color-neutral-background-5-pressed: #050505; 542 + --color-neutral-background-5-selected: #0f0f0f; 543 + --color-neutral-background-6: #333333; 544 + --color-neutral-background-alpha: rgba(26, 26, 26, 0.5); 545 + --color-neutral-background-alpha-2: rgba(26, 26, 26, 0.7); 546 + --color-neutral-background-disabled: #141414; 547 + --color-neutral-background-inverted: #ffffff; 548 + --color-neutral-background-inverted-hover: #f5f5f5; 549 + --color-neutral-background-inverted-pressed: #e0e0e0; 550 + --color-neutral-background-inverted-selected: #ebebeb; 551 + --color-neutral-background-static: #3d3d3d; 552 + --color-neutral-foreground-1: #ffffff; 553 + --color-neutral-foreground-1-hover: #ffffff; 554 + --color-neutral-foreground-1-pressed: #ffffff; 555 + --color-neutral-foreground-1-selected: #ffffff; 556 + --color-neutral-foreground-1-static: #242424; 557 + --color-neutral-foreground-2: #d6d6d6; 558 + --color-neutral-foreground-2-brand-hover: #ba65ff; 559 + --color-neutral-foreground-2-brand-pressed: #b456ff; 560 + --color-neutral-foreground-2-brand-selected: #ba65ff; 561 + --color-neutral-foreground-2-hover: #ffffff; 562 + --color-neutral-foreground-2-pressed: #ffffff; 563 + --color-neutral-foreground-2-selected: #ffffff; 564 + --color-neutral-foreground-3: #adadad; 565 + --color-neutral-foreground-3-hover: #d6d6d6; 566 + --color-neutral-foreground-3-pressed: #d6d6d6; 567 + --color-neutral-foreground-3-selected: #d6d6d6; 568 + --color-neutral-foreground-4: #999999; 569 + --color-neutral-foreground-5: #adadad; 570 + --color-neutral-foreground-5-hover: #ffffff; 571 + --color-neutral-foreground-5-pressed: #ffffff; 572 + --color-neutral-foreground-5-selected: #ffffff; 573 + --color-neutral-foreground-disabled: #5c5c5c; 574 + --color-neutral-foreground-inverted: #242424; 575 + --color-neutral-foreground-inverted-2: #242424; 576 + --color-neutral-foreground-inverted-hover: #242424; 577 + --color-neutral-foreground-inverted-pressed: #242424; 578 + --color-neutral-foreground-inverted-selected: #242424; 579 + --color-neutral-foreground-on-brand: #ffffff; 580 + --color-neutral-foreground-static-inverted: #ffffff; 581 + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.24); 582 + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.4); 583 + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.12); 584 + --color-neutral-shadow-key: rgba(0, 0, 0, 0.28); 585 + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.48); 586 + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.14); 587 + --color-neutral-stroke-1: #666666; 588 + --color-neutral-stroke-1-hover: #757575; 589 + --color-neutral-stroke-1-pressed: #6b6b6b; 590 + --color-neutral-stroke-1-selected: #707070; 591 + --color-neutral-stroke-2: #525252; 592 + --color-neutral-stroke-3: #3d3d3d; 593 + --color-neutral-stroke-accessible: #adadad; 594 + --color-neutral-stroke-accessible-hover: #bdbdbd; 595 + --color-neutral-stroke-accessible-pressed: #b3b3b3; 596 + --color-neutral-stroke-accessible-selected: #ba65ff; 597 + --color-neutral-stroke-alpha: rgba(255, 255, 255, 0.1); 598 + --color-neutral-stroke-alpha-2: rgba(255, 255, 255, 0.2); 599 + --color-neutral-stroke-disabled: #424242; 600 + --color-neutral-stroke-inverted-disabled: rgba(255, 255, 255, 0.4); 601 + --color-neutral-stroke-on-brand: #292929; 602 + --color-neutral-stroke-on-brand-2: #ffffff; 603 + --color-neutral-stroke-on-brand-2-hover: #ffffff; 604 + --color-neutral-stroke-on-brand-2-pressed: #ffffff; 605 + --color-neutral-stroke-on-brand-2-selected: #ffffff; 606 + --color-neutral-stroke-subtle: #0a0a0a; 607 + --color-scrollbar-overlay: rgba(255, 255, 255, 0.6); 608 + --color-status-danger-background-1: #3b0509; 609 + --color-status-danger-background-2: #6e0811; 610 + --color-status-danger-background-3: #c50f1f; 611 + --color-status-danger-background-3-hover: #b10e1c; 612 + --color-status-danger-background-3-pressed: #960b18; 613 + --color-status-danger-border-1: #c50f1f; 614 + --color-status-danger-border-2: #dc626d; 615 + --color-status-danger-border-active: #dc626d; 616 + --color-status-danger-foreground-1: #dc626d; 617 + --color-status-danger-foreground-2: #eeacb2; 618 + --color-status-danger-foreground-3: #eeacb2; 619 + --color-status-danger-foreground-inverted: #cc2635; 620 + --color-status-success-background-1: #052505; 621 + --color-status-success-background-2: #094509; 622 + --color-status-success-background-3: #107c10; 623 + --color-status-success-border-1: #107c10; 624 + --color-status-success-border-2: #359b35; 625 + --color-status-success-border-active: #54b054; 626 + --color-status-success-foreground-1: #54b054; 627 + --color-status-success-foreground-2: #9fd89f; 628 + --color-status-success-foreground-3: #359b35; 629 + --color-status-success-foreground-inverted: #0e700e; 630 + --color-status-warning-background-1: #4a1e04; 631 + --color-status-warning-background-2: #8a3707; 632 + --color-status-warning-background-3: #f7630c; 633 + --color-status-warning-border-1: #f7630c; 634 + --color-status-warning-border-2: #f98845; 635 + --color-status-warning-border-active: #faa06b; 636 + --color-status-warning-foreground-1: #faa06b; 637 + --color-status-warning-foreground-2: #fdcfb5; 638 + --color-status-warning-foreground-3: #fdcfb5; 639 + --color-status-warning-foreground-inverted: #8a3707; 640 + --color-stroke-focus-1: #000000; 641 + --color-stroke-focus-2: #ffffff; 642 + --color-subtle-background: transparent; 643 + --color-subtle-background-hover: #383838; 644 + --color-subtle-background-inverted: transparent; 645 + --color-subtle-background-inverted-hover: rgba(0, 0, 0, 0.1); 646 + --color-subtle-background-inverted-pressed: rgba(0, 0, 0, 0.3); 647 + --color-subtle-background-inverted-selected: rgba(0, 0, 0, 0.2); 648 + --color-subtle-background-light-alpha-hover: rgba(255, 255, 255, 0.1); 649 + --color-subtle-background-light-alpha-pressed: rgba(255, 255, 255, 0.05); 650 + --color-subtle-background-light-alpha-selected: transparent; 651 + --color-subtle-background-pressed: #2e2e2e; 652 + --color-subtle-background-selected: #333333; 653 + --color-transparent-background: transparent; 654 + --color-transparent-background-hover: transparent; 655 + --color-transparent-background-pressed: transparent; 656 + --color-transparent-background-selected: transparent; 657 + --color-transparent-stroke: transparent; 658 + --color-transparent-stroke-disabled: transparent; 659 + --color-transparent-stroke-interactive: transparent; 660 + 661 + --shadow-2: 0 0 2px rgba(0, 0, 0, 0.24), 0 1px 2px rgba(0, 0, 0, 0.28); 662 + --shadow-4: 0 0 2px rgba(0, 0, 0, 0.24), 0 2px 4px rgba(0, 0, 0, 0.28); 663 + --shadow-8: 0 0 2px rgba(0, 0, 0, 0.24), 0 4px 8px rgba(0, 0, 0, 0.28); 664 + --shadow-16: 0 0 2px rgba(0, 0, 0, 0.24), 0 8px 16px rgba(0, 0, 0, 0.28); 665 + --shadow-28: 0 0 8px rgba(0, 0, 0, 0.24), 0 14px 28px rgba(0, 0, 0, 0.28); 666 + --shadow-64: 0 0 8px rgba(0, 0, 0, 0.24), 0 32px 64px rgba(0, 0, 0, 0.28); 667 + } 668 + } 669 + 670 + @layer base { 671 + :where(body) { 672 + background: var(--color-neutral-background-2); 673 + color: var(--color-neutral-foreground-1); 674 + color-scheme: light dark; 675 + } 676 + 677 + /* prevent body scroll when modal dialog is open */ 678 + :where(body:has(dialog:modal)) { 679 + overflow: hidden; 680 + } 681 + } 682 + 683 + /* #region anchor positioning */ 684 + 685 + /* anchor-name: set via --anchor CSS variable */ 686 + @utility anchor { 687 + anchor-name: var(--anchor); 688 + } 689 + 690 + /* position-anchor: attach to anchor via --anchor CSS variable */ 691 + @utility anchored { 692 + position-anchor: var(--anchor); 693 + :where(&) { 694 + position: absolute; 695 + } 696 + } 697 + 698 + /* position-area: position relative to anchor */ 699 + @utility anchored-top { 700 + position-area: top; 701 + } 702 + @utility anchored-top-center { 703 + position-area: top center; 704 + } 705 + @utility anchored-top-left { 706 + position-area: top left; 707 + } 708 + @utility anchored-top-right { 709 + position-area: top right; 710 + } 711 + @utility anchored-top-span-left { 712 + position-area: top span-left; 713 + } 714 + @utility anchored-top-span-right { 715 + position-area: top span-right; 716 + } 717 + @utility anchored-bottom { 718 + position-area: bottom; 719 + } 720 + @utility anchored-bottom-center { 721 + position-area: bottom center; 722 + } 723 + @utility anchored-bottom-left { 724 + position-area: bottom left; 725 + } 726 + @utility anchored-bottom-right { 727 + position-area: bottom right; 728 + } 729 + @utility anchored-bottom-span-left { 730 + position-area: bottom span-left; 731 + } 732 + @utility anchored-bottom-span-right { 733 + position-area: bottom span-right; 734 + } 735 + @utility anchored-left { 736 + position-area: left; 737 + } 738 + @utility anchored-left-center { 739 + position-area: left center; 740 + } 741 + @utility anchored-left-span-top { 742 + position-area: left span-top; 743 + } 744 + @utility anchored-left-span-bottom { 745 + position-area: left span-bottom; 746 + } 747 + @utility anchored-right { 748 + position-area: right; 749 + } 750 + @utility anchored-right-center { 751 + position-area: right center; 752 + } 753 + @utility anchored-right-span-top { 754 + position-area: right span-top; 755 + } 756 + @utility anchored-right-span-bottom { 757 + position-area: right span-bottom; 758 + } 759 + 760 + /* position-try-fallbacks: fallback positions */ 761 + @utility try-none { 762 + position-try-fallbacks: none; 763 + } 764 + @utility try-flip-x { 765 + position-try-fallbacks: flip-inline; 766 + } 767 + @utility try-flip-y { 768 + position-try-fallbacks: flip-block; 769 + } 770 + @utility try-flip-all { 771 + position-try-fallbacks: 772 + flip-block, 773 + flip-inline, 774 + flip-block flip-inline; 775 + } 776 + @utility try-flip-start { 777 + position-try-fallbacks: flip-start; 778 + } 779 + 780 + /* position-try-order: fallback ordering */ 781 + @utility try-order-normal { 782 + position-try-order: normal; 783 + } 784 + @utility try-order-width { 785 + position-try-order: most-width; 786 + } 787 + @utility try-order-height { 788 + position-try-order: most-height; 789 + } 790 + 791 + /* position-visibility: conditional visibility */ 792 + @utility anchored-visible-always { 793 + position-visibility: always; 794 + } 795 + @utility anchored-visible-anchor { 796 + position-visibility: anchors-visible; 797 + } 798 + @utility anchored-visible-no-overflow { 799 + position-visibility: no-overflow; 800 + } 801 + 802 + /* #endregion */ 803 + 804 + /* #region dialog animations */ 805 + 806 + /* 807 + * dialog entry/exit animations using @starting-style 808 + * inspired by FluentUI's Dialog motion: scale + fade with decelerate/accelerate easing 809 + */ 810 + 811 + @utility dialog-animate { 812 + /* transition properties for both dialog surface and backdrop */ 813 + transition-property: opacity, scale, overlay, display; 814 + transition-duration: var(--duration-faster); 815 + transition-timing-function: var(--ease-decelerate-mid); 816 + transition-behavior: allow-discrete; 817 + 818 + /* final open state */ 819 + &[open] { 820 + opacity: 1; 821 + scale: 1; 822 + } 823 + 824 + /* exit state (dialog closing) */ 825 + &:not([open]) { 826 + opacity: 0; 827 + scale: 0.95; 828 + transition-timing-function: var(--ease-accelerate-min); 829 + } 830 + 831 + /* entry starting state */ 832 + @starting-style { 833 + &[open] { 834 + opacity: 0; 835 + scale: 0.95; 836 + } 837 + } 838 + } 839 + 840 + /* backdrop animation (fade only, no scale) */ 841 + @utility dialog-backdrop-animate { 842 + &::backdrop { 843 + transition-property: opacity, overlay, display; 844 + transition-duration: var(--duration-faster); 845 + transition-timing-function: var(--ease-decelerate-mid); 846 + transition-behavior: allow-discrete; 847 + opacity: 1; 848 + } 849 + 850 + &:not([open])::backdrop { 851 + opacity: 0; 852 + transition-timing-function: var(--ease-accelerate-min); 853 + } 854 + 855 + @starting-style { 856 + &[open]::backdrop { 857 + opacity: 0; 858 + } 859 + } 860 + } 861 + 862 + /* #endregion */ 863 + 864 + /* #region popover animations */ 865 + 866 + /* 867 + * popover entry/exit animations using @starting-style 868 + * inspired by FluentUI's Menu motion: slide + fade based on placement 869 + */ 870 + 871 + @utility popover-animate { 872 + --_slide-x: 0; 873 + --_slide-y: 8px; 874 + 875 + transition-property: opacity, translate, overlay, display; 876 + transition-duration: var(--duration-faster); 877 + transition-timing-function: var(--ease-decelerate-mid); 878 + transition-behavior: allow-discrete; 879 + 880 + /* final open state */ 881 + &:popover-open { 882 + opacity: 1; 883 + translate: 0 0; 884 + } 885 + 886 + /* exit state */ 887 + &:not(:popover-open) { 888 + opacity: 0; 889 + translate: var(--_slide-x) var(--_slide-y); 890 + transition-timing-function: var(--ease-accelerate-min); 891 + } 892 + 893 + /* entry starting state */ 894 + @starting-style { 895 + &:popover-open { 896 + opacity: 0; 897 + translate: var(--_slide-x) var(--_slide-y); 898 + } 899 + } 900 + } 901 + 902 + /* slide direction variants based on anchor position */ 903 + @utility popover-slide-down { 904 + --_slide-x: 0; 905 + --_slide-y: -8px; 906 + } 907 + @utility popover-slide-up { 908 + --_slide-x: 0; 909 + --_slide-y: 8px; 910 + } 911 + @utility popover-slide-left { 912 + --_slide-x: 8px; 913 + --_slide-y: 0; 914 + } 915 + @utility popover-slide-right { 916 + --_slide-x: -8px; 917 + --_slide-y: 0; 918 + } 919 + 920 + /* #endregion */
+9
src/index.tsx
··· 1 + /* @refresh reload */ 2 + import { render } from 'solid-js/web'; 3 + 4 + import './index.css'; 5 + import App from './app.tsx'; 6 + 7 + const root = document.getElementById('root'); 8 + 9 + render(() => <App />, root!);
+3
src/lib/classes.ts
··· 1 + export const tw = (strings: TemplateStringsArray, ...values: string[]) => { 2 + return String.raw({ raw: strings }, ...values); 3 + };
+95
src/lib/emitter.ts
··· 1 + /** 2 + * callback type for event listeners. 3 + */ 4 + type Callback<T extends unknown[]> = (...args: T) => void; 5 + 6 + /** 7 + * a simple typed event emitter. 8 + */ 9 + export interface EventEmitter<T extends unknown[]> { 10 + /** 11 + * registers a listener for this event. 12 + * @param callback the function to call when the event fires 13 + * @returns a cleanup function that removes the listener 14 + */ 15 + listen: (callback: Callback<T>) => () => void; 16 + /** 17 + * emits the event, calling all registered listeners synchronously. 18 + * @param args the arguments to pass to listeners 19 + */ 20 + emit: (...args: T) => void; 21 + } 22 + 23 + /** 24 + * creates a typed event emitter. 25 + * uses a Set internally for O(1) add/remove operations. 26 + * 27 + * @returns an event emitter with listen and emit methods 28 + * @example 29 + * ```ts 30 + * const onProgress = createEventEmitter<[current: number, total: number]>(); 31 + * 32 + * // in a Solid component: 33 + * onCleanup(onProgress.listen((current, total) => { 34 + * console.log(`${current}/${total}`); 35 + * })); 36 + * 37 + * // elsewhere: 38 + * onProgress.emit(5, 10); 39 + * ``` 40 + */ 41 + export function createEventEmitter<T extends unknown[]>(): EventEmitter<T> { 42 + let listener: Callback<T> | Callback<T>[] | undefined; 43 + 44 + return { 45 + listen(callback) { 46 + let closed = false; 47 + 48 + if (listener === undefined) { 49 + listener = callback; 50 + } else if (typeof listener === 'function') { 51 + listener = [listener, callback]; 52 + } else { 53 + listener = listener.concat(callback); 54 + } 55 + 56 + return () => { 57 + if (closed) { 58 + return; 59 + } 60 + 61 + closed = true; 62 + 63 + if (listener === undefined) { 64 + return; 65 + } 66 + 67 + if (listener === callback) { 68 + listener = undefined; 69 + } else if (typeof listener !== 'function') { 70 + const index = listener.indexOf(callback); 71 + if (index !== -1) { 72 + if (listener.length === 2) { 73 + // ^ flips the bit, it's either 0 or 1 here. 74 + listener = listener[index ^ 1]; 75 + } else { 76 + listener = listener.toSpliced(index, 1); 77 + } 78 + } 79 + } 80 + }; 81 + }, 82 + emit(...args) { 83 + if (listener === undefined) { 84 + return false; 85 + } 86 + if (typeof listener === 'function') { 87 + listener.apply(this, args); 88 + } else { 89 + for (let idx = 0, len = listener.length; idx < len; idx++) { 90 + listener[idx].apply(this, args); 91 + } 92 + } 93 + }, 94 + }; 95 + }
+28
src/lib/format.ts
··· 1 + /** 2 + * formats bytes into a human-readable string. 3 + * 4 + * @param bytes the number of bytes 5 + * @param decimals number of decimal places (default: 1) 6 + * @returns formatted string like "1.2 kB" or "3.4 MB" 7 + */ 8 + export function formatBytes(bytes: number, decimals: number = 1): string { 9 + if (bytes === 0) { 10 + return '0 B'; 11 + } 12 + 13 + const k = 1000; 14 + const sizes = ['B', 'kB', 'MB', 'GB']; 15 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 16 + 17 + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`; 18 + } 19 + 20 + /** 21 + * formats a number with thousand separators. 22 + * 23 + * @param n the number to format 24 + * @returns formatted string like "1,234" 25 + */ 26 + export function formatNumber(n: number): string { 27 + return n.toLocaleString('en-US'); 28 + }
+230
src/lib/lru.ts
··· 1 + interface LRUNode<K, V> { 2 + key: K; 3 + value: V; 4 + prev: LRUNode<K, V> | null; 5 + next: LRUNode<K, V> | null; 6 + } 7 + 8 + /** 9 + * a least recently used (LRU) cache with fixed capacity 10 + * evicts the least recently used items when capacity is exceeded 11 + */ 12 + export class LRUCache<K, V> { 13 + readonly #size: number; 14 + #count = 0; 15 + 16 + #map = new Map<K, LRUNode<K, V>>(); 17 + #head: LRUNode<K, V> | null = null; 18 + #tail: LRUNode<K, V> | null = null; 19 + 20 + /** 21 + * creates a new LRU cache with the specified capacity 22 + * @param size the maximum number of items the cache can hold 23 + */ 24 + constructor(size: number) { 25 + this.#size = size; 26 + } 27 + 28 + /** the maximum capacity of the cache */ 29 + get size(): number { 30 + return this.#size; 31 + } 32 + 33 + /** 34 + * gets a value without affecting its position in the cache 35 + * @param key the key to look up 36 + * @returns the value associated with the key, or undefined if not found 37 + */ 38 + peek(key: K): V | undefined { 39 + const node = this.#map.get(key); 40 + if (node === undefined) { 41 + return undefined; 42 + } 43 + 44 + return node.value; 45 + } 46 + 47 + /** 48 + * gets a value and marks it as most recently used 49 + * @param key the key to look up 50 + * @returns the value associated with the key, or undefined if not found 51 + */ 52 + get(key: K): V | undefined { 53 + const node = this.#map.get(key); 54 + if (node === undefined) { 55 + return undefined; 56 + } 57 + 58 + this.#moveToFront(node); 59 + return node.value; 60 + } 61 + 62 + /** 63 + * stores a value for the given key, marking it as most recently used 64 + * evicts the least recently used item if the cache is at capacity 65 + * @param key the key to store 66 + * @param value the value to associate with the key 67 + */ 68 + put(key: K, value: V): void { 69 + { 70 + const existing = this.#map.get(key); 71 + 72 + if (existing !== undefined) { 73 + existing.value = value; 74 + this.#moveToFront(existing); 75 + return; 76 + } 77 + } 78 + 79 + { 80 + const node: LRUNode<K, V> = { key, value, prev: null, next: null }; 81 + this.#map.set(key, node); 82 + this.#addToFront(node); 83 + 84 + this.#count++; 85 + } 86 + 87 + this.#evict(); 88 + } 89 + 90 + /** 91 + * removes a key from the cache 92 + * @param key the key to remove 93 + * @returns true if the key was found and removed, false otherwise 94 + */ 95 + delete(key: K): boolean { 96 + const node = this.#map.get(key); 97 + if (node === undefined) { 98 + return false; 99 + } 100 + 101 + this.#map.delete(key); 102 + this.#removeNode(node); 103 + this.#count--; 104 + return true; 105 + } 106 + 107 + /** 108 + * removes all items from the cache 109 + */ 110 + clear(): void { 111 + this.#map.clear(); 112 + this.#head = null; 113 + this.#tail = null; 114 + this.#count = 0; 115 + } 116 + 117 + /** 118 + * checks if a key exists in the cache 119 + * @param key the key to check 120 + * @returns true if the key exists, false otherwise 121 + */ 122 + has(key: K): boolean { 123 + return this.#map.has(key); 124 + } 125 + 126 + /** 127 + * iterates over the keys in LRU order (most to least recently used) 128 + * @returns iterator of keys 129 + */ 130 + *keys(): IterableIterator<K> { 131 + let current = this.#head; 132 + while (current !== null) { 133 + yield current.key; 134 + current = current.next; 135 + } 136 + } 137 + 138 + /** 139 + * iterates over the values in LRU order (most to least recently used) 140 + * @returns iterator of values 141 + */ 142 + *values(): IterableIterator<V> { 143 + let current = this.#head; 144 + while (current !== null) { 145 + yield current.value; 146 + current = current.next; 147 + } 148 + } 149 + 150 + /** 151 + * iterates over the key-value pairs in LRU order (most to least recently used) 152 + * @returns iterator of [key, value] tuples 153 + */ 154 + *entries(): IterableIterator<[K, V]> { 155 + let current = this.#head; 156 + while (current !== null) { 157 + yield [current.key, current.value]; 158 + current = current.next; 159 + } 160 + } 161 + 162 + #moveToFront(node: LRUNode<K, V>): void { 163 + if (this.#head === node) { 164 + return; 165 + } 166 + 167 + if (node.prev !== null) { 168 + node.prev.next = node.next; 169 + } 170 + 171 + if (node.next !== null) { 172 + node.next.prev = node.prev; 173 + } else { 174 + this.#tail = node.prev; 175 + } 176 + 177 + node.prev = null; 178 + node.next = this.#head; 179 + 180 + // Safe because this method is only called when head exists 181 + this.#head!.prev = node; 182 + this.#head = node; 183 + } 184 + 185 + #addToFront(node: LRUNode<K, V>): void { 186 + node.next = this.#head; 187 + node.prev = null; 188 + 189 + if (this.#head !== null) { 190 + this.#head.prev = node; 191 + } else { 192 + this.#tail = node; 193 + } 194 + 195 + this.#head = node; 196 + } 197 + 198 + #removeNode(node: LRUNode<K, V>): void { 199 + if (node.prev !== null) { 200 + node.prev.next = node.next; 201 + } else { 202 + this.#head = node.next; 203 + } 204 + 205 + if (node.next !== null) { 206 + node.next.prev = node.prev; 207 + } else { 208 + this.#tail = node.prev; 209 + } 210 + } 211 + 212 + #evict(): void { 213 + const excess = this.#count - this.#size; 214 + if (excess <= 0) { 215 + return; 216 + } 217 + 218 + let current: LRUNode<K, V> = this.#tail!; 219 + 220 + for (let i = 0; i < excess; i++) { 221 + this.#map.delete(current.key); 222 + current = current.prev!; 223 + } 224 + 225 + current.next = null; 226 + this.#tail = current; 227 + 228 + this.#count -= excess; 229 + } 230 + }
+10
src/lib/modality.ts
··· 1 + import { createSignal } from 'solid-js'; 2 + 3 + export type Modality = 'pointer' | 'keyboard'; 4 + 5 + const [modality, setModality] = createSignal<Modality>('pointer'); 6 + 7 + document.addEventListener('keydown', () => setModality('keyboard')); 8 + document.addEventListener('pointermove', () => setModality('pointer')); 9 + 10 + export { modality };
+42
src/lib/package-name.ts
··· 1 + /** 2 + * regex for validating package specifiers. 3 + * matches: `[npm:|jsr:][@scope/]name[@version]` 4 + */ 5 + export const PACKAGE_SPECIFIER_RE = 6 + /^(?:(jsr|npm):)?((?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*)(?:@(.+))?$/; 7 + 8 + export type Registry = 'npm' | 'jsr'; 9 + 10 + export interface ParsedPackageSpecifier { 11 + registry: Registry; 12 + name: string; 13 + range: string; 14 + } 15 + 16 + /** 17 + * parses a package specifier into registry, name, and version range. 18 + * @param input the string to parse 19 + * @returns parsed result, or null if invalid 20 + */ 21 + export const parsePackageSpecifier = (input: string): ParsedPackageSpecifier | null => { 22 + const match = PACKAGE_SPECIFIER_RE.exec(input); 23 + if (!match) { 24 + return null; 25 + } 26 + return { 27 + registry: (match[1] as Registry) ?? 'npm', 28 + name: match[2], 29 + range: match[3] ?? 'latest', 30 + }; 31 + }; 32 + 33 + /** 34 + * formats a package specifier into a string. 35 + * always includes registry prefix, omits version if it's 'latest'. 36 + * @param spec the parsed specifier 37 + * @returns formatted specifier string 38 + */ 39 + export const formatPackageSpecifier = (spec: ParsedPackageSpecifier): string => { 40 + const version = spec.range !== 'latest' ? `@${spec.range}` : ''; 41 + return `${spec.registry}:${spec.name}${version}`; 42 + };
+194
src/lib/query.ts
··· 1 + import { createComputed, createMemo, createSignal, untrack } from 'solid-js'; 2 + 3 + // #region types 4 + 5 + export type QueryState = 'unresolved' | 'pending' | 'ready' | 'refreshing' | 'errored'; 6 + 7 + export interface QueryResource<T> { 8 + (): T | undefined; 9 + state: QueryState; 10 + loading: boolean; 11 + error: Error | undefined; 12 + } 13 + 14 + export interface QueryActions<T, R = unknown> { 15 + mutate: Setter<T | undefined>; 16 + refetch: (info?: R) => Promise<T | undefined> | T | undefined; 17 + } 18 + 19 + export type QueryReturn<T, R = unknown> = [QueryResource<T>, QueryActions<T, R>]; 20 + 21 + export type QuerySource<S> = S | false | null | undefined | (() => S | false | null | undefined); 22 + export type QueryFetcher<S, T, R> = (source: S, info: ResourceFetcherInfo<T, R>) => T | Promise<T>; 23 + 24 + export type ResourceFetcherInfo<T, R = unknown> = { value: T | undefined; refetching: R | boolean }; 25 + 26 + export interface QueryOptions<T> { 27 + initialValue?: T; 28 + /** keep previous data while loading new data (default: true) */ 29 + keepPreviousData?: boolean; 30 + } 31 + 32 + type Setter<T> = (v: T | ((prev: T) => T)) => T; 33 + 34 + // #endregion 35 + 36 + // #region helpers 37 + 38 + function isPromise<T>(v: unknown): v is Promise<T> { 39 + return v !== null && typeof v === 'object' && 'then' in v; 40 + } 41 + 42 + function castError(err: unknown): Error { 43 + if (err instanceof Error) { 44 + return err; 45 + } 46 + return new Error(typeof err === 'string' ? err : 'unknown error', { cause: err }); 47 + } 48 + 49 + // #endregion 50 + 51 + // #region createQuery 52 + 53 + /** 54 + * creates a reactive query that fetches data based on a source signal. 55 + * similar to createResource but without Suspense integration (no throwing). 56 + * 57 + * @param source the source signal or value (false/null/undefined to skip fetching) 58 + * @param fetcher the async function to fetch data 59 + * @param options query options 60 + * @returns tuple of [resource, actions] 61 + */ 62 + export function createQuery<T, S = true, R = unknown>( 63 + source: QuerySource<S>, 64 + fetcher: QueryFetcher<S, T, R>, 65 + options?: QueryOptions<T>, 66 + ): QueryReturn<T, R>; 67 + export function createQuery<T, R = unknown>( 68 + fetcher: QueryFetcher<true, T, R>, 69 + options?: QueryOptions<T>, 70 + ): QueryReturn<T, R>; 71 + export function createQuery<T, S, R>( 72 + pSource: QuerySource<S> | QueryFetcher<S, T, R>, 73 + pFetcher?: QueryFetcher<S, T, R> | QueryOptions<T>, 74 + pOptions?: QueryOptions<T>, 75 + ): QueryReturn<T, R> { 76 + let source: QuerySource<S>; 77 + let fetcher: QueryFetcher<S, T, R>; 78 + let options: QueryOptions<T>; 79 + 80 + // normalize arguments 81 + if (typeof pFetcher === 'function') { 82 + source = pSource as QuerySource<S>; 83 + fetcher = pFetcher; 84 + options = pOptions || {}; 85 + } else { 86 + source = true as QuerySource<S>; 87 + fetcher = pSource as QueryFetcher<S, T, R>; 88 + options = (pFetcher || {}) as QueryOptions<T>; 89 + } 90 + 91 + let pr: Promise<T> | null = null; 92 + let scheduled = false; 93 + let resolved = 'initialValue' in options; 94 + 95 + const dynamic = typeof source === 'function' && createMemo(source as () => S | false | null | undefined); 96 + const [value, setValue] = createSignal<T | undefined>(options.initialValue); 97 + const [error, setError] = createSignal<Error | undefined>(undefined); 98 + const [state, setState] = createSignal<QueryState>(resolved ? 'ready' : 'unresolved'); 99 + 100 + function loadEnd(p: Promise<T> | null, v: T | undefined, err?: Error) { 101 + if (pr === p) { 102 + pr = null; 103 + if (err === undefined) { 104 + setValue(() => v); 105 + resolved = true; 106 + } 107 + setState(err !== undefined ? 'errored' : resolved ? 'ready' : 'unresolved'); 108 + setError(err); 109 + } 110 + return v; 111 + } 112 + 113 + function read(): T | undefined { 114 + // no throwing - just return the value (or undefined if loading/errored) 115 + return value(); 116 + } 117 + 118 + function load(refetching: R | boolean = true): Promise<T | undefined> | T | undefined { 119 + if (refetching !== false && scheduled) { 120 + return; 121 + } 122 + scheduled = false; 123 + 124 + const lookup = dynamic ? dynamic() : (source as S); 125 + 126 + if (lookup == null || lookup === false) { 127 + if (options.keepPreviousData === false) { 128 + resolved = false; 129 + loadEnd(pr, undefined); 130 + } else { 131 + loadEnd(pr, untrack(value)); 132 + } 133 + return; 134 + } 135 + 136 + let fetchError: unknown; 137 + const p = untrack(() => { 138 + try { 139 + return fetcher(lookup, { 140 + value: value(), 141 + refetching, 142 + }); 143 + } catch (e) { 144 + fetchError = e; 145 + } 146 + }); 147 + 148 + if (fetchError !== undefined) { 149 + loadEnd(pr, undefined, castError(fetchError)); 150 + return; 151 + } else if (!isPromise<T>(p)) { 152 + loadEnd(pr, p); 153 + return p; 154 + } 155 + 156 + pr = p; 157 + scheduled = true; 158 + queueMicrotask(() => (scheduled = false)); 159 + 160 + if (options.keepPreviousData === false) { 161 + setValue(undefined); 162 + resolved = false; 163 + } 164 + 165 + setState(resolved ? 'refreshing' : 'pending'); 166 + 167 + return p.then( 168 + (v) => loadEnd(p, v), 169 + (e) => loadEnd(p, undefined, castError(e)), 170 + ); 171 + } 172 + 173 + // attach properties to read function 174 + Object.defineProperties(read, { 175 + state: { get: () => state() }, 176 + error: { get: () => error() }, 177 + loading: { 178 + get() { 179 + const s = state(); 180 + return s === 'pending' || s === 'refreshing'; 181 + }, 182 + }, 183 + }); 184 + 185 + if (dynamic) { 186 + createComputed(() => load(false)); 187 + } else { 188 + load(false); 189 + } 190 + 191 + return [read as QueryResource<T>, { refetch: load, mutate: setValue }]; 192 + } 193 + 194 + // #endregion
+71
src/lib/signals.ts
··· 1 + import { createEffect, createMemo, createSignal, onCleanup, type Accessor, type Signal } from 'solid-js'; 2 + 3 + /** 4 + * creates a signal that derives its initial value from an accessor but can be overwritten. 5 + * when the source accessor changes, the signal resets to the new derived value. 6 + * 7 + * @param accessor the source accessor to derive from 8 + * @returns a signal tuple [getter, setter] 9 + */ 10 + export function createDerivedSignal<T>(accessor: Accessor<T>): Signal<T> { 11 + const computable = createMemo(() => createSignal(accessor())); 12 + 13 + // @ts-expect-error: setter type mismatch is fine 14 + return [() => computable()[0](), (next) => computable()[1](next)] as Signal<T>; 15 + } 16 + 17 + /** 18 + * creates an abortable signal pair for cancelling async operations. 19 + * calling create() aborts any previous signal and returns a fresh one. 20 + * automatically aborts on component cleanup. 21 + * 22 + * @returns tuple of [create, abort] functions 23 + */ 24 + export const makeAbortable = (): [create: () => AbortSignal, abort: () => void] => { 25 + let controller: AbortController | undefined; 26 + 27 + const abort = (): void => { 28 + controller?.abort(); 29 + }; 30 + const create = (): AbortSignal => { 31 + abort(); 32 + controller = new AbortController(); 33 + return controller.signal; 34 + }; 35 + 36 + onCleanup(abort); 37 + 38 + return [create, abort]; 39 + }; 40 + 41 + /** 42 + * creates a throttled accessor that emits the source value at most once per interval. 43 + * uses trailing edge only: waits for the interval to elapse, then emits the latest value. 44 + * 45 + * @param source the source accessor to throttle 46 + * @param ms throttle interval in milliseconds 47 + * @returns throttled accessor 48 + */ 49 + export function createTrailingThrottle<T>(source: Accessor<T>, ms: number): Accessor<T> { 50 + let lastSource = source(); 51 + let timeout: number | undefined; 52 + 53 + const [throttled, setThrottled] = createSignal(lastSource); 54 + 55 + createEffect((initialMount: boolean) => { 56 + lastSource = source(); 57 + 58 + if (initialMount || timeout !== undefined) { 59 + return false; 60 + } 61 + 62 + timeout = setTimeout(() => { 63 + timeout = undefined; 64 + return setThrottled(() => lastSource); 65 + }, ms); 66 + 67 + return false; 68 + }, true); 69 + 70 + return throttled; 71 + }
+3
src/lib/strings.ts
··· 1 + export function normalizeWhitespace(input: string): string { 2 + return input.replace(/\s+/g, ' ').trim(); 3 + }
+129
src/lib/use-search-params.ts
··· 1 + import { dequal } from 'dequal'; 2 + import { createSignal, onCleanup, type Accessor } from 'solid-js'; 3 + import * as v from 'valibot'; 4 + 5 + /** schema definition for search params - each schema must accept string input */ 6 + type SearchParamsDefinition = Record<string, v.GenericSchema<string | string[] | undefined, unknown>>; 7 + 8 + export interface UseSearchParamsOptions { 9 + /** use replaceState instead of pushState when updating URL */ 10 + replace?: boolean; 11 + } 12 + 13 + /** infers output type with all fields optional */ 14 + type InferSearchParamsOutput<T extends SearchParamsDefinition> = { 15 + [K in keyof T]: v.InferOutput<T[K]> | undefined; 16 + }; 17 + 18 + /** 19 + * creates a reactive signal for validated URL search parameters. 20 + * all parameters are optional. array schemas automatically use getAll(). 21 + * 22 + * @param definition record of valibot schemas (each should accept string input) 23 + * @param options configuration options 24 + * @returns a tuple of [params accessor, setParams function] 25 + * 26 + * @example 27 + * ```ts 28 + * const [params, setParams] = useSearchParams({ 29 + * q: v.string(), 30 + * page: v.pipe(v.string(), v.transform(Number)), 31 + * tags: v.array(v.string()), 32 + * }); 33 + * 34 + * params().q // string | undefined 35 + * params().page // number | undefined 36 + * params().tags // string[] | undefined 37 + * ``` 38 + */ 39 + export const useSearchParams = <T extends SearchParamsDefinition>( 40 + definition: T, 41 + options?: UseSearchParamsOptions, 42 + ): [Accessor<InferSearchParamsOutput<T>>, (params: Partial<InferSearchParamsOutput<T>>) => void] => { 43 + const getValidatedParams = () => { 44 + const searchParams = new URLSearchParams(window.location.search); 45 + const result: Record<string, unknown> = {}; 46 + 47 + for (const key in definition) { 48 + const schema = definition[key]; 49 + 50 + let raw: string | string[] | undefined; 51 + if (schema.type === 'array') { 52 + const values = searchParams.getAll(key); 53 + raw = values.length > 0 ? values : undefined; 54 + } else { 55 + raw = searchParams.get(key) ?? undefined; 56 + } 57 + 58 + if (raw === undefined) { 59 + result[key] = undefined; 60 + } else { 61 + const parsed = v.safeParse(schema, raw); 62 + result[key] = parsed.success ? parsed.output : undefined; 63 + } 64 + } 65 + 66 + return result as InferSearchParamsOutput<T>; 67 + }; 68 + 69 + const [params, setParamsInternal] = createSignal(getValidatedParams()); 70 + 71 + const handlePopState = () => { 72 + setParamsInternal(() => getValidatedParams()); 73 + }; 74 + 75 + window.addEventListener('popstate', handlePopState); 76 + onCleanup(() => window.removeEventListener('popstate', handlePopState)); 77 + 78 + const setParams = (newParams: Partial<InferSearchParamsOutput<T>>) => { 79 + const current = params(); 80 + const merged = { ...current, ...newParams }; 81 + 82 + const result: Record<string, unknown> = {}; 83 + const searchParams = new URLSearchParams(window.location.search); 84 + 85 + for (const key in definition) { 86 + searchParams.delete(key); 87 + 88 + const value = merged[key]; 89 + if (value === undefined) { 90 + result[key] = undefined; 91 + continue; 92 + } 93 + 94 + const parsed = v.safeParse(definition[key], value); 95 + if (!parsed.success) { 96 + result[key] = undefined; 97 + continue; 98 + } 99 + 100 + result[key] = parsed.output; 101 + if (Array.isArray(parsed.output)) { 102 + for (const item of parsed.output) { 103 + searchParams.append(key, String(item)); 104 + } 105 + } else { 106 + searchParams.set(key, String(parsed.output)); 107 + } 108 + } 109 + 110 + const validated = result as InferSearchParamsOutput<T>; 111 + 112 + if (dequal(current, validated)) { 113 + return; 114 + } 115 + 116 + const url = new URL(window.location.href); 117 + url.search = searchParams.toString(); 118 + 119 + if (options?.replace) { 120 + history.replaceState(null, '', url); 121 + } else { 122 + history.pushState(null, '', url); 123 + } 124 + 125 + setParamsInternal(() => validated); 126 + }; 127 + 128 + return [params, setParams]; 129 + };
+291
src/npm/bundler.ts
··· 1 + import { getUtf8Length } from '@atcute/uint8array'; 2 + import { rolldown } from '@rolldown/browser'; 3 + import { memfs } from '@rolldown/browser/experimental'; 4 + 5 + import { BundleError } from './errors'; 6 + import { progress } from './events'; 7 + 8 + const { volume } = memfs!; 9 + 10 + // #region types 11 + 12 + /** 13 + * options for bundling. 14 + */ 15 + export interface BundleOptions { 16 + /** additional rolldown options */ 17 + rolldown?: { 18 + /** external packages to exclude from bundle */ 19 + external?: string[]; 20 + /** whether to minify */ 21 + minify?: boolean; 22 + }; 23 + } 24 + 25 + /** 26 + * a bundled chunk. 27 + */ 28 + export interface BundleChunk { 29 + /** chunk filename */ 30 + fileName: string; 31 + /** the bundled code */ 32 + code: string; 33 + /** raw size in bytes */ 34 + size: number; 35 + /** gzipped size in bytes */ 36 + gzipSize: number; 37 + /** brotli size in bytes, if supported */ 38 + brotliSize?: number; 39 + /** whether this is the entry chunk */ 40 + isEntry: boolean; 41 + /** exported names from this chunk */ 42 + exports: string[]; 43 + } 44 + 45 + /** 46 + * result of bundling a package. 47 + */ 48 + export interface BundleResult { 49 + /** all output chunks */ 50 + chunks: BundleChunk[]; 51 + /** total raw size in bytes (all chunks) */ 52 + size: number; 53 + /** total gzipped size in bytes (all chunks) */ 54 + gzipSize: number; 55 + /** total brotli size in bytes (all chunks), if supported */ 56 + brotliSize?: number; 57 + /** exported names from the entry chunk */ 58 + exports: string[]; 59 + } 60 + 61 + // #endregion 62 + 63 + // #region helpers 64 + 65 + const VIRTUAL_ENTRY_ID = '\0virtual:entry'; 66 + 67 + /** 68 + * checks if a file likely has a default export. 69 + * looks for common patterns in ESM and CJS. 70 + */ 71 + function hasDefaultExport(source: string): boolean { 72 + // ESM patterns 73 + if (/\bexport\s+default\b/.test(source)) { 74 + return true; 75 + } 76 + if (/\bexport\s*\{\s*[^}]*\bdefault\b/.test(source)) { 77 + return true; 78 + } 79 + // CJS patterns (bundlers typically convert these to default exports) 80 + if (/\bmodule\.exports\s*=/.test(source)) { 81 + return true; 82 + } 83 + if (/\bexports\.default\s*=/.test(source)) { 84 + return true; 85 + } 86 + return false; 87 + } 88 + 89 + /** 90 + * creates a virtual entry point that imports and re-exports from a specific subpath. 91 + * 92 + * @param packageName the package name 93 + * @param subpath the export subpath (e.g., ".", "./utils") 94 + * @param selectedExports list of specific exports to include, or null for all 95 + * @param includeDefault whether to include default export (only used when selectedExports is null) 96 + * @returns the entry point code 97 + */ 98 + function createVirtualEntry( 99 + packageName: string, 100 + subpath: string, 101 + selectedExports: string[] | null, 102 + includeDefault: boolean, 103 + ): string { 104 + const importPath = subpath === '.' ? packageName : `${packageName}${subpath.slice(1)}`; 105 + 106 + if (selectedExports === null) { 107 + // re-export everything 108 + let code = `export * from '${importPath}';\n`; 109 + if (includeDefault) { 110 + code += `export { default } from '${importPath}';\n`; 111 + } 112 + return code; 113 + } 114 + 115 + // specific exports selected (empty array = export nothing) 116 + // quote names to handle non-identifier exports 117 + const quoted = selectedExports.map((e) => JSON.stringify(e)); 118 + return `export { ${quoted.join(', ')} } from '${importPath}';\n`; 119 + } 120 + 121 + /** 122 + * get compressed size using a compression stream. 123 + */ 124 + async function getCompressedSize(code: string, format: CompressionFormat): Promise<number> { 125 + const stream = new Blob([code]).stream(); 126 + const compressed = stream.pipeThrough(new CompressionStream(format)); 127 + const reader = compressed.getReader(); 128 + 129 + let size = 0; 130 + while (true) { 131 + const { done, value } = await reader.read(); 132 + if (done) { 133 + break; 134 + } 135 + 136 + size += value.byteLength; 137 + } 138 + 139 + return size; 140 + } 141 + 142 + /** 143 + * get gzip size using compression stream. 144 + */ 145 + async function getGzipSize(code: string): Promise<number> { 146 + return getCompressedSize(code, 'gzip'); 147 + } 148 + 149 + /** 150 + * whether brotli compression is supported. 151 + * - `undefined`: not yet checked 152 + * - `true`: supported 153 + * - `false`: not supported 154 + */ 155 + export let isBrotliSupported: boolean | undefined; 156 + 157 + /** 158 + * get brotli size using compression stream, if supported. 159 + * returns `undefined` if brotli is not supported by the browser. 160 + */ 161 + export async function getBrotliSize(code: string): Promise<number | undefined> { 162 + if (isBrotliSupported === false) { 163 + return undefined; 164 + } 165 + 166 + if (isBrotliSupported === undefined) { 167 + try { 168 + // @ts-expect-error 'br' is not in the type definition yet 169 + const size = await getCompressedSize(code, 'br'); 170 + isBrotliSupported = true; 171 + return size; 172 + } catch { 173 + isBrotliSupported = false; 174 + return undefined; 175 + } 176 + } 177 + 178 + // @ts-expect-error 'br' is not in the type definition yet 179 + return getCompressedSize(code, 'br'); 180 + } 181 + 182 + // #endregion 183 + 184 + // #region core 185 + 186 + /** 187 + * bundles a subpath from a package that's already loaded in rolldown's memfs. 188 + * 189 + * @param packageName the package name (e.g., "react") 190 + * @param subpath the export subpath to bundle (e.g., ".", "./utils") 191 + * @param selectedExports specific exports to include, or null for all 192 + * @param options bundling options 193 + * @returns bundle result with chunks, sizes, and exported names 194 + */ 195 + export async function bundlePackage( 196 + packageName: string, 197 + subpath: string, 198 + selectedExports: string[] | null, 199 + options: BundleOptions, 200 + ): Promise<BundleResult> { 201 + // bundle with rolldown 202 + const bundle = await rolldown({ 203 + input: { main: VIRTUAL_ENTRY_ID }, 204 + cwd: '/', 205 + external: options.rolldown?.external, 206 + plugins: [ 207 + { 208 + name: 'virtual-entry', 209 + resolveId(id: string) { 210 + if (id === VIRTUAL_ENTRY_ID) { 211 + return id; 212 + } 213 + }, 214 + async load(id: string) { 215 + if (id !== VIRTUAL_ENTRY_ID) { 216 + return; 217 + } 218 + 219 + // check if the module has a default export 220 + let includeDefault = false; 221 + if (selectedExports === null) { 222 + const importPath = subpath === '.' ? packageName : `${packageName}${subpath.slice(1)}`; 223 + const resolved = await this.resolve(importPath); 224 + 225 + if (resolved) { 226 + try { 227 + const source = volume.readFileSync(resolved.id, 'utf8') as string; 228 + includeDefault = hasDefaultExport(source); 229 + } catch { 230 + // couldn't read file, skip default export 231 + } 232 + } 233 + } 234 + 235 + return createVirtualEntry(packageName, subpath, selectedExports, includeDefault); 236 + }, 237 + }, 238 + ], 239 + }); 240 + 241 + const output = await bundle.generate({ 242 + format: 'esm', 243 + minify: options.rolldown?.minify ?? true, 244 + }); 245 + 246 + // process all chunks 247 + const rawChunks = output.output.filter((o) => o.type === 'chunk'); 248 + 249 + progress.emit({ type: 'progress', kind: 'compress' }); 250 + 251 + const chunks: BundleChunk[] = await Promise.all( 252 + rawChunks.map(async (chunk) => { 253 + const code = chunk.code; 254 + const size = getUtf8Length(code); 255 + const [gzipSize, brotliSize] = await Promise.all([getGzipSize(code), getBrotliSize(code)]); 256 + 257 + return { 258 + fileName: chunk.fileName, 259 + code, 260 + size, 261 + gzipSize, 262 + brotliSize, 263 + isEntry: chunk.isEntry, 264 + exports: chunk.exports || [], 265 + }; 266 + }), 267 + ); 268 + 269 + // find entry chunk for exports 270 + const entryChunk = chunks.find((c) => c.isEntry); 271 + if (!entryChunk) { 272 + throw new BundleError('no entry chunk found in bundle output'); 273 + } 274 + 275 + // aggregate sizes 276 + const totalSize = chunks.reduce((acc, c) => acc + c.size, 0); 277 + const totalGzipSize = chunks.reduce((acc, c) => acc + c.gzipSize, 0); 278 + const totalBrotliSize = isBrotliSupported ? chunks.reduce((acc, c) => acc + c.brotliSize!, 0) : undefined; 279 + 280 + await bundle.close(); 281 + 282 + return { 283 + chunks, 284 + size: totalSize, 285 + gzipSize: totalGzipSize, 286 + brotliSize: totalBrotliSize, 287 + exports: entryChunk.exports, 288 + }; 289 + } 290 + 291 + // #endregion
+79
src/npm/errors.ts
··· 1 + /** 2 + * base class for all teardown errors. 3 + */ 4 + export class TeardownError extends Error { 5 + constructor(message: string) { 6 + super(message); 7 + this.name = 'TeardownError'; 8 + } 9 + } 10 + 11 + /** 12 + * thrown when a package cannot be found in the registry. 13 + */ 14 + export class PackageNotFoundError extends TeardownError { 15 + readonly packageName: string; 16 + readonly registry: string; 17 + 18 + constructor(packageName: string, registry: string) { 19 + super(`package not found: ${packageName}`); 20 + this.name = 'PackageNotFoundError'; 21 + this.packageName = packageName; 22 + this.registry = registry; 23 + } 24 + } 25 + 26 + /** 27 + * thrown when no version of a package satisfies the requested range. 28 + */ 29 + export class NoMatchingVersionError extends TeardownError { 30 + readonly packageName: string; 31 + readonly range: string; 32 + 33 + constructor(packageName: string, range: string) { 34 + super(`no version of ${packageName} satisfies ${range}`); 35 + this.name = 'NoMatchingVersionError'; 36 + this.packageName = packageName; 37 + this.range = range; 38 + } 39 + } 40 + 41 + /** 42 + * thrown when a package specifier is malformed. 43 + */ 44 + export class InvalidSpecifierError extends TeardownError { 45 + readonly specifier: string; 46 + 47 + constructor(specifier: string, reason?: string) { 48 + super(reason ? `invalid specifier: ${specifier} (${reason})` : `invalid specifier: ${specifier}`); 49 + this.name = 'InvalidSpecifierError'; 50 + this.specifier = specifier; 51 + } 52 + } 53 + 54 + /** 55 + * thrown when a network request fails. 56 + */ 57 + export class FetchError extends TeardownError { 58 + readonly url: string; 59 + readonly status: number; 60 + readonly statusText: string; 61 + 62 + constructor(url: string, status: number, statusText: string) { 63 + super(`fetch failed: ${status} ${statusText}`); 64 + this.name = 'FetchError'; 65 + this.url = url; 66 + this.status = status; 67 + this.statusText = statusText; 68 + } 69 + } 70 + 71 + /** 72 + * thrown when bundling fails. 73 + */ 74 + export class BundleError extends TeardownError { 75 + constructor(message: string) { 76 + super(message); 77 + this.name = 'BundleError'; 78 + } 79 + }
+8
src/npm/events.ts
··· 1 + import { createEventEmitter } from '../lib/emitter'; 2 + 3 + import type { ProgressMessage } from './worker-protocol'; 4 + 5 + /** 6 + * emitted during package initialization and bundling. 7 + */ 8 + export const progress = createEventEmitter<[ProgressMessage]>();
+156
src/npm/fetch.test.ts
··· 1 + import { Volume } from 'memfs'; 2 + import { describe, expect, it } from 'vitest'; 3 + 4 + import { DEFAULT_EXCLUDE_PATTERNS, fetchPackagesToVolume } from './fetch'; 5 + import { hoist } from './hoist'; 6 + import { resolve } from './resolve'; 7 + 8 + describe('fetchPackagesToVolume', () => { 9 + it('fetches and extracts a simple package', async () => { 10 + // resolve a tiny package 11 + const result = await resolve(['is-odd@3.0.1'], { installPeers: false }); 12 + const hoisted = hoist(result.roots); 13 + const volume = new Volume(); 14 + await fetchPackagesToVolume(hoisted, volume); 15 + 16 + // should have files from is-odd 17 + const isOddPackageJson = volume.readFileSync('/node_modules/is-odd/package.json', 'utf8'); 18 + expect(isOddPackageJson).toBeDefined(); 19 + 20 + // verify it's valid JSON 21 + const json = JSON.parse(isOddPackageJson as string); 22 + expect(json.name).toBe('is-odd'); 23 + 24 + // should also have is-number (dependency) 25 + const isNumberPackageJson = volume.readFileSync('/node_modules/is-number/package.json', 'utf8'); 26 + expect(isNumberPackageJson).toBeDefined(); 27 + }); 28 + 29 + it('respects concurrency limit', async () => { 30 + const result = await resolve(['is-odd@3.0.1'], { installPeers: false }); 31 + const hoisted = hoist(result.roots); 32 + const volume = new Volume(); 33 + 34 + // should work with concurrency of 1 35 + await fetchPackagesToVolume(hoisted, volume, { concurrency: 1 }); 36 + const files = volume.toJSON(); 37 + expect(Object.keys(files).length).toBeGreaterThan(0); 38 + }); 39 + 40 + it('excludes files matching default patterns', async () => { 41 + const result = await resolve(['is-odd@3.0.1'], { installPeers: false }); 42 + const hoisted = hoist(result.roots); 43 + const volume = new Volume(); 44 + await fetchPackagesToVolume(hoisted, volume); 45 + 46 + // should not have README or LICENSE 47 + const files = volume.toJSON(); 48 + for (const path of Object.keys(files)) { 49 + const filename = path.split('/').pop()!; 50 + expect(filename.toUpperCase()).not.toMatch(/^README/); 51 + expect(filename.toUpperCase()).not.toMatch(/^LICENSE/); 52 + } 53 + }); 54 + 55 + it('can disable exclusions with empty array', async () => { 56 + const result = await resolve(['is-odd@3.0.1'], { installPeers: false }); 57 + const hoisted = hoist(result.roots); 58 + 59 + const volumeNoExclude = new Volume(); 60 + await fetchPackagesToVolume(hoisted, volumeNoExclude, { exclude: [] }); 61 + 62 + const volumeWithExclude = new Volume(); 63 + await fetchPackagesToVolume(hoisted, volumeWithExclude); 64 + 65 + // should have more files when nothing is excluded 66 + const noExcludeCount = Object.keys(volumeNoExclude.toJSON()).length; 67 + const withExcludeCount = Object.keys(volumeWithExclude.toJSON()).length; 68 + expect(noExcludeCount).toBeGreaterThanOrEqual(withExcludeCount); 69 + }); 70 + }); 71 + 72 + describe('unpackedSize calculation', () => { 73 + it('populates unpackedSize from tarball when registry does not provide it', async () => { 74 + // JSR packages don't have unpackedSize in registry metadata 75 + const result = await resolve(['jsr:@luca/flag@1.0.1']); 76 + const hoisted = hoist(result.roots); 77 + const volume = new Volume(); 78 + 79 + // before fetch, unpackedSize should be undefined (JSR doesn't provide it) 80 + const rootNode = hoisted.root.get('@luca/flag')!; 81 + expect(rootNode.unpackedSize).toBeUndefined(); 82 + 83 + await fetchPackagesToVolume(hoisted, volume); 84 + 85 + // after fetch, unpackedSize should be populated from tarball 86 + expect(rootNode.unpackedSize).toBeGreaterThan(0); 87 + }); 88 + 89 + it('preserves registry-provided unpackedSize for npm packages', async () => { 90 + const result = await resolve(['is-odd@3.0.1'], { installPeers: false }); 91 + const hoisted = hoist(result.roots); 92 + const volume = new Volume(); 93 + 94 + // npm registry provides unpackedSize 95 + const rootNode = hoisted.root.get('is-odd')!; 96 + const registrySize = rootNode.unpackedSize; 97 + expect(registrySize).toBeGreaterThan(0); 98 + 99 + await fetchPackagesToVolume(hoisted, volume); 100 + 101 + // should preserve the registry-provided size 102 + expect(rootNode.unpackedSize).toBe(registrySize); 103 + }); 104 + 105 + it('includes excluded files in size calculation', async () => { 106 + const result = await resolve(['is-odd@3.0.1'], { installPeers: false }); 107 + const hoisted = hoist(result.roots); 108 + 109 + // clear the registry-provided size to force calculation from tarball 110 + const rootNode = hoisted.root.get('is-odd')!; 111 + rootNode.unpackedSize = undefined; 112 + 113 + const volume = new Volume(); 114 + await fetchPackagesToVolume(hoisted, volume); 115 + 116 + // size should include README, LICENSE, etc. even though they're excluded from extraction 117 + const extractedFiles = volume.toJSON(); 118 + const extractedSize = Object.values(extractedFiles).reduce( 119 + (sum, content) => sum + (content as string).length, 120 + 0, 121 + ); 122 + 123 + // tarball size should be >= extracted size (includes excluded files) 124 + expect(rootNode.unpackedSize).toBeGreaterThanOrEqual(extractedSize); 125 + }); 126 + }); 127 + 128 + describe('DEFAULT_EXCLUDE_PATTERNS', () => { 129 + it('matches README files', () => { 130 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('README.md'))).toBe(true); 131 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('README'))).toBe(true); 132 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('readme.txt'))).toBe(true); 133 + }); 134 + 135 + it('matches LICENSE files', () => { 136 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('LICENSE'))).toBe(true); 137 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('LICENSE.md'))).toBe(true); 138 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('LICENCE'))).toBe(true); 139 + }); 140 + 141 + it('matches test directories', () => { 142 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('__tests__/foo.js'))).toBe(true); 143 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('test/index.js'))).toBe(true); 144 + }); 145 + 146 + it('matches source maps', () => { 147 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('index.js.map'))).toBe(true); 148 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('dist/bundle.js.map'))).toBe(true); 149 + }); 150 + 151 + it('does not match source files', () => { 152 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('index.js'))).toBe(false); 153 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('src/utils.ts'))).toBe(false); 154 + expect(DEFAULT_EXCLUDE_PATTERNS.some((p) => p.test('package.json'))).toBe(false); 155 + }); 156 + });
+192
src/npm/fetch.ts
··· 1 + import { untar } from '@mary/tar'; 2 + import type { Volume } from 'memfs'; 3 + 4 + import { FetchError } from './errors'; 5 + import { progress } from './events'; 6 + import type { HoistedNode, HoistedResult } from './types'; 7 + 8 + /** 9 + * options for fetching packages. 10 + */ 11 + export interface FetchOptions { 12 + /** max concurrent fetches (default 6) */ 13 + concurrency?: number; 14 + /** regex patterns for files to exclude (matched against path after "package/" prefix is stripped) */ 15 + exclude?: RegExp[]; 16 + } 17 + 18 + /** 19 + * default patterns for files that are not needed for bundling. 20 + * matches against the file path within the package (e.g., "README.md", "docs/guide.md") 21 + */ 22 + export const DEFAULT_EXCLUDE_PATTERNS: RegExp[] = [ 23 + // docs and meta files 24 + /^README(\..*)?$/i, 25 + /^LICENSE(\..*)?$/i, 26 + /^LICENCE(\..*)?$/i, 27 + /^CHANGELOG(\..*)?$/i, 28 + /^HISTORY(\..*)?$/i, 29 + /^CONTRIBUTING(\..*)?$/i, 30 + /^AUTHORS(\..*)?$/i, 31 + /^SECURITY(\..*)?$/i, 32 + /^CODE_OF_CONDUCT(\..*)?$/i, 33 + /^\.github\//, 34 + /^\.vscode\//, 35 + /^\.idea\//, 36 + /^docs?\//i, 37 + 38 + // test files 39 + /^__tests__\//, 40 + /^tests?\//i, 41 + /^specs?\//i, 42 + /\.(test|spec)\.[jt]sx?$/, 43 + /\.stories\.[jt]sx?$/, 44 + 45 + // config files 46 + /^\..+rc(\..*)?$/, 47 + /^\.editorconfig$/, 48 + /^\.gitignore$/, 49 + /^\.npmignore$/, 50 + /^\.eslint/, 51 + /^\.prettier/, 52 + /^tsconfig(\..+)?\.json$/, 53 + /^jest\.config/, 54 + /^vitest\.config/, 55 + /^rollup\.config/, 56 + /^webpack\.config/, 57 + /^vite\.config/, 58 + /^babel\.config/, 59 + 60 + // source maps (usually not needed in bundling) 61 + /\.map$/, 62 + ]; 63 + 64 + /** 65 + * fetches a tarball and writes its contents directly to a volume. 66 + * handles gzip decompression and strips the "package/" prefix from paths. 67 + * 68 + * @param url the tarball URL 69 + * @param destPath the destination path in the volume (e.g., "/node_modules/react") 70 + * @param volume the volume to write to 71 + * @param exclude regex patterns for files to skip 72 + * @returns the total size of extracted files in bytes 73 + */ 74 + async function fetchTarballToVolume( 75 + url: string, 76 + destPath: string, 77 + volume: Volume, 78 + exclude: RegExp[] = [], 79 + ): Promise<number> { 80 + const response = await fetch(url); 81 + if (!response.ok) { 82 + throw new FetchError(url, response.status, response.statusText); 83 + } 84 + 85 + const body = response.body; 86 + if (!body) { 87 + throw new FetchError(url, 0, 'response has no body'); 88 + } 89 + 90 + // decompress gzip -> extract tar 91 + const decompressed = body.pipeThrough(new DecompressionStream('gzip')); 92 + 93 + let totalSize = 0; 94 + 95 + for await (const entry of untar(decompressed)) { 96 + // skip directories 97 + if (entry.type !== 'file') { 98 + continue; 99 + } 100 + 101 + // count size from tar header for all files (including excluded ones) 102 + totalSize += entry.size; 103 + 104 + // npm tarballs have files under "package/" prefix - strip it 105 + let path = entry.name; 106 + if (path.startsWith('package/')) { 107 + path = path.slice(8); 108 + } 109 + 110 + // check if file should be excluded (skip extraction but size already counted) 111 + if (exclude.some((pattern) => pattern.test(path))) { 112 + continue; 113 + } 114 + 115 + const content = await entry.bytes(); 116 + const fullPath = `${destPath}/${path}`; 117 + 118 + // ensure parent directories exist 119 + const parentDir = fullPath.slice(0, fullPath.lastIndexOf('/')); 120 + if (!volume.existsSync(parentDir)) { 121 + volume.mkdirSync(parentDir, { recursive: true }); 122 + } 123 + 124 + volume.writeFileSync(fullPath, content); 125 + } 126 + 127 + return totalSize; 128 + } 129 + 130 + /** 131 + * fetches all packages in a hoisted result and writes them to a volume. 132 + * uses default exclude patterns to skip unnecessary files. 133 + * 134 + * @param hoisted the hoisted package structure 135 + * @param volume the volume to write to 136 + * @param options fetch options 137 + */ 138 + export async function fetchPackagesToVolume( 139 + hoisted: HoistedResult, 140 + volume: Volume, 141 + options: FetchOptions = {}, 142 + ): Promise<void> { 143 + const concurrency = options.concurrency ?? 6; 144 + const exclude = options.exclude ?? DEFAULT_EXCLUDE_PATTERNS; 145 + const queue: Array<{ node: HoistedNode; basePath: string }> = []; 146 + 147 + // collect all nodes into a flat queue 148 + function collectNodes(node: HoistedNode, basePath: string): void { 149 + queue.push({ node, basePath }); 150 + if (node.nested.size > 0) { 151 + const nestedBasePath = `${basePath}/${node.name}/node_modules`; 152 + for (const nested of node.nested.values()) { 153 + collectNodes(nested, nestedBasePath); 154 + } 155 + } 156 + } 157 + 158 + for (const node of hoisted.root.values()) { 159 + collectNodes(node, '/node_modules'); 160 + } 161 + 162 + // process queue with concurrency limit using a simple semaphore pattern 163 + let index = 0; 164 + let completed = 0; 165 + const total = queue.length; 166 + 167 + async function worker(): Promise<void> { 168 + while (true) { 169 + const i = index++; 170 + if (i >= queue.length) { 171 + break; 172 + } 173 + 174 + const { node, basePath } = queue[i]; 175 + const packagePath = `${basePath}/${node.name}`; 176 + 177 + const extractedSize = await fetchTarballToVolume(node.tarball, packagePath, volume, exclude); 178 + 179 + // use extracted size if registry didn't provide unpackedSize (e.g., JSR packages) 180 + if (node.unpackedSize === undefined) { 181 + node.unpackedSize = extractedSize; 182 + } 183 + 184 + completed++; 185 + progress.emit({ type: 'progress', kind: 'fetch', current: completed, total, name: node.name }); 186 + } 187 + } 188 + 189 + // start concurrent workers 190 + const workers = Array.from({ length: Math.min(concurrency, queue.length) }, () => worker()); 191 + await Promise.all(workers); 192 + }
+139
src/npm/hoist.ts
··· 1 + import type { HoistedNode, HoistedResult, ResolvedPackage } from './types'; 2 + 3 + /** 4 + * attempts to place a package at the root level. 5 + * returns true if placement succeeded, false if there's a conflict. 6 + * 7 + * a conflict occurs when: 8 + * - a different version of the same package is already at root 9 + * 10 + * @param root the current root node_modules map 11 + * @param pkg the package to place 12 + * @returns true if placed at root, false if needs nesting 13 + */ 14 + function tryPlaceAtRoot(root: Map<string, HoistedNode>, pkg: ResolvedPackage): boolean { 15 + const existing = root.get(pkg.name); 16 + 17 + if (!existing) { 18 + // no conflict, place at root 19 + root.set(pkg.name, { 20 + name: pkg.name, 21 + version: pkg.version, 22 + tarball: pkg.tarball, 23 + integrity: pkg.integrity, 24 + unpackedSize: pkg.unpackedSize, 25 + description: pkg.description, 26 + license: pkg.license, 27 + dependencyCount: pkg.dependencies.size, 28 + nested: new Map(), 29 + }); 30 + return true; 31 + } 32 + 33 + // same version already at root - reuse it 34 + if (existing.version === pkg.version) { 35 + return true; 36 + } 37 + 38 + // different version - conflict, needs nesting 39 + return false; 40 + } 41 + 42 + /** 43 + * hoists dependencies as high as possible in the tree. 44 + * follows npm's hoisting algorithm: 45 + * 1. try to place each package at root 46 + * 2. if conflict, nest it under its parent 47 + * 48 + * peer dependencies are handled by the resolver - they're added as regular 49 + * dependencies of the package that requested them, so they naturally get 50 + * hoisted to root if no conflict, or nested under the dependent if there's 51 + * a version conflict. this ensures the bundler resolves peers correctly. 52 + * 53 + * @param roots the root packages from resolution 54 + * @returns the hoisted node_modules structure 55 + */ 56 + export function hoist(roots: ResolvedPackage[]): HoistedResult { 57 + const root = new Map<string, HoistedNode>(); 58 + 59 + // track which packages we've visited to avoid infinite loops 60 + const visited = new Set<string>(); 61 + 62 + /** 63 + * recursively process a package and its dependencies. 64 + * returns the hoisted node for this package. 65 + */ 66 + function processPackage(pkg: ResolvedPackage, parentNode: HoistedNode | null): HoistedNode | null { 67 + const key = `${pkg.name}@${pkg.version}`; 68 + 69 + // skip if already processed 70 + if (visited.has(key)) { 71 + // return the existing node from root if it exists 72 + return root.get(pkg.name) ?? null; 73 + } 74 + visited.add(key); 75 + 76 + // try to place at root first 77 + const placedAtRoot = tryPlaceAtRoot(root, pkg); 78 + let node: HoistedNode; 79 + 80 + if (placedAtRoot) { 81 + node = root.get(pkg.name)!; 82 + } else if (parentNode) { 83 + // conflict at root, nest under parent 84 + node = { 85 + name: pkg.name, 86 + version: pkg.version, 87 + tarball: pkg.tarball, 88 + integrity: pkg.integrity, 89 + unpackedSize: pkg.unpackedSize, 90 + description: pkg.description, 91 + license: pkg.license, 92 + dependencyCount: pkg.dependencies.size, 93 + nested: new Map(), 94 + }; 95 + parentNode.nested.set(pkg.name, node); 96 + } else { 97 + // this shouldn't happen for root packages 98 + throw new Error(`cannot place root package ${pkg.name}@${pkg.version}`); 99 + } 100 + 101 + // process dependencies 102 + for (const dep of pkg.dependencies.values()) { 103 + processPackage(dep, node); 104 + } 105 + 106 + return node; 107 + } 108 + 109 + // process all root packages 110 + for (const rootPkg of roots) { 111 + processPackage(rootPkg, null); 112 + } 113 + 114 + return { root }; 115 + } 116 + 117 + /** 118 + * converts a hoisted result to a flat list of paths. 119 + * useful for debugging and testing. 120 + * 121 + * @param result the hoisted result 122 + * @returns array of paths like ["node_modules/react", "node_modules/react/node_modules/scheduler"] 123 + */ 124 + export function hoistedToPaths(result: HoistedResult): string[] { 125 + const paths: string[] = []; 126 + 127 + function walk(nodes: Map<string, HoistedNode>, prefix: string): void { 128 + for (const [name, node] of nodes) { 129 + const path = `${prefix}/${name}`; 130 + paths.push(path); 131 + if (node.nested.size > 0) { 132 + walk(node.nested, `${path}/node_modules`); 133 + } 134 + } 135 + } 136 + 137 + walk(result.root, 'node_modules'); 138 + return paths.sort(); 139 + }
+105
src/npm/registry.ts
··· 1 + import { FetchError, InvalidSpecifierError, PackageNotFoundError } from './errors'; 2 + import type { Packument, Registry } from './types'; 3 + 4 + const NPM_REGISTRY = 'https://registry.npmjs.org'; 5 + const JSR_REGISTRY = 'https://npm.jsr.io'; 6 + 7 + /** 8 + * cache for packuments to avoid refetching during resolution. 9 + * key format: "registry:name" (e.g., "npm:react" or "jsr:@luca/flag") 10 + */ 11 + const packumentCache = new Map<string, Packument>(); 12 + 13 + /** 14 + * transforms a JSR package name to the npm-compatible format. 15 + * `@scope/name` becomes `@jsr/scope__name` 16 + * 17 + * @param name the JSR package name (must be scoped) 18 + * @returns the transformed npm-compatible name 19 + */ 20 + export function transformJsrName(name: string): string { 21 + if (!name.startsWith('@')) { 22 + throw new InvalidSpecifierError(name, 'JSR packages must be scoped'); 23 + } 24 + // @scope/name -> @jsr/scope__name 25 + const withoutAt = name.slice(1); // "scope/name" 26 + const transformed = withoutAt.replace('/', '__'); // "scope__name" 27 + return `@jsr/${transformed}`; 28 + } 29 + 30 + /** 31 + * reverses the JSR npm-compatible name back to the canonical format. 32 + * `@jsr/scope__name` becomes `@scope/name` 33 + * 34 + * @param name the npm-compatible JSR package name 35 + * @returns the canonical JSR package name 36 + */ 37 + export function reverseJsrName(name: string): string { 38 + if (!name.startsWith('@jsr/')) { 39 + throw new InvalidSpecifierError(name, 'not a JSR npm-compatible name'); 40 + } 41 + // @jsr/scope__name -> @scope/name 42 + const withoutPrefix = name.slice(5); // "scope__name" 43 + const restored = withoutPrefix.replace('__', '/'); // "scope/name" 44 + return `@${restored}`; 45 + } 46 + 47 + /** 48 + * fetches the packument (full package metadata) from a registry. 49 + * uses the abbreviated format when possible for smaller payloads. 50 + * 51 + * @param name the package name (can be scoped like @scope/pkg) 52 + * @param registry which registry to fetch from (defaults to 'npm') 53 + * @returns the packument with all versions 54 + * @throws if the package doesn't exist or network fails 55 + */ 56 + export async function fetchPackument(name: string, registry: Registry = 'npm'): Promise<Packument> { 57 + const cacheKey = `${registry}:${name}`; 58 + const cached = packumentCache.get(cacheKey); 59 + if (cached) { 60 + return cached; 61 + } 62 + 63 + let registryUrl: string; 64 + let fetchName: string; 65 + 66 + if (registry === 'jsr') { 67 + registryUrl = JSR_REGISTRY; 68 + fetchName = transformJsrName(name); 69 + } else { 70 + registryUrl = NPM_REGISTRY; 71 + fetchName = name; 72 + } 73 + 74 + const encodedName = fetchName.startsWith('@') 75 + ? `@${encodeURIComponent(fetchName.slice(1))}` 76 + : encodeURIComponent(fetchName); 77 + 78 + const url = `${registryUrl}/${encodedName}`; 79 + 80 + const response = await fetch(url, { 81 + headers: { 82 + // request abbreviated format (corgi) for smaller payloads 83 + Accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8', 84 + }, 85 + }); 86 + 87 + if (!response.ok) { 88 + if (response.status === 404) { 89 + throw new PackageNotFoundError(name, registry); 90 + } 91 + throw new FetchError(url, response.status, response.statusText); 92 + } 93 + 94 + const packument = (await response.json()) as Packument; 95 + packumentCache.set(cacheKey, packument); 96 + return packument; 97 + } 98 + 99 + /** 100 + * clears the packument cache. 101 + * useful for testing or when you want fresh data. 102 + */ 103 + export function clearPackumentCache(): void { 104 + packumentCache.clear(); 105 + }
+303
src/npm/resolve.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import { hoist, hoistedToPaths } from './hoist'; 4 + import { reverseJsrName, transformJsrName } from './registry'; 5 + import { parseSpecifier, pickVersion, resolve } from './resolve'; 6 + import type { PackageManifest } from './types'; 7 + 8 + describe('parseSpecifier', () => { 9 + it('parses bare package name', () => { 10 + expect(parseSpecifier('react')).toEqual({ name: 'react', range: 'latest', registry: 'npm' }); 11 + }); 12 + 13 + it('parses package with version', () => { 14 + expect(parseSpecifier('react@18.2.0')).toEqual({ 15 + name: 'react', 16 + range: '18.2.0', 17 + registry: 'npm', 18 + }); 19 + }); 20 + 21 + it('parses package with range', () => { 22 + expect(parseSpecifier('react@^18.0.0')).toEqual({ 23 + name: 'react', 24 + range: '^18.0.0', 25 + registry: 'npm', 26 + }); 27 + }); 28 + 29 + it('parses scoped package', () => { 30 + expect(parseSpecifier('@babel/core')).toEqual({ 31 + name: '@babel/core', 32 + range: 'latest', 33 + registry: 'npm', 34 + }); 35 + }); 36 + 37 + it('parses scoped package with version', () => { 38 + expect(parseSpecifier('@babel/core@7.23.0')).toEqual({ 39 + name: '@babel/core', 40 + range: '7.23.0', 41 + registry: 'npm', 42 + }); 43 + }); 44 + 45 + it('parses scoped package with range', () => { 46 + expect(parseSpecifier('@types/node@^20.0.0')).toEqual({ 47 + name: '@types/node', 48 + range: '^20.0.0', 49 + registry: 'npm', 50 + }); 51 + }); 52 + 53 + it('parses jsr package', () => { 54 + expect(parseSpecifier('jsr:@luca/flag')).toEqual({ 55 + name: '@luca/flag', 56 + range: 'latest', 57 + registry: 'jsr', 58 + }); 59 + }); 60 + 61 + it('parses jsr package with version', () => { 62 + expect(parseSpecifier('jsr:@luca/flag@1.0.0')).toEqual({ 63 + name: '@luca/flag', 64 + range: '1.0.0', 65 + registry: 'jsr', 66 + }); 67 + }); 68 + 69 + it('parses jsr package with range', () => { 70 + expect(parseSpecifier('jsr:@std/path@^1.0.0')).toEqual({ 71 + name: '@std/path', 72 + range: '^1.0.0', 73 + registry: 'jsr', 74 + }); 75 + }); 76 + 77 + it('throws for unscoped jsr package', () => { 78 + expect(() => parseSpecifier('jsr:flag')).toThrow('JSR packages must be scoped'); 79 + }); 80 + 81 + it('parses npm: prefix as noop', () => { 82 + expect(parseSpecifier('npm:react')).toEqual({ 83 + name: 'react', 84 + range: 'latest', 85 + registry: 'npm', 86 + }); 87 + }); 88 + 89 + it('parses npm: prefix with version', () => { 90 + expect(parseSpecifier('npm:react@18.2.0')).toEqual({ 91 + name: 'react', 92 + range: '18.2.0', 93 + registry: 'npm', 94 + }); 95 + }); 96 + 97 + it('parses npm: prefix with scoped package', () => { 98 + expect(parseSpecifier('npm:@babel/core@^7.0.0')).toEqual({ 99 + name: '@babel/core', 100 + range: '^7.0.0', 101 + registry: 'npm', 102 + }); 103 + }); 104 + }); 105 + 106 + describe('transformJsrName', () => { 107 + it('transforms scoped package name', () => { 108 + expect(transformJsrName('@luca/flag')).toBe('@jsr/luca__flag'); 109 + }); 110 + 111 + it('transforms std package name', () => { 112 + expect(transformJsrName('@std/path')).toBe('@jsr/std__path'); 113 + }); 114 + 115 + it('throws for unscoped package', () => { 116 + expect(() => transformJsrName('flag')).toThrow('JSR packages must be scoped'); 117 + }); 118 + }); 119 + 120 + describe('reverseJsrName', () => { 121 + it('reverses npm-compatible JSR name', () => { 122 + expect(reverseJsrName('@jsr/luca__flag')).toBe('@luca/flag'); 123 + }); 124 + 125 + it('reverses std package name', () => { 126 + expect(reverseJsrName('@jsr/std__internal')).toBe('@std/internal'); 127 + }); 128 + 129 + it('throws for non-JSR name', () => { 130 + expect(() => reverseJsrName('@babel/core')).toThrow('not a JSR npm-compatible name'); 131 + }); 132 + }); 133 + 134 + describe('pickVersion', () => { 135 + const mockVersions: Record<string, PackageManifest> = { 136 + '1.0.0': { 137 + name: 'test', 138 + version: '1.0.0', 139 + dist: { tarball: 'https://example.com/test-1.0.0.tgz' }, 140 + }, 141 + '1.1.0': { 142 + name: 'test', 143 + version: '1.1.0', 144 + dist: { tarball: 'https://example.com/test-1.1.0.tgz' }, 145 + }, 146 + '2.0.0': { 147 + name: 'test', 148 + version: '2.0.0', 149 + dist: { tarball: 'https://example.com/test-2.0.0.tgz' }, 150 + }, 151 + '2.1.0-beta.1': { 152 + name: 'test', 153 + version: '2.1.0-beta.1', 154 + dist: { tarball: 'https://example.com/test-2.1.0-beta.1.tgz' }, 155 + }, 156 + }; 157 + 158 + const distTags = { latest: '2.0.0', next: '2.1.0-beta.1' }; 159 + 160 + it('resolves dist-tag', () => { 161 + const result = pickVersion(mockVersions, distTags, 'latest'); 162 + expect(result?.version).toBe('2.0.0'); 163 + }); 164 + 165 + it('resolves next dist-tag', () => { 166 + const result = pickVersion(mockVersions, distTags, 'next'); 167 + expect(result?.version).toBe('2.1.0-beta.1'); 168 + }); 169 + 170 + it('resolves exact version', () => { 171 + const result = pickVersion(mockVersions, distTags, '1.0.0'); 172 + expect(result?.version).toBe('1.0.0'); 173 + }); 174 + 175 + it('resolves caret range', () => { 176 + const result = pickVersion(mockVersions, distTags, '^1.0.0'); 177 + expect(result?.version).toBe('1.1.0'); 178 + }); 179 + 180 + it('resolves tilde range', () => { 181 + const result = pickVersion(mockVersions, distTags, '~1.0.0'); 182 + expect(result?.version).toBe('1.0.0'); 183 + }); 184 + 185 + it('returns null for unsatisfied range', () => { 186 + const result = pickVersion(mockVersions, distTags, '^3.0.0'); 187 + expect(result).toBeNull(); 188 + }); 189 + }); 190 + 191 + describe('resolve', () => { 192 + it('resolves a simple package', async () => { 193 + const result = await resolve(['is-odd@3.0.1']); 194 + 195 + expect(result.roots).toHaveLength(1); 196 + expect(result.roots[0].name).toBe('is-odd'); 197 + expect(result.roots[0].version).toBe('3.0.1'); 198 + 199 + // is-odd depends on is-number 200 + expect(result.roots[0].dependencies.has('is-number')).toBe(true); 201 + }); 202 + 203 + it('resolves multiple packages', async () => { 204 + const result = await resolve(['is-odd@3.0.1', 'is-even@1.0.0']); 205 + 206 + expect(result.roots).toHaveLength(2); 207 + expect(result.roots[0].name).toBe('is-odd'); 208 + expect(result.roots[1].name).toBe('is-even'); 209 + }); 210 + 211 + it('deduplicates shared dependencies', async () => { 212 + // both is-odd and is-even depend on is-number 213 + const result = await resolve(['is-odd@3.0.1', 'is-even@1.0.0']); 214 + 215 + // count unique packages 216 + const isNumberVersions = new Set<string>(); 217 + for (const pkg of result.packages.values()) { 218 + if (pkg.name === 'is-number') { 219 + isNumberVersions.add(pkg.version); 220 + } 221 + } 222 + 223 + // should have is-number in the packages map 224 + expect(isNumberVersions.size).toBeGreaterThan(0); 225 + }); 226 + 227 + it('resolves a JSR package', async () => { 228 + const result = await resolve(['jsr:@luca/flag@1.0.1']); 229 + 230 + expect(result.roots).toHaveLength(1); 231 + expect(result.roots[0].name).toBe('@luca/flag'); 232 + expect(result.roots[0].version).toBe('1.0.1'); 233 + expect(result.roots[0].tarball).toContain('npm.jsr.io'); 234 + }); 235 + 236 + it('resolves a JSR package with JSR dependencies', async () => { 237 + // @std/path@1.1.4 depends on @jsr/std__internal (reversed to @std/internal) 238 + const result = await resolve(['jsr:@std/path@1.1.4']); 239 + 240 + expect(result.roots).toHaveLength(1); 241 + expect(result.roots[0].name).toBe('@std/path'); 242 + expect(result.roots[0].version).toBe('1.1.4'); 243 + 244 + // dependency is stored under the original name from the manifest 245 + expect(result.roots[0].dependencies.has('@jsr/std__internal')).toBe(true); 246 + const internal = result.roots[0].dependencies.get('@jsr/std__internal')!; 247 + // but the resolved package uses the canonical name 248 + expect(internal.name).toBe('@std/internal'); 249 + expect(internal.tarball).toContain('npm.jsr.io'); 250 + }); 251 + 252 + it('auto-installs required peer dependencies', async () => { 253 + // use-sync-external-store has react as a required peer 254 + const result = await resolve(['use-sync-external-store@1.2.0']); 255 + 256 + // react should be added as a dependency of use-sync-external-store 257 + const mainPkg = result.roots[0]; 258 + expect(mainPkg.dependencies.has('react')).toBe(true); 259 + 260 + // react should also be in the packages map 261 + const hasReact = Array.from(result.packages.values()).some((p) => p.name === 'react'); 262 + expect(hasReact).toBe(true); 263 + }); 264 + 265 + it('skips optional peer dependencies', async () => { 266 + // resolve something with optional peers and verify they're not installed 267 + const result = await resolve(['use-sync-external-store@1.2.0']); 268 + 269 + // react should be there (required peer) as a dependency 270 + const mainPkg = result.roots[0]; 271 + expect(mainPkg.dependencies.has('react')).toBe(true); 272 + }); 273 + 274 + it('respects installPeers: false option', async () => { 275 + const result = await resolve(['use-sync-external-store@1.2.0'], { installPeers: false }); 276 + 277 + // should only have the requested package 278 + expect(result.roots).toHaveLength(1); 279 + expect(result.roots[0].name).toBe('use-sync-external-store'); 280 + }); 281 + }); 282 + 283 + describe('hoist', () => { 284 + it('hoists simple dependencies to root', async () => { 285 + const result = await resolve(['is-odd@3.0.1']); 286 + const hoisted = hoist(result.roots); 287 + const paths = hoistedToPaths(hoisted); 288 + 289 + // both is-odd and is-number should be at root 290 + expect(paths).toContain('node_modules/is-odd'); 291 + expect(paths).toContain('node_modules/is-number'); 292 + }); 293 + 294 + it('nests conflicting versions', async () => { 295 + // this test would need packages with conflicting versions 296 + // for now, just verify the basic structure works 297 + const result = await resolve(['is-odd@3.0.1']); 298 + const hoisted = hoist(result.roots); 299 + 300 + expect(hoisted.root.size).toBeGreaterThan(0); 301 + expect(hoisted.root.has('is-odd')).toBe(true); 302 + }); 303 + });
+262
src/npm/resolve.ts
··· 1 + import * as semver from 'semver'; 2 + 3 + import { InvalidSpecifierError, NoMatchingVersionError } from './errors'; 4 + import { progress } from './events'; 5 + import { fetchPackument, reverseJsrName } from './registry'; 6 + import type { PackageManifest, PackageSpecifier, Registry, ResolvedPackage, ResolutionResult } from './types'; 7 + 8 + /** 9 + * parses a package specifier string into name, range, and registry. 10 + * handles scoped packages, JSR packages, and various formats: 11 + * - "foo" -> { name: "foo", range: "latest", registry: "npm" } 12 + * - "foo@^1.0.0" -> { name: "foo", range: "^1.0.0", registry: "npm" } 13 + * - "@scope/foo@~2.0.0" -> { name: "@scope/foo", range: "~2.0.0", registry: "npm" } 14 + * - "npm:foo@^1.0.0" -> { name: "foo", range: "^1.0.0", registry: "npm" } 15 + * - "jsr:@luca/flag" -> { name: "@luca/flag", range: "latest", registry: "jsr" } 16 + * - "jsr:@luca/flag@^1.0.0" -> { name: "@luca/flag", range: "^1.0.0", registry: "jsr" } 17 + * 18 + * @param spec the package specifier string 19 + * @returns parsed specifier with name, range, and registry 20 + */ 21 + export function parseSpecifier(spec: string): PackageSpecifier { 22 + let registry: Registry = 'npm'; 23 + let rest = spec; 24 + 25 + // check for registry prefixes 26 + if (spec.startsWith('jsr:')) { 27 + registry = 'jsr'; 28 + rest = spec.slice(4); // remove "jsr:" 29 + } else if (spec.startsWith('npm:')) { 30 + rest = spec.slice(4); // remove "npm:", registry already 'npm' 31 + } 32 + 33 + // handle scoped packages: @scope/name or @scope/name@version 34 + if (rest.startsWith('@')) { 35 + const slashIdx = rest.indexOf('/'); 36 + if (slashIdx === -1) { 37 + throw new InvalidSpecifierError(spec, 'scoped package missing slash'); 38 + } 39 + const atIdx = rest.indexOf('@', slashIdx); 40 + if (atIdx === -1) { 41 + return { name: rest, range: 'latest', registry }; 42 + } 43 + return { name: rest.slice(0, atIdx), range: rest.slice(atIdx + 1), registry }; 44 + } 45 + 46 + // JSR packages must be scoped 47 + if (registry === 'jsr') { 48 + throw new InvalidSpecifierError(spec, 'JSR packages must be scoped'); 49 + } 50 + 51 + // handle regular packages: name or name@version 52 + const atIdx = rest.indexOf('@'); 53 + if (atIdx === -1) { 54 + return { name: rest, range: 'latest', registry }; 55 + } 56 + return { name: rest.slice(0, atIdx), range: rest.slice(atIdx + 1), registry }; 57 + } 58 + 59 + /** 60 + * picks the best version from a packument that satisfies a range. 61 + * follows npm's algorithm: 62 + * 1. if range is a dist-tag, use that version 63 + * 2. if range is a specific version, use that 64 + * 3. otherwise, find highest version that satisfies the semver range 65 + * 66 + * @param versions available versions (version string -> manifest) 67 + * @param distTags dist-tags mapping (e.g., { latest: "1.2.3" }) 68 + * @param range the version range to satisfy 69 + * @returns the best matching manifest, or null if none match 70 + */ 71 + export function pickVersion( 72 + versions: Record<string, PackageManifest>, 73 + distTags: Record<string, string>, 74 + range: string, 75 + ): PackageManifest | null { 76 + // check if range is a dist-tag 77 + if (range in distTags) { 78 + const taggedVersion = distTags[range]; 79 + return versions[taggedVersion] ?? null; 80 + } 81 + 82 + // check if range is an exact version 83 + if (versions[range]) { 84 + return versions[range]; 85 + } 86 + 87 + // find highest version satisfying the range 88 + const validVersions = Object.keys(versions) 89 + .filter((v) => semver.satisfies(v, range)) 90 + .sort(semver.rcompare); 91 + 92 + if (validVersions.length === 0) { 93 + return null; 94 + } 95 + 96 + return versions[validVersions[0]]; 97 + } 98 + 99 + /** 100 + * options for dependency resolution. 101 + */ 102 + export interface ResolveOptions { 103 + /** 104 + * whether to auto-install peer dependencies. 105 + * when true, required (non-optional) peer dependencies are resolved automatically. 106 + * @default true 107 + */ 108 + installPeers?: boolean; 109 + } 110 + 111 + /** 112 + * context for tracking resolution state across recursive calls. 113 + */ 114 + interface ResolutionContext { 115 + /** all resolved packages by "registry:name@version" key for deduping */ 116 + resolved: Map<string, ResolvedPackage>; 117 + /** packages currently being resolved (for cycle detection) */ 118 + resolving: Set<string>; 119 + /** resolution options */ 120 + options: Required<ResolveOptions>; 121 + } 122 + 123 + /** 124 + * resolves a single package and its dependencies recursively. 125 + * 126 + * @param name package name 127 + * @param range version range to satisfy 128 + * @param registry which registry to fetch from 129 + * @param ctx resolution context for deduping and cycle detection 130 + * @returns the resolved package tree 131 + */ 132 + async function resolvePackage( 133 + name: string, 134 + range: string, 135 + registry: Registry, 136 + ctx: ResolutionContext, 137 + ): Promise<ResolvedPackage> { 138 + const packument = await fetchPackument(name, registry); 139 + const manifest = pickVersion(packument.versions, packument['dist-tags'], range); 140 + 141 + if (!manifest) { 142 + throw new NoMatchingVersionError(name, range); 143 + } 144 + 145 + progress.emit({ type: 'progress', kind: 'resolve', name, version: manifest.version }); 146 + 147 + const key = `${registry}:${name}@${manifest.version}`; 148 + 149 + // check if already resolved (deduplication) 150 + const existing = ctx.resolved.get(key); 151 + if (existing) { 152 + return existing; 153 + } 154 + 155 + // cycle detection - if we're already resolving this, return a placeholder 156 + // the actual dependencies will be filled in by the original resolution 157 + if (ctx.resolving.has(key)) { 158 + // create a minimal resolved package for the cycle 159 + const cyclic: ResolvedPackage = { 160 + name, 161 + version: manifest.version, 162 + tarball: manifest.dist.tarball, 163 + integrity: manifest.dist.integrity, 164 + dependencies: new Map(), 165 + }; 166 + return cyclic; 167 + } 168 + 169 + ctx.resolving.add(key); 170 + 171 + // create the resolved package 172 + const resolved: ResolvedPackage = { 173 + name, 174 + version: manifest.version, 175 + tarball: manifest.dist.tarball, 176 + integrity: manifest.dist.integrity, 177 + unpackedSize: manifest.dist.unpackedSize, 178 + description: manifest.description, 179 + license: manifest.license, 180 + dependencies: new Map(), 181 + }; 182 + 183 + // register early so cycles can find it 184 + ctx.resolved.set(key, resolved); 185 + 186 + // collect all dependencies to resolve (regular deps + peer deps) 187 + const depsToResolve: Array<[string, string]> = []; 188 + 189 + // add regular dependencies 190 + const deps = manifest.dependencies ?? {}; 191 + for (const [depName, depRange] of Object.entries(deps)) { 192 + depsToResolve.push([depName, depRange]); 193 + } 194 + 195 + // add peer dependencies as regular dependencies of this package 196 + // this ensures they get hoisted correctly - placed at root if no conflict, 197 + // or nested under this package if there's a version conflict 198 + if (ctx.options.installPeers && manifest.peerDependencies) { 199 + const peerMeta = manifest.peerDependenciesMeta ?? {}; 200 + for (const [peerName, peerRange] of Object.entries(manifest.peerDependencies)) { 201 + const isOptional = peerMeta[peerName]?.optional === true; 202 + if (!isOptional) { 203 + // only add if not already in regular deps (regular deps take precedence) 204 + if (!(peerName in deps)) { 205 + depsToResolve.push([peerName, peerRange]); 206 + } 207 + } 208 + } 209 + } 210 + 211 + // resolve all dependencies in parallel 212 + const resolvedDeps = await Promise.all( 213 + depsToResolve.map(async ([depName, depRange]) => { 214 + // when a JSR package depends on @jsr/*, reverse to canonical name and fetch from JSR 215 + // otherwise use npm (even for @jsr/* from npm packages - that's what the author intended) 216 + let resolvedName = depName; 217 + let depRegistry: Registry = 'npm'; 218 + if (registry === 'jsr' && depName.startsWith('@jsr/')) { 219 + resolvedName = reverseJsrName(depName); 220 + depRegistry = 'jsr'; 221 + } 222 + const dep = await resolvePackage(resolvedName, depRange, depRegistry, ctx); 223 + return [depName, dep] as const; 224 + }), 225 + ); 226 + 227 + for (const [depName, dep] of resolvedDeps) { 228 + resolved.dependencies.set(depName, dep); 229 + } 230 + 231 + ctx.resolving.delete(key); 232 + return resolved; 233 + } 234 + 235 + /** 236 + * resolves one or more packages and all their dependencies. 237 + * this is the main entry point for dependency resolution. 238 + * 239 + * @param specifiers package specifiers to resolve (e.g., ["react@^18.0.0", "jsr:@luca/flag"]) 240 + * @param options resolution options 241 + * @returns the full resolution result with all packages 242 + */ 243 + export async function resolve(specifiers: string[], options: ResolveOptions = {}): Promise<ResolutionResult> { 244 + const ctx: ResolutionContext = { 245 + resolved: new Map(), 246 + resolving: new Set(), 247 + options: { 248 + installPeers: options.installPeers ?? true, 249 + }, 250 + }; 251 + 252 + const parsedSpecs = specifiers.map(parseSpecifier); 253 + 254 + const roots = await Promise.all( 255 + parsedSpecs.map(({ name, range, registry }) => resolvePackage(name, range, registry, ctx)), 256 + ); 257 + 258 + return { 259 + roots, 260 + packages: ctx.resolved, 261 + }; 262 + }
+321
src/npm/subpaths.ts
··· 1 + import type { Volume } from 'memfs'; 2 + 3 + import type { PackageExports, PackageJson } from './types'; 4 + 5 + // #region types 6 + 7 + /** 8 + * a discovered subpath entry from package.json exports. 9 + */ 10 + export interface Subpath { 11 + /** the subpath pattern (e.g., ".", "./utils", "./feature/*") */ 12 + subpath: string; 13 + /** the resolved file path relative to package root */ 14 + target: string; 15 + /** whether this is a wildcard-expanded entry */ 16 + isWildcard: boolean; 17 + } 18 + 19 + /** 20 + * result of discovering package subpaths. 21 + */ 22 + export interface DiscoveredSubpaths { 23 + /** all available subpaths */ 24 + subpaths: Subpath[]; 25 + /** the default subpath to select (usually ".") */ 26 + defaultSubpath: string | null; 27 + } 28 + 29 + // #endregion 30 + 31 + // #region condition resolution 32 + 33 + /** 34 + * condition priority for ESM browser bundling. 35 + * higher index = higher priority. 36 + */ 37 + const CONDITION_PRIORITY = ['default', 'module', 'import', 'browser'] as const; 38 + 39 + /** 40 + * resolves a conditional export to a file path. 41 + * handles nested conditions and returns the best match for ESM browser. 42 + */ 43 + function resolveCondition(value: PackageExports): string | null { 44 + if (value === null) { 45 + return null; 46 + } 47 + 48 + if (typeof value === 'string') { 49 + return value; 50 + } 51 + 52 + if (Array.isArray(value)) { 53 + // array means "try in order", take first 54 + for (const item of value) { 55 + const resolved = resolveCondition(item); 56 + if (resolved) { 57 + return resolved; 58 + } 59 + } 60 + return null; 61 + } 62 + 63 + if (typeof value === 'object') { 64 + // check if this is a conditions object or a subpath object 65 + const keys = Object.keys(value); 66 + 67 + // if any key starts with '.', this is a subpath object, not conditions 68 + if (keys.some((k) => k.startsWith('.'))) { 69 + return null; 70 + } 71 + 72 + // this is a conditions object, find best match 73 + let bestMatch: string | null = null; 74 + let bestPriority = -1; 75 + 76 + for (const [condition, target] of Object.entries(value)) { 77 + const priority = CONDITION_PRIORITY.indexOf(condition as (typeof CONDITION_PRIORITY)[number]); 78 + 79 + if (priority > bestPriority) { 80 + const resolved = resolveCondition(target as PackageExports); 81 + if (resolved) { 82 + bestMatch = resolved; 83 + bestPriority = priority; 84 + } 85 + } 86 + } 87 + 88 + return bestMatch; 89 + } 90 + 91 + return null; 92 + } 93 + 94 + // #endregion 95 + 96 + // #region wildcard expansion 97 + 98 + /** 99 + * recursively lists all files in a directory. 100 + */ 101 + function listFilesRecursive(volume: Volume, dir: string): string[] { 102 + const files: string[] = []; 103 + 104 + try { 105 + const entries = volume.readdirSync(dir, { withFileTypes: true }); 106 + for (const entry of entries) { 107 + const fullPath = `${dir}/${entry.name}`; 108 + if (entry.isDirectory()) { 109 + files.push(...listFilesRecursive(volume, fullPath)); 110 + } else if (entry.isFile()) { 111 + files.push(fullPath); 112 + } 113 + } 114 + } catch { 115 + // directory doesn't exist or can't be read 116 + } 117 + 118 + return files; 119 + } 120 + 121 + /** 122 + * expands a wildcard pattern against the volume files. 123 + * 124 + * @param subpath the subpath pattern with wildcard (e.g., "./*") 125 + * @param target the target pattern (e.g., "./*.js") 126 + * @param packagePath the package path in volume (e.g., "/node_modules/pkg") 127 + * @param volume the volume to search in 128 + * @returns expanded subpath entries 129 + */ 130 + function expandWildcard(subpath: string, target: string, packagePath: string, volume: Volume): Subpath[] { 131 + const entries: Subpath[] = []; 132 + 133 + // extract the parts before and after the wildcard 134 + const targetParts = target.split('*'); 135 + if (targetParts.length !== 2) { 136 + // invalid pattern, skip 137 + return entries; 138 + } 139 + 140 + const [prefix, suffix] = targetParts; 141 + const subpathParts = subpath.split('*'); 142 + if (subpathParts.length !== 2) { 143 + return entries; 144 + } 145 + 146 + const [subpathPrefix, subpathSuffix] = subpathParts; 147 + 148 + // normalize the prefix to match volume paths 149 + // target like "./src/*.js" becomes "/node_modules/pkg/src" 150 + const searchDir = `${packagePath}/${prefix.replace(/^\.\//, '').replace(/\/$/, '')}`; 151 + 152 + // list all files in the search directory 153 + const allFiles = listFilesRecursive(volume, searchDir); 154 + 155 + for (const filePath of allFiles) { 156 + // check if file matches the pattern 157 + const relativePath = filePath.slice(searchDir.length + 1); 158 + 159 + if (suffix && !filePath.endsWith(suffix)) { 160 + continue; 161 + } 162 + 163 + // extract the wildcard match 164 + const match = suffix ? relativePath.slice(0, relativePath.length - suffix.length) : relativePath; 165 + 166 + // construct the subpath 167 + const expandedSubpath = `${subpathPrefix}${match}${subpathSuffix}`; 168 + 169 + // construct the relative target 170 + const expandedTarget = `./${prefix.replace(/^\.\//, '')}${match}${suffix}`; 171 + 172 + entries.push({ 173 + subpath: expandedSubpath, 174 + target: expandedTarget, 175 + isWildcard: true, 176 + }); 177 + } 178 + 179 + return entries; 180 + } 181 + 182 + // #endregion 183 + 184 + // #region main discovery 185 + 186 + /** 187 + * discovers all available subpaths from a package's exports field. 188 + * 189 + * @param packageJson the package.json content 190 + * @param volume the volume containing package files 191 + * @returns discovered subpaths with default selection 192 + */ 193 + export function discoverSubpaths(packageJson: PackageJson, volume: Volume): DiscoveredSubpaths { 194 + const entries: Subpath[] = []; 195 + const packagePath = `/node_modules/${packageJson.name}`; 196 + 197 + // check for exports field first (takes precedence) 198 + if (packageJson.exports !== undefined) { 199 + const exportsField = packageJson.exports; 200 + 201 + if (typeof exportsField === 'string') { 202 + // simple string export: "exports": "./index.js" 203 + entries.push({ 204 + subpath: '.', 205 + target: exportsField, 206 + isWildcard: false, 207 + }); 208 + } else if (Array.isArray(exportsField)) { 209 + // array export: "exports": ["./index.js", "./index.cjs"] 210 + const resolved = resolveCondition(exportsField); 211 + if (resolved) { 212 + entries.push({ 213 + subpath: '.', 214 + target: resolved, 215 + isWildcard: false, 216 + }); 217 + } 218 + } else if (typeof exportsField === 'object' && exportsField !== null) { 219 + // object export - could be conditions or subpaths 220 + const keys = Object.keys(exportsField); 221 + const hasSubpaths = keys.some((k) => k.startsWith('.')); 222 + 223 + if (hasSubpaths) { 224 + // subpath exports 225 + for (const [subpath, value] of Object.entries(exportsField)) { 226 + if (!subpath.startsWith('.')) { 227 + continue; 228 + } 229 + 230 + if (subpath.includes('*')) { 231 + // wildcard pattern 232 + const target = resolveCondition(value as PackageExports); 233 + if (target && target.includes('*')) { 234 + const expanded = expandWildcard(subpath, target, packagePath, volume); 235 + entries.push(...expanded); 236 + } 237 + } else { 238 + // regular subpath 239 + const target = resolveCondition(value as PackageExports); 240 + if (target) { 241 + entries.push({ 242 + subpath, 243 + target, 244 + isWildcard: false, 245 + }); 246 + } 247 + } 248 + } 249 + } else { 250 + // top-level conditions (no subpaths means this is conditions for ".") 251 + const target = resolveCondition(exportsField); 252 + if (target) { 253 + entries.push({ 254 + subpath: '.', 255 + target, 256 + isWildcard: false, 257 + }); 258 + } 259 + } 260 + } 261 + } else { 262 + // fallback to legacy fields 263 + // priority: module > main > index.js 264 + let legacyMain = packageJson.module || packageJson.main; 265 + 266 + if (!legacyMain) { 267 + // check if index.js exists 268 + try { 269 + volume.statSync(`${packagePath}/index.js`); 270 + legacyMain = './index.js'; 271 + } catch { 272 + // no index.js 273 + } 274 + } 275 + 276 + if (legacyMain) { 277 + entries.push({ 278 + subpath: '.', 279 + target: legacyMain.startsWith('.') ? legacyMain : `./${legacyMain}`, 280 + isWildcard: false, 281 + }); 282 + } 283 + } 284 + 285 + // determine default subpath 286 + let defaultSubpath: string | null = null; 287 + 288 + // prefer "." if it exists 289 + const mainEntry = entries.find((e) => e.subpath === '.'); 290 + if (mainEntry) { 291 + defaultSubpath = '.'; 292 + } else if (entries.length > 0) { 293 + // otherwise, pick first alphabetically 294 + entries.sort((a, b) => a.subpath.localeCompare(b.subpath)); 295 + defaultSubpath = entries[0].subpath; 296 + } 297 + 298 + return { 299 + subpaths: entries, 300 + defaultSubpath, 301 + }; 302 + } 303 + 304 + /** 305 + * gets the import specifier for a subpath entry. 306 + * this is what you'd write in an import statement. 307 + * 308 + * @param packageName the package name 309 + * @param entry the subpath entry 310 + * @returns the import specifier (e.g., "react", "react/jsx-runtime") 311 + */ 312 + export function getImportSpecifier(packageName: string, entry: Subpath): string { 313 + if (entry.subpath === '.') { 314 + return packageName; 315 + } 316 + 317 + // "./foo" -> "package/foo" 318 + return `${packageName}${entry.subpath.slice(1)}`; 319 + } 320 + 321 + // #endregion
+140
src/npm/types.ts
··· 1 + /** 2 + * the parsed package.json of the main package. 3 + * includes fields relevant for export discovery and display. 4 + */ 5 + export interface PackageJson { 6 + name: string; 7 + version: string; 8 + description?: string; 9 + license?: string; 10 + main?: string; 11 + module?: string; 12 + browser?: string | Record<string, string | false>; 13 + types?: string; 14 + typings?: string; 15 + exports?: PackageExports; 16 + type?: 'module' | 'commonjs'; 17 + } 18 + 19 + /** 20 + * package exports field - can be a string, array, object, or nested conditions. 21 + * https://nodejs.org/api/packages.html#exports 22 + */ 23 + export type PackageExports = string | string[] | { [key: string]: PackageExports } | null; 24 + 25 + /** 26 + * npm registry packument - the full metadata for a package including all versions. 27 + * fetched from registry.npmjs.org/{package-name} 28 + */ 29 + export interface Packument { 30 + name: string; 31 + 'dist-tags': Record<string, string>; 32 + versions: Record<string, PackageManifest>; 33 + time?: Record<string, string>; 34 + } 35 + 36 + /** 37 + * package manifest for a specific version. 38 + * this is what you'd find in a package.json plus registry metadata. 39 + */ 40 + export interface PackageManifest { 41 + name: string; 42 + version: string; 43 + description?: string; 44 + license?: string; 45 + main?: string; 46 + module?: string; 47 + exports?: PackageExports; 48 + type?: 'module' | 'commonjs'; 49 + dependencies?: Record<string, string>; 50 + devDependencies?: Record<string, string>; 51 + peerDependencies?: Record<string, string>; 52 + peerDependenciesMeta?: Record<string, { optional?: boolean }>; 53 + optionalDependencies?: Record<string, string>; 54 + dist: { 55 + tarball: string; 56 + integrity?: string; 57 + shasum?: string; 58 + /** total unpacked size in bytes */ 59 + unpackedSize?: number; 60 + /** number of files in the tarball */ 61 + fileCount?: number; 62 + }; 63 + } 64 + 65 + /** 66 + * a resolved package with its dependencies. 67 + * this is the output of the resolution step before hoisting. 68 + */ 69 + export interface ResolvedPackage { 70 + name: string; 71 + version: string; 72 + /** the tarball URL for fetching */ 73 + tarball: string; 74 + /** SRI integrity hash if available */ 75 + integrity?: string; 76 + /** unpacked size in bytes (from registry) */ 77 + unpackedSize?: number; 78 + /** package description */ 79 + description?: string; 80 + /** license identifier */ 81 + license?: string; 82 + /** resolved dependencies (name -> ResolvedPackage) */ 83 + dependencies: Map<string, ResolvedPackage>; 84 + } 85 + 86 + /** 87 + * supported package registries. 88 + */ 89 + export type Registry = 'npm' | 'jsr'; 90 + 91 + /** 92 + * the input to the resolver - a package specifier. 93 + * can be just a name (uses latest) or name@version/range. 94 + */ 95 + export interface PackageSpecifier { 96 + name: string; 97 + /** version, range, or dist-tag. defaults to 'latest' */ 98 + range: string; 99 + /** which registry to fetch from. defaults to 'npm' */ 100 + registry: Registry; 101 + } 102 + 103 + /** 104 + * the full resolution result - a tree of resolved packages. 105 + */ 106 + export interface ResolutionResult { 107 + /** the root package(s) that were requested */ 108 + roots: ResolvedPackage[]; 109 + /** all unique packages in the resolution (for deduping) */ 110 + packages: Map<string, ResolvedPackage>; 111 + } 112 + 113 + /** 114 + * a node in the hoisted node_modules structure. 115 + * represents what should be written to node_modules/{name} 116 + */ 117 + export interface HoistedNode { 118 + name: string; 119 + version: string; 120 + tarball: string; 121 + integrity?: string; 122 + /** unpacked size in bytes (from registry) */ 123 + unpackedSize?: number; 124 + /** package description */ 125 + description?: string; 126 + /** license identifier */ 127 + license?: string; 128 + /** number of direct dependencies */ 129 + dependencyCount: number; 130 + /** nested node_modules for this package (when hoisting fails) */ 131 + nested: Map<string, HoistedNode>; 132 + } 133 + 134 + /** 135 + * the result of hoisting - a flat(ish) node_modules structure. 136 + */ 137 + export interface HoistedResult { 138 + /** top-level node_modules entries */ 139 + root: Map<string, HoistedNode>; 140 + }
+147
src/npm/worker-client.ts
··· 1 + import * as v from 'valibot'; 2 + 3 + import type { BundleOptions, BundleResult } from './bundler'; 4 + import { progress } from './events'; 5 + import { 6 + workerResponseSchema, 7 + type InitOptions, 8 + type InitResult, 9 + type WorkerRequest, 10 + } from './worker-protocol'; 11 + 12 + export type { InitResult }; 13 + 14 + /** 15 + * a session for working with a package. 16 + * holds the worker and initialization result. 17 + */ 18 + export interface PackageSession extends InitResult { 19 + /** the worker instance for this session */ 20 + worker: BundlerWorker; 21 + } 22 + 23 + /** 24 + * client for communicating with a bundler worker. 25 + * each instance spawns a new worker, intended for one package. 26 + */ 27 + export class BundlerWorker { 28 + private worker: Worker; 29 + private nextId = 0; 30 + private pending = new Map<number, PromiseWithResolvers<unknown>>(); 31 + private ready: Promise<void>; 32 + private resolveReady!: () => void; 33 + 34 + constructor() { 35 + this.ready = new Promise((resolve) => { 36 + this.resolveReady = resolve; 37 + }); 38 + 39 + this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }); 40 + this.worker.onmessage = this.handleMessage.bind(this); 41 + this.worker.onerror = this.handleError.bind(this); 42 + } 43 + 44 + private handleMessage(event: MessageEvent<unknown>): void { 45 + // check for ready signal 46 + if (event.data && typeof event.data === 'object' && 'type' in event.data && event.data.type === 'ready') { 47 + console.log('[worker-client] received ready signal'); 48 + this.resolveReady(); 49 + return; 50 + } 51 + 52 + const parsed = v.safeParse(workerResponseSchema, event.data); 53 + if (!parsed.success) { 54 + console.error('[worker-client] invalid response:', parsed.issues, event.data); 55 + return; 56 + } 57 + 58 + const response = parsed.output; 59 + 60 + // forward progress messages to global emitter 61 + if (response.type === 'progress') { 62 + progress.emit(response); 63 + return; 64 + } 65 + 66 + const deferred = this.pending.get(response.id); 67 + if (!deferred) { 68 + // response for a request we no longer care about (e.g., superseded bundle) 69 + return; 70 + } 71 + 72 + this.pending.delete(response.id); 73 + 74 + if (response.type === 'error') { 75 + deferred.reject(new Error(response.error)); 76 + } else { 77 + deferred.resolve(response.result); 78 + } 79 + } 80 + 81 + private handleError(event: ErrorEvent): void { 82 + console.error('[worker-client] worker error:', event); 83 + // reject all pending requests 84 + for (const deferred of this.pending.values()) { 85 + deferred.reject(new Error('Worker error')); 86 + } 87 + this.pending.clear(); 88 + } 89 + 90 + private async send<T>(message: WorkerRequest): Promise<T> { 91 + // wait for worker to be ready before sending 92 + await this.ready; 93 + 94 + const deferred = Promise.withResolvers<T>(); 95 + this.pending.set(message.id, deferred as PromiseWithResolvers<unknown>); 96 + console.log('[worker-client] posting message:', message); 97 + this.worker.postMessage(message); 98 + return deferred.promise; 99 + } 100 + 101 + /** 102 + * initializes the worker with a package. 103 + * only the first call does work; subsequent calls return cached result. 104 + */ 105 + init(packageSpec: string, options?: InitOptions): Promise<InitResult> { 106 + return this.send<InitResult>({ id: this.nextId++, type: 'init', packageSpec, options }); 107 + } 108 + 109 + /** 110 + * bundles a subpath from the initialized package. 111 + * uses "latest wins" - if called while a bundle is in progress, 112 + * the previous pending request is superseded. 113 + */ 114 + bundle(subpath: string, selectedExports: string[] | null, options?: BundleOptions): Promise<BundleResult> { 115 + return this.send<BundleResult>({ id: this.nextId++, type: 'bundle', subpath, selectedExports, options }); 116 + } 117 + 118 + /** 119 + * terminates the worker. 120 + */ 121 + terminate(): void { 122 + this.worker.terminate(); 123 + for (const deferred of this.pending.values()) { 124 + deferred.reject(new DOMException('Worker terminated', 'AbortError')); 125 + } 126 + this.pending.clear(); 127 + } 128 + } 129 + 130 + /** 131 + * creates a new worker and initializes it with a package. 132 + * if initialization fails, the worker is terminated. 133 + * 134 + * @param packageSpec package specifier (e.g., "react@^18.0.0") 135 + * @param options initialization options 136 + * @returns session containing the worker and init result 137 + */ 138 + export async function initPackage(packageSpec: string, options?: InitOptions): Promise<PackageSession> { 139 + const worker = new BundlerWorker(); 140 + try { 141 + const result = await worker.init(packageSpec, options); 142 + return { worker, ...result }; 143 + } catch (error) { 144 + worker.terminate(); 145 + throw error; 146 + } 147 + }
+165
src/npm/worker-protocol.ts
··· 1 + import * as v from 'valibot'; 2 + 3 + // #region option schemas 4 + 5 + const resolveOptionsSchema = v.object({ 6 + installPeers: v.optional(v.boolean()), 7 + }); 8 + 9 + const fetchOptionsSchema = v.object({ 10 + concurrency: v.optional(v.number()), 11 + exclude: v.optional(v.array(v.instance(RegExp))), 12 + }); 13 + 14 + const initOptionsSchema = v.object({ 15 + resolve: v.optional(resolveOptionsSchema), 16 + fetch: v.optional(fetchOptionsSchema), 17 + }); 18 + 19 + const bundleOptionsSchema = v.object({ 20 + rolldown: v.optional( 21 + v.object({ 22 + external: v.optional(v.array(v.string())), 23 + minify: v.optional(v.boolean()), 24 + }), 25 + ), 26 + }); 27 + 28 + // #endregion 29 + 30 + // #region result schemas 31 + 32 + const subpathSchema = v.object({ 33 + subpath: v.string(), 34 + target: v.string(), 35 + isWildcard: v.boolean(), 36 + }); 37 + 38 + const discoveredSubpathsSchema = v.object({ 39 + subpaths: v.array(subpathSchema), 40 + defaultSubpath: v.nullable(v.string()), 41 + }); 42 + 43 + const installedPackageSchema = v.object({ 44 + name: v.string(), 45 + version: v.string(), 46 + size: v.number(), 47 + path: v.string(), 48 + level: v.number(), 49 + installedBy: v.number(), 50 + dependencyCount: v.number(), 51 + description: v.optional(v.string()), 52 + license: v.optional(v.string()), 53 + }); 54 + 55 + const initResultSchema = v.object({ 56 + name: v.string(), 57 + version: v.string(), 58 + subpaths: discoveredSubpathsSchema, 59 + installSize: v.number(), 60 + packages: v.array(installedPackageSchema), 61 + }); 62 + 63 + const bundleChunkSchema = v.object({ 64 + fileName: v.string(), 65 + code: v.string(), 66 + size: v.number(), 67 + gzipSize: v.number(), 68 + brotliSize: v.optional(v.number()), 69 + isEntry: v.boolean(), 70 + exports: v.array(v.string()), 71 + }); 72 + 73 + const bundleResultSchema = v.object({ 74 + chunks: v.array(bundleChunkSchema), 75 + size: v.number(), 76 + gzipSize: v.number(), 77 + brotliSize: v.optional(v.number()), 78 + exports: v.array(v.string()), 79 + }); 80 + 81 + // #endregion 82 + 83 + // #region request schemas (worker parses these) 84 + 85 + const initRequestSchema = v.object({ 86 + id: v.number(), 87 + type: v.literal('init'), 88 + packageSpec: v.string(), 89 + options: v.optional(initOptionsSchema), 90 + }); 91 + 92 + const bundleRequestSchema = v.object({ 93 + id: v.number(), 94 + type: v.literal('bundle'), 95 + subpath: v.string(), 96 + selectedExports: v.nullable(v.array(v.string())), 97 + options: v.optional(bundleOptionsSchema), 98 + }); 99 + 100 + export const workerRequestSchema = v.variant('type', [initRequestSchema, bundleRequestSchema]); 101 + 102 + export type WorkerRequest = v.InferOutput<typeof workerRequestSchema>; 103 + 104 + // #endregion 105 + 106 + // #region response schemas (main thread parses these) 107 + 108 + const initResponseSchema = v.object({ 109 + id: v.number(), 110 + type: v.literal('init'), 111 + result: initResultSchema, 112 + }); 113 + 114 + const bundleResponseSchema = v.object({ 115 + id: v.number(), 116 + type: v.literal('bundle'), 117 + result: bundleResultSchema, 118 + }); 119 + 120 + const errorResponseSchema = v.object({ 121 + id: v.number(), 122 + type: v.literal('error'), 123 + error: v.string(), 124 + }); 125 + 126 + const progressResponseSchema = v.variant('kind', [ 127 + v.object({ 128 + type: v.literal('progress'), 129 + kind: v.literal('resolve'), 130 + name: v.string(), 131 + version: v.string(), 132 + }), 133 + v.object({ 134 + type: v.literal('progress'), 135 + kind: v.literal('fetch'), 136 + current: v.number(), 137 + total: v.number(), 138 + name: v.string(), 139 + }), 140 + v.object({ 141 + type: v.literal('progress'), 142 + kind: v.literal('bundle'), 143 + }), 144 + v.object({ 145 + type: v.literal('progress'), 146 + kind: v.literal('compress'), 147 + }), 148 + ]); 149 + 150 + export const workerResponseSchema = v.variant('type', [ 151 + initResponseSchema, 152 + bundleResponseSchema, 153 + errorResponseSchema, 154 + progressResponseSchema, 155 + ]); 156 + 157 + export type ProgressMessage = v.InferOutput<typeof progressResponseSchema>; 158 + export type WorkerResponse = v.InferOutput<typeof workerResponseSchema>; 159 + 160 + // re-export inferred types for convenience 161 + export type InitOptions = v.InferOutput<typeof initOptionsSchema>; 162 + export type InitResult = v.InferOutput<typeof initResultSchema>; 163 + export type InstalledPackage = v.InferOutput<typeof installedPackageSchema>; 164 + 165 + // #endregion
+253
src/npm/worker.ts
··· 1 + import { memfs } from '@rolldown/browser/experimental'; 2 + import * as v from 'valibot'; 3 + 4 + import { bundlePackage, type BundleOptions } from './bundler'; 5 + import { progress } from './events'; 6 + import { fetchPackagesToVolume } from './fetch'; 7 + import { hoist } from './hoist'; 8 + import { resolve } from './resolve'; 9 + import { discoverSubpaths } from './subpaths'; 10 + import type { HoistedNode, HoistedResult, PackageJson, ResolvedPackage } from './types'; 11 + import { 12 + workerRequestSchema, 13 + type InitOptions, 14 + type InitResult, 15 + type InstalledPackage, 16 + type WorkerResponse, 17 + } from './worker-protocol'; 18 + 19 + const { volume } = memfs!; 20 + 21 + // forward progress events to main thread 22 + progress.listen((msg) => { 23 + self.postMessage(msg satisfies WorkerResponse); 24 + }); 25 + 26 + // #region helpers 27 + 28 + function computePackageLevels(roots: ResolvedPackage[]): Map<string, number> { 29 + const levels = new Map<string, number>(); 30 + const visited = new Set<string>(); 31 + 32 + function walk(pkg: ResolvedPackage, level: number): void { 33 + const key = `${pkg.name}@${pkg.version}`; 34 + 35 + const existingLevel = levels.get(key); 36 + if (existingLevel === undefined || level < existingLevel) { 37 + levels.set(key, level); 38 + } 39 + 40 + if (visited.has(key)) { 41 + return; 42 + } 43 + visited.add(key); 44 + 45 + for (const dep of pkg.dependencies.values()) { 46 + walk(dep, level + 1); 47 + } 48 + 49 + visited.delete(key); 50 + } 51 + 52 + for (const root of roots) { 53 + walk(root, 0); 54 + } 55 + 56 + return levels; 57 + } 58 + 59 + function buildInstalledPackages( 60 + hoisted: HoistedResult, 61 + packageLevels: Map<string, number>, 62 + ): InstalledPackage[] { 63 + const packages: InstalledPackage[] = []; 64 + const installedByCount = new Map<string, number>(); 65 + 66 + function collectPackages(nodes: Map<string, HoistedNode>, basePath: string): void { 67 + for (const node of nodes.values()) { 68 + const path = `${basePath}/${node.name}`; 69 + const key = `${node.name}@${node.version}`; 70 + 71 + packages.push({ 72 + name: node.name, 73 + version: node.version, 74 + size: node.unpackedSize ?? 0, 75 + path, 76 + level: packageLevels.get(key) ?? 0, 77 + installedBy: 0, 78 + dependencyCount: node.dependencyCount, 79 + description: node.description, 80 + license: node.license, 81 + }); 82 + 83 + for (const nested of node.nested.values()) { 84 + const nestedKey = `${nested.name}@${nested.version}`; 85 + installedByCount.set(nestedKey, (installedByCount.get(nestedKey) ?? 0) + 1); 86 + } 87 + 88 + if (node.nested.size > 0) { 89 + collectPackages(node.nested, `${path}/node_modules`); 90 + } 91 + } 92 + } 93 + 94 + for (const node of hoisted.root.values()) { 95 + const key = `${node.name}@${node.version}`; 96 + installedByCount.set(key, (installedByCount.get(key) ?? 0) + 1); 97 + } 98 + 99 + collectPackages(hoisted.root, 'node_modules'); 100 + 101 + for (const pkg of packages) { 102 + const key = `${pkg.name}@${pkg.version}`; 103 + pkg.installedBy = installedByCount.get(key) ?? 0; 104 + } 105 + 106 + return packages; 107 + } 108 + 109 + // #endregion 110 + 111 + // #region state 112 + 113 + let packageName: string | null = null; 114 + let initResult: InitResult | null = null; 115 + 116 + let bundleInProgress = false; 117 + let pendingBundleRequest: { 118 + id: number; 119 + subpath: string; 120 + selectedExports: string[] | null; 121 + options: BundleOptions; 122 + } | null = null; 123 + 124 + // #endregion 125 + 126 + // #region handlers 127 + 128 + async function handleInit(id: number, packageSpec: string, options: InitOptions = {}): Promise<void> { 129 + // if already initialized, return cached result 130 + if (initResult !== null) { 131 + self.postMessage({ id, type: 'init', result: initResult } satisfies WorkerResponse); 132 + return; 133 + } 134 + 135 + try { 136 + volume.reset(); 137 + 138 + const resolution = await resolve([packageSpec], options.resolve); 139 + const hoisted = hoist(resolution.roots); 140 + 141 + await fetchPackagesToVolume(hoisted, volume, options.fetch); 142 + 143 + const mainPackage = resolution.roots[0]; 144 + const pkgJsonPath = `/node_modules/${mainPackage.name}/package.json`; 145 + const pkgJsonContent = volume.readFileSync(pkgJsonPath, 'utf8') as string; 146 + const manifest = JSON.parse(pkgJsonContent) as PackageJson; 147 + 148 + packageName = mainPackage.name; 149 + 150 + const subpaths = discoverSubpaths(manifest, volume); 151 + 152 + const packageLevels = computePackageLevels(resolution.roots); 153 + const packages = buildInstalledPackages(hoisted, packageLevels); 154 + const installSize = packages.reduce((sum, pkg) => sum + pkg.size, 0); 155 + 156 + initResult = { 157 + name: mainPackage.name, 158 + version: mainPackage.version, 159 + subpaths, 160 + installSize, 161 + packages, 162 + }; 163 + 164 + self.postMessage({ id, type: 'init', result: initResult } satisfies WorkerResponse); 165 + } catch (error) { 166 + self.postMessage({ id, type: 'error', error: String(error) } satisfies WorkerResponse); 167 + } 168 + } 169 + 170 + async function handleBundle( 171 + id: number, 172 + subpath: string, 173 + selectedExports: string[] | null, 174 + options: BundleOptions = {}, 175 + ): Promise<void> { 176 + if (!packageName) { 177 + self.postMessage({ 178 + id, 179 + type: 'error', 180 + error: 'not initialized - call init() first', 181 + } satisfies WorkerResponse); 182 + return; 183 + } 184 + 185 + // if a bundle is in progress, queue this one (replacing any previous pending) 186 + if (bundleInProgress) { 187 + // reject the previous pending request if any 188 + if (pendingBundleRequest) { 189 + self.postMessage({ 190 + id: pendingBundleRequest.id, 191 + type: 'error', 192 + error: 'Superseded by newer request', 193 + } satisfies WorkerResponse); 194 + } 195 + pendingBundleRequest = { id, subpath, selectedExports, options }; 196 + return; 197 + } 198 + 199 + await processBundleRequest(id, subpath, selectedExports, options); 200 + } 201 + 202 + async function processBundleRequest( 203 + id: number, 204 + subpath: string, 205 + selectedExports: string[] | null, 206 + options: BundleOptions, 207 + ): Promise<void> { 208 + bundleInProgress = true; 209 + 210 + try { 211 + const result = await bundlePackage(packageName!, subpath, selectedExports, options); 212 + self.postMessage({ id, type: 'bundle', result } satisfies WorkerResponse); 213 + } catch (error) { 214 + self.postMessage({ id, type: 'error', error: String(error) } satisfies WorkerResponse); 215 + } finally { 216 + bundleInProgress = false; 217 + 218 + // process pending request if any 219 + if (pendingBundleRequest) { 220 + const pending = pendingBundleRequest; 221 + pendingBundleRequest = null; 222 + await processBundleRequest(pending.id, pending.subpath, pending.selectedExports, pending.options); 223 + } 224 + } 225 + } 226 + 227 + // #endregion 228 + 229 + // #region message handler 230 + 231 + self.onmessage = (event: MessageEvent<unknown>) => { 232 + const parsed = v.safeParse(workerRequestSchema, event.data); 233 + if (!parsed.success) { 234 + console.error('[worker] invalid request:', parsed.issues); 235 + return; 236 + } 237 + 238 + const request = parsed.output; 239 + 240 + switch (request.type) { 241 + case 'init': 242 + handleInit(request.id, request.packageSpec, request.options); 243 + break; 244 + case 'bundle': 245 + handleBundle(request.id, request.subpath, request.selectedExports, request.options); 246 + break; 247 + } 248 + }; 249 + 250 + // signal to main thread that we're ready 251 + self.postMessage({ type: 'ready' }); 252 + 253 + // #endregion
+78
src/primitives/button.tsx
··· 1 + import { splitProps, type JSX } from 'solid-js'; 2 + 3 + import { tw } from '../lib/classes'; 4 + 5 + // #region types 6 + 7 + export type ButtonAppearance = 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent'; 8 + export type ButtonSize = 'small' | 'medium' | 'large'; 9 + 10 + export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> { 11 + /** button content */ 12 + children: JSX.Element; 13 + /** visual style variant */ 14 + appearance?: ButtonAppearance; 15 + /** size variant */ 16 + size?: ButtonSize; 17 + /** icon-only button (square aspect ratio) */ 18 + iconOnly?: boolean; 19 + } 20 + 21 + // #endregion 22 + 23 + // #region styles 24 + 25 + const baseStyles = tw`inline-flex items-center justify-center gap-1.5 rounded-md font-medium outline-2 -outline-offset-2 outline-transparent transition duration-100 select-none`; 26 + 27 + const sizeStyles: Record<ButtonSize, string> = { 28 + // padding 3px 8px, fontSize 12px, fontWeight 400, minHeight 24px 29 + small: tw`min-h-6 min-w-16 px-2 py-0.5 text-base-200 font-normal`, 30 + // padding 5px 12px, fontSize 14px, fontWeight 600, minHeight 32px 31 + medium: tw`min-h-8 min-w-24 px-3 py-1 text-base-300`, 32 + // padding 8px 16px, fontSize 16px, fontWeight 600, minHeight 40px 33 + large: tw`min-h-10 min-w-24 px-4 py-2 text-base-400`, 34 + }; 35 + 36 + const iconOnlySizeStyles: Record<ButtonSize, string> = { 37 + small: tw`min-h-6 min-w-6 p-0.5`, 38 + medium: tw`min-h-8 min-w-8 p-1`, 39 + large: tw`min-h-10 min-w-10 p-2`, 40 + }; 41 + 42 + const appearanceStyles: Record<ButtonAppearance, string> = { 43 + // default: neutral background with border 44 + secondary: tw`border border-neutral-stroke-1 bg-neutral-background-1 text-neutral-foreground-1 hover:border-neutral-stroke-1-hover hover:bg-neutral-background-1-hover focus-visible:outline-compound-brand-stroke active:border-neutral-stroke-1-pressed active:bg-neutral-background-1-pressed disabled:cursor-not-allowed disabled:border-neutral-stroke-disabled disabled:bg-neutral-background-disabled disabled:text-neutral-foreground-disabled`, 45 + // brand background, white text 46 + primary: tw`border border-transparent bg-compound-brand-background text-neutral-foreground-on-brand hover:bg-compound-brand-background-hover focus-visible:outline-compound-brand-stroke active:bg-compound-brand-background-pressed disabled:cursor-not-allowed disabled:bg-neutral-background-disabled disabled:text-neutral-foreground-disabled`, 47 + // transparent background, visible border 48 + outline: tw`border border-neutral-stroke-1 bg-transparent text-neutral-foreground-1 hover:bg-subtle-background-hover focus-visible:outline-compound-brand-stroke active:bg-subtle-background-pressed disabled:cursor-not-allowed disabled:border-neutral-stroke-disabled disabled:text-neutral-foreground-disabled`, 49 + // subtle background, no border 50 + subtle: tw`border border-transparent bg-subtle-background text-neutral-foreground-2 hover:bg-subtle-background-hover focus-visible:outline-compound-brand-stroke active:bg-subtle-background-pressed disabled:cursor-not-allowed disabled:bg-transparent disabled:text-neutral-foreground-disabled`, 51 + // fully transparent 52 + transparent: tw`border border-transparent bg-transparent text-neutral-foreground-2 hover:bg-transparent-background-hover focus-visible:outline-compound-brand-stroke active:bg-transparent-background-pressed disabled:cursor-not-allowed disabled:text-neutral-foreground-disabled`, 53 + }; 54 + 55 + // #endregion 56 + 57 + // #region component 58 + 59 + const Button = (props: ButtonProps) => { 60 + const [local, rest] = splitProps(props, ['children', 'class', 'appearance', 'size', 'iconOnly']); 61 + 62 + const appearance = () => local.appearance ?? 'secondary'; 63 + const size = () => local.size ?? 'medium'; 64 + 65 + return ( 66 + <button 67 + type="button" 68 + class={`${baseStyles} ${local.iconOnly ? iconOnlySizeStyles[size()] : sizeStyles[size()]} ${appearanceStyles[appearance()]} ${local.class ?? ''}`} 69 + {...rest} 70 + > 71 + {local.children} 72 + </button> 73 + ); 74 + }; 75 + 76 + export default Button; 77 + 78 + // #endregion
+10
src/primitives/dropdown.tsx
··· 1 + export { default as Root } from './dropdown/dropdown-root'; 2 + export { default as Trigger } from './dropdown/dropdown-trigger'; 3 + export { default as Listbox } from './dropdown/dropdown-listbox'; 4 + export { default as Option } from './dropdown/dropdown-option'; 5 + 6 + export type { DropdownRootProps } from './dropdown/dropdown-root'; 7 + export type { DropdownTriggerProps, DropdownSize, DropdownAppearance } from './dropdown/dropdown-trigger'; 8 + export type { DropdownListboxProps } from './dropdown/dropdown-listbox'; 9 + export type { DropdownOptionProps } from './dropdown/dropdown-option'; 10 + export type { DropdownContextValue } from './dropdown/context';
+54
src/primitives/dropdown/context.tsx
··· 1 + import { createContext, useContext, type Accessor } from 'solid-js'; 2 + 3 + import type { ActiveDescendantController } from '../lib/create-active-descendant'; 4 + 5 + // #region types 6 + 7 + export interface DropdownContextValue { 8 + /** whether the listbox is open */ 9 + open: Accessor<boolean>; 10 + /** set the listbox open state */ 11 + setOpen: (open: boolean) => void; 12 + /** the trigger element ref */ 13 + triggerRef: Accessor<HTMLElement | null>; 14 + /** set the trigger element ref */ 15 + setTriggerRef: (el: HTMLElement | null) => void; 16 + /** unique ID for the trigger */ 17 + triggerId: string; 18 + /** unique ID for the listbox */ 19 + listboxId: string; 20 + /** currently selected value */ 21 + selectedValue: Accessor<string | undefined>; 22 + /** select an option by value */ 23 + selectOption: (value: string, label: string) => void; 24 + /** active descendant controller for keyboard navigation */ 25 + activeDescendant: ActiveDescendantController; 26 + /** the listbox element ref for scrolling */ 27 + listboxRef: Accessor<HTMLElement | null>; 28 + /** set the listbox element ref */ 29 + setListboxRef: (el: HTMLElement | null) => void; 30 + /** map from option ID to option value */ 31 + getOptionValue: (id: string) => string | undefined; 32 + /** register option value for an ID */ 33 + registerOptionValue: (id: string, value: string) => void; 34 + /** unregister option value for an ID */ 35 + unregisterOptionValue: (id: string) => void; 36 + } 37 + 38 + // #endregion 39 + 40 + // #region context 41 + 42 + const DropdownContext = createContext<DropdownContextValue>(); 43 + 44 + export const DropdownProvider = DropdownContext.Provider; 45 + 46 + export function useDropdownContext(): DropdownContextValue { 47 + const ctx = useContext(DropdownContext); 48 + if (!ctx) { 49 + throw new Error('Dropdown components must be used within a Dropdown.Root'); 50 + } 51 + return ctx; 52 + } 53 + 54 + // #endregion
+105
src/primitives/dropdown/dropdown-listbox.tsx
··· 1 + import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'; 2 + import { createEffect, onCleanup, onMount, type JSX } from 'solid-js'; 3 + import { Portal } from 'solid-js/web'; 4 + 5 + import { useDropdownContext } from './context'; 6 + 7 + // #region types 8 + 9 + export interface DropdownListboxProps { 10 + /** listbox content (DropdownOption components) */ 11 + children: JSX.Element; 12 + } 13 + 14 + // #endregion 15 + 16 + // #region component 17 + 18 + const DropdownListbox = (props: DropdownListboxProps) => { 19 + const ctx = useDropdownContext(); 20 + 21 + return ( 22 + <> 23 + {ctx.open() && ( 24 + <Portal> 25 + <div 26 + ref={(el) => { 27 + ctx.setListboxRef(el); 28 + 29 + onMount(() => { 30 + createEffect(() => { 31 + const trigger = ctx.triggerRef(); 32 + if (!trigger) { 33 + return; 34 + } 35 + 36 + const updatePosition = async () => { 37 + const { x, y } = await computePosition(trigger, el, { 38 + placement: 'bottom-start', 39 + strategy: 'absolute', 40 + middleware: [offset(4), flip(), shift({ padding: 8 })], 41 + }); 42 + 43 + Object.assign(el.style, { 44 + position: 'absolute', 45 + left: `${x}px`, 46 + top: `${y}px`, 47 + minWidth: `${trigger.offsetWidth}px`, 48 + }); 49 + }; 50 + 51 + onCleanup(autoUpdate(trigger, el, updatePosition)); 52 + }); 53 + 54 + { 55 + // handle click outside to close 56 + const handleClickOutside = (ev: MouseEvent) => { 57 + const currentTrigger = ctx.triggerRef(); 58 + if ( 59 + !el.contains(ev.target as Node) && 60 + currentTrigger && 61 + !currentTrigger.contains(ev.target as Node) 62 + ) { 63 + ctx.setOpen(false); 64 + } 65 + }; 66 + 67 + // handle escape key to close 68 + const handleKeyDown = (ev: KeyboardEvent) => { 69 + if (ev.key === 'Escape') { 70 + ev.preventDefault(); 71 + ctx.setOpen(false); 72 + ctx.triggerRef()?.focus(); 73 + } 74 + }; 75 + 76 + document.addEventListener('mousedown', handleClickOutside); 77 + document.addEventListener('keydown', handleKeyDown); 78 + 79 + onCleanup(() => { 80 + document.removeEventListener('mousedown', handleClickOutside); 81 + document.removeEventListener('keydown', handleKeyDown); 82 + }); 83 + } 84 + }); 85 + 86 + onCleanup(() => { 87 + ctx.setListboxRef(null); 88 + }); 89 + }} 90 + id={ctx.listboxId} 91 + role="listbox" 92 + aria-labelledby={ctx.triggerId} 93 + class="fixed z-50 box-border flex max-h-80 min-w-34.5 flex-col gap-0.5 overflow-y-auto overscroll-contain rounded-md border border-transparent-stroke bg-neutral-background-1 p-1 shadow-16" 94 + > 95 + {props.children} 96 + </div> 97 + </Portal> 98 + )} 99 + </> 100 + ); 101 + }; 102 + 103 + export default DropdownListbox; 104 + 105 + // #endregion
+107
src/primitives/dropdown/dropdown-option.tsx
··· 1 + import { createEffect, createUniqueId, onCleanup, Show, type JSX } from 'solid-js'; 2 + 3 + import { LucideCheck } from '../../icons/lucide'; 4 + import { modality } from '../../lib/modality'; 5 + 6 + import { useDropdownContext } from './context'; 7 + 8 + // #region types 9 + 10 + export interface DropdownOptionProps { 11 + /** option content (display label) */ 12 + children: JSX.Element; 13 + /** option value (defaults to children if string) */ 14 + value: string; 15 + /** whether the option is disabled */ 16 + disabled?: boolean; 17 + } 18 + 19 + // #endregion 20 + 21 + // #region component 22 + 23 + const DropdownOption = (props: DropdownOptionProps) => { 24 + const ctx = useDropdownContext(); 25 + const optionId = createUniqueId(); 26 + 27 + const isSelected = () => ctx.selectedValue() === props.value; 28 + const isActive = () => ctx.activeDescendant.activeId() === optionId; 29 + 30 + const handleClick = () => { 31 + if (props.disabled) { 32 + return; 33 + } 34 + const label = typeof props.children === 'string' ? props.children : props.value; 35 + ctx.selectOption(props.value, label); 36 + }; 37 + 38 + const handleMouseMove = () => { 39 + if (modality() === 'pointer' && !props.disabled) { 40 + ctx.activeDescendant.setActiveId(optionId); 41 + } 42 + }; 43 + 44 + return ( 45 + <div 46 + ref={(el) => { 47 + // register option with active descendant controller 48 + const textValue = typeof props.children === 'string' ? props.children : props.value; 49 + ctx.activeDescendant.register({ 50 + id: optionId, 51 + disabled: props.disabled, 52 + textValue, 53 + }); 54 + ctx.registerOptionValue(optionId, props.value); 55 + 56 + onCleanup(() => { 57 + ctx.activeDescendant.unregister(optionId); 58 + ctx.unregisterOptionValue(optionId); 59 + }); 60 + 61 + // scroll into view when active changes via keyboard 62 + createEffect(() => { 63 + const listbox = ctx.listboxRef(); 64 + if (isActive() && modality() === 'keyboard' && listbox) { 65 + const padding = 4; 66 + const listboxRect = listbox.getBoundingClientRect(); 67 + const optionRect = el.getBoundingClientRect(); 68 + 69 + if (optionRect.top < listboxRect.top + padding) { 70 + listbox.scrollTop -= listboxRect.top + padding - optionRect.top; 71 + } else if (optionRect.bottom > listboxRect.bottom - padding) { 72 + listbox.scrollTop += optionRect.bottom - (listboxRect.bottom - padding); 73 + } 74 + } 75 + }); 76 + }} 77 + id={optionId} 78 + role="option" 79 + tabIndex={-1} 80 + aria-selected={isSelected()} 81 + aria-disabled={props.disabled} 82 + onClick={handleClick} 83 + onMouseMove={handleMouseMove} 84 + class="box-border flex min-h-8 items-center gap-1 rounded-md px-2 py-1.5 text-base-300 outline-none select-none" 85 + classList={{ 86 + 'text-neutral-foreground-disabled cursor-not-allowed': props.disabled, 87 + 'text-neutral-foreground-2 hover:text-neutral-foreground-2-hover active:bg-neutral-background-1-pressed': 88 + !props.disabled, 89 + 'bg-neutral-background-1-hover': isActive() && !props.disabled, 90 + }} 91 + > 92 + {/* checkmark for selected state */} 93 + <span class="flex size-4 shrink-0 items-center justify-center"> 94 + <Show when={isSelected()}> 95 + <LucideCheck class="size-4" /> 96 + </Show> 97 + </span> 98 + 99 + {/* content */} 100 + <span class="grow">{props.children}</span> 101 + </div> 102 + ); 103 + }; 104 + 105 + export default DropdownOption; 106 + 107 + // #endregion
+111
src/primitives/dropdown/dropdown-root.tsx
··· 1 + import { createEffect, createMemo, createSignal, createUniqueId, type JSX } from 'solid-js'; 2 + 3 + import { createActiveDescendant } from '../lib/create-active-descendant'; 4 + 5 + import { DropdownProvider, type DropdownContextValue } from './context'; 6 + 7 + // #region types 8 + 9 + export interface DropdownRootProps { 10 + /** dropdown content (DropdownTrigger and DropdownListbox) */ 11 + children: JSX.Element; 12 + /** controlled selected value */ 13 + value?: string; 14 + /** default selected value for uncontrolled usage */ 15 + defaultValue?: string; 16 + /** callback when selection changes */ 17 + onValueChange?: (value: string) => void; 18 + /** controlled open state */ 19 + open?: boolean; 20 + /** default open state for uncontrolled usage */ 21 + defaultOpen?: boolean; 22 + /** callback when open state changes */ 23 + onOpenChange?: (open: boolean) => void; 24 + } 25 + 26 + // #endregion 27 + 28 + // #region component 29 + 30 + const DropdownRoot = (props: DropdownRootProps) => { 31 + const triggerId = createUniqueId(); 32 + const listboxId = createUniqueId(); 33 + 34 + const [triggerRef, setTriggerRef] = createSignal<HTMLElement | null>(null); 35 + const [listboxRef, setListboxRef] = createSignal<HTMLElement | null>(null); 36 + 37 + // open state - support both controlled and uncontrolled 38 + const [internalOpen, setInternalOpen] = createSignal(props.defaultOpen ?? false); 39 + const open = () => props.open ?? internalOpen(); 40 + const setOpen = (value: boolean) => { 41 + if (props.open === undefined) { 42 + setInternalOpen(value); 43 + } 44 + props.onOpenChange?.(value); 45 + }; 46 + 47 + // selected value - support both controlled and uncontrolled 48 + const [internalValue, setInternalValue] = createSignal(props.defaultValue); 49 + const selectedValue = createMemo(() => props.value ?? internalValue()); 50 + 51 + const selectOption = (value: string, _label: string) => { 52 + if (props.value === undefined) { 53 + setInternalValue(value); 54 + } 55 + props.onValueChange?.(value); 56 + setOpen(false); 57 + }; 58 + 59 + // active descendant for keyboard navigation 60 + const activeDescendant = createActiveDescendant(); 61 + 62 + // map option IDs to values 63 + const optionValueMap = new Map<string, string>(); 64 + 65 + const getOptionValue = (id: string) => optionValueMap.get(id); 66 + const registerOptionValue = (id: string, value: string) => optionValueMap.set(id, value); 67 + const unregisterOptionValue = (id: string) => optionValueMap.delete(id); 68 + 69 + // when opening, set active to selected value or first item 70 + createEffect(() => { 71 + if (open()) { 72 + const selected = selectedValue(); 73 + if (selected) { 74 + // find the option ID for the selected value 75 + for (const [id, value] of optionValueMap) { 76 + if (value === selected) { 77 + activeDescendant.setActiveId(id); 78 + return; 79 + } 80 + } 81 + } 82 + // no selection or not found, activate first 83 + activeDescendant.first(); 84 + } else { 85 + activeDescendant.clear(); 86 + } 87 + }); 88 + 89 + const context: DropdownContextValue = { 90 + open, 91 + setOpen, 92 + triggerRef, 93 + setTriggerRef, 94 + triggerId, 95 + listboxId, 96 + selectedValue, 97 + selectOption, 98 + activeDescendant, 99 + listboxRef, 100 + setListboxRef, 101 + getOptionValue, 102 + registerOptionValue, 103 + unregisterOptionValue, 104 + }; 105 + 106 + return <DropdownProvider value={context}>{props.children}</DropdownProvider>; 107 + }; 108 + 109 + export default DropdownRoot; 110 + 111 + // #endregion
+178
src/primitives/dropdown/dropdown-trigger.tsx
··· 1 + import type { JSX } from 'solid-js'; 2 + 3 + import { LucideChevronDown } from '../../icons/lucide'; 4 + import { tw } from '../../lib/classes'; 5 + import { useFieldContext } from '../field'; 6 + 7 + import { useDropdownContext } from './context'; 8 + 9 + // #region types 10 + 11 + export type DropdownSize = 'small' | 'medium' | 'large'; 12 + export type DropdownAppearance = 'outline' | 'underline'; 13 + 14 + export interface DropdownTriggerProps { 15 + /** placeholder text when no value is selected */ 16 + placeholder?: string; 17 + /** display text for the selected value (if different from value) */ 18 + children?: JSX.Element; 19 + /** size variant */ 20 + size?: DropdownSize; 21 + /** visual style variant */ 22 + appearance?: DropdownAppearance; 23 + /** whether the dropdown is disabled */ 24 + disabled?: boolean; 25 + /** additional CSS classes */ 26 + class?: string; 27 + } 28 + 29 + // #endregion 30 + 31 + // #region styles 32 + 33 + const rootBaseStyles = tw`inline-flex w-full items-center justify-between bg-neutral-background-1 align-middle outline-2 -outline-offset-2 outline-transparent transition duration-100 select-none`; 34 + 35 + const rootSizeStyles: Record<DropdownSize, string> = { 36 + small: tw`min-h-6 gap-2 px-2 text-base-200`, 37 + medium: tw`min-h-8 gap-2 px-2.5 text-base-300`, 38 + large: tw`min-h-10 gap-2 px-3 text-base-400`, 39 + }; 40 + 41 + const rootAppearanceStyles: Record<DropdownAppearance, string> = { 42 + outline: tw`rounded-md border border-neutral-stroke-1 hover:border-neutral-stroke-1-hover focus-visible:outline-compound-brand-stroke`, 43 + underline: tw`rounded-none border-b border-neutral-stroke-1 hover:border-neutral-stroke-1-hover focus-visible:outline-compound-brand-stroke`, 44 + }; 45 + 46 + const rootDisabledStyles = tw`cursor-not-allowed border-neutral-stroke-disabled bg-transparent text-neutral-foreground-disabled hover:border-neutral-stroke-disabled`; 47 + 48 + const rootInvalidStyles = tw`border-status-danger-border-2 hover:border-status-danger-border-2 focus-visible:outline-status-danger-border-2`; 49 + 50 + const iconSizeStyles: Record<DropdownSize, string> = { 51 + small: tw`size-3`, 52 + medium: tw`size-4`, 53 + large: tw`size-5`, 54 + }; 55 + 56 + // #endregion 57 + 58 + // #region component 59 + 60 + const DropdownTrigger = (props: DropdownTriggerProps) => { 61 + const ctx = useDropdownContext(); 62 + const fieldCtx = useFieldContext(); 63 + 64 + const size = () => props.size ?? 'medium'; 65 + const appearance = () => props.appearance ?? 'outline'; 66 + 67 + // field context integration 68 + const triggerId = () => fieldCtx?.controlId ?? ctx.triggerId; 69 + const isInvalid = () => fieldCtx?.validationState() === 'error'; 70 + const isRequired = () => fieldCtx?.required() ?? false; 71 + 72 + const ariaDescribedBy = () => { 73 + if (!fieldCtx) { 74 + return undefined; 75 + } 76 + return `${fieldCtx.validationMessageId} ${fieldCtx.hintId}`; 77 + }; 78 + 79 + const handleClick = () => { 80 + if (props.disabled) { 81 + return; 82 + } 83 + ctx.setOpen(!ctx.open()); 84 + }; 85 + 86 + const handleKeyDown = (ev: KeyboardEvent) => { 87 + if (props.disabled) { 88 + return; 89 + } 90 + 91 + if (!ctx.open()) { 92 + // when closed, open on ArrowDown/ArrowUp/Enter/Space 93 + if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(ev.key)) { 94 + ev.preventDefault(); 95 + ctx.setOpen(true); 96 + } 97 + return; 98 + } 99 + 100 + // when open, handle navigation 101 + switch (ev.key) { 102 + case 'ArrowDown': { 103 + ev.preventDefault(); 104 + ctx.activeDescendant.next(); 105 + break; 106 + } 107 + case 'ArrowUp': { 108 + ev.preventDefault(); 109 + ctx.activeDescendant.prev(); 110 + break; 111 + } 112 + case 'Home': { 113 + ev.preventDefault(); 114 + ctx.activeDescendant.first(); 115 + break; 116 + } 117 + case 'End': { 118 + ev.preventDefault(); 119 + ctx.activeDescendant.last(); 120 + break; 121 + } 122 + case 'Enter': 123 + case ' ': { 124 + ev.preventDefault(); 125 + const activeId = ctx.activeDescendant.activeId(); 126 + if (activeId) { 127 + const value = ctx.getOptionValue(activeId); 128 + if (value !== undefined) { 129 + ctx.selectOption(value, value); 130 + } 131 + } 132 + break; 133 + } 134 + case 'Escape': { 135 + ev.preventDefault(); 136 + ctx.setOpen(false); 137 + break; 138 + } 139 + default: { 140 + // character search (typeahead) 141 + if (ev.key.length === 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey) { 142 + ctx.activeDescendant.search(ev.key); 143 + } 144 + } 145 + } 146 + }; 147 + 148 + const hasValue = () => ctx.selectedValue() !== undefined; 149 + 150 + return ( 151 + <button 152 + ref={(el) => ctx.setTriggerRef(el)} 153 + type="button" 154 + id={triggerId()} 155 + role="combobox" 156 + aria-expanded={ctx.open()} 157 + aria-controls={ctx.open() ? ctx.listboxId : undefined} 158 + aria-haspopup="listbox" 159 + aria-activedescendant={ctx.open() ? (ctx.activeDescendant.activeId() ?? undefined) : undefined} 160 + aria-describedby={ariaDescribedBy()} 161 + aria-invalid={isInvalid() || undefined} 162 + aria-required={isRequired() || undefined} 163 + disabled={props.disabled} 164 + onClick={handleClick} 165 + onKeyDown={handleKeyDown} 166 + class={`${rootBaseStyles} ${rootSizeStyles[size()]} ${rootAppearanceStyles[appearance()]} ${props.disabled ? rootDisabledStyles : isInvalid() ? rootInvalidStyles : ''} ${props.class ?? ''}`} 167 + > 168 + <span class={hasValue() ? 'text-neutral-foreground-1' : 'text-neutral-foreground-4'}> 169 + {props.children ?? props.placeholder} 170 + </span> 171 + <LucideChevronDown class={`${iconSizeStyles[size()]} shrink-0 text-neutral-foreground-3`} /> 172 + </button> 173 + ); 174 + }; 175 + 176 + export default DropdownTrigger; 177 + 178 + // #endregion
+5
src/primitives/field.tsx
··· 1 + export { default as Root } from './field/field-root'; 2 + 3 + export type { FieldRootProps, FieldSize, FieldOrientation } from './field/field-root'; 4 + export type { FieldContextValue, FieldValidationState } from './field/context'; 5 + export { useFieldContext } from './field/context';
+36
src/primitives/field/context.tsx
··· 1 + import { createContext, useContext, type Accessor } from 'solid-js'; 2 + 3 + // #region types 4 + 5 + export type FieldValidationState = 'error' | 'warning' | 'success' | 'none'; 6 + 7 + export interface FieldContextValue { 8 + /** unique ID for the control element */ 9 + controlId: string; 10 + /** unique ID for the validation message element */ 11 + validationMessageId: string; 12 + /** unique ID for the hint element */ 13 + hintId: string; 14 + /** whether the field is required */ 15 + required: Accessor<boolean>; 16 + /** current validation state */ 17 + validationState: Accessor<FieldValidationState>; 18 + } 19 + 20 + // #endregion 21 + 22 + // #region context 23 + 24 + const FieldContext = createContext<FieldContextValue>(); 25 + 26 + export const FieldProvider = FieldContext.Provider; 27 + 28 + /** 29 + * returns field context if inside a Field, undefined otherwise. 30 + * use this in form controls to integrate with Field. 31 + */ 32 + export function useFieldContext(): FieldContextValue | undefined { 33 + return useContext(FieldContext); 34 + } 35 + 36 + // #endregion
+172
src/primitives/field/field-root.tsx
··· 1 + import { createMemo, createUniqueId, Show, type JSX } from 'solid-js'; 2 + 3 + import { 4 + CentralCircleCheckSolid, 5 + CentralCircleXSolid, 6 + CentralExclamationTriangleSolid, 7 + } from '../../icons/central'; 8 + import { tw } from '../../lib/classes'; 9 + 10 + import { FieldProvider, type FieldContextValue, type FieldValidationState } from './context'; 11 + 12 + // #region types 13 + 14 + export type FieldSize = 'small' | 'medium' | 'large'; 15 + export type FieldOrientation = 'vertical' | 'horizontal'; 16 + 17 + export interface FieldRootProps { 18 + /** field content (the form control) */ 19 + children: JSX.Element; 20 + /** label text for the field */ 21 + label?: string; 22 + /** hint text displayed below the control */ 23 + hint?: string; 24 + /** validation message displayed below the control */ 25 + validationMessage?: string; 26 + /** validation state affects icon and colors */ 27 + validationState?: FieldValidationState; 28 + /** marks the field as required, adds asterisk to label */ 29 + required?: boolean; 30 + /** size variant affects label text size */ 31 + size?: FieldSize; 32 + /** layout orientation */ 33 + orientation?: FieldOrientation; 34 + } 35 + 36 + // #endregion 37 + 38 + // #region styles 39 + 40 + const rootBaseStyles = tw`grid`; 41 + 42 + const rootOrientationStyles: Record<FieldOrientation, string> = { 43 + vertical: '', 44 + horizontal: tw`grid-cols-[33%_1fr]`, 45 + }; 46 + 47 + const labelBaseStyles = tw`max-w-max text-neutral-foreground-1`; 48 + 49 + const labelSizeStyles: Record<FieldSize, string> = { 50 + small: tw`text-base-200`, 51 + medium: tw`text-base-300`, 52 + large: tw`text-base-400`, 53 + }; 54 + 55 + const labelVerticalStyles: Record<FieldSize, string> = { 56 + small: tw`mb-0.5 py-0.5`, 57 + medium: tw`mb-0.5 py-0.5`, 58 + large: tw`mb-1 py-px`, 59 + }; 60 + 61 + const labelHorizontalStyles: Record<FieldSize, string> = { 62 + small: tw`row-span-full mr-3 py-1`, 63 + medium: tw`row-span-full mr-3 py-1`, 64 + large: tw`row-span-full mr-3 py-2`, 65 + }; 66 + 67 + const secondaryTextStyles = tw`mt-0.5 text-base-100 text-neutral-foreground-3`; 68 + 69 + const validationMessageStyles: Record<FieldValidationState, string> = { 70 + error: tw`text-status-danger-foreground-1`, 71 + warning: tw`text-neutral-foreground-3`, 72 + success: tw`text-neutral-foreground-3`, 73 + none: tw`text-neutral-foreground-3`, 74 + }; 75 + 76 + const validationIconStyles: Record<Exclude<FieldValidationState, 'none'>, string> = { 77 + error: tw`text-status-danger-foreground-1`, 78 + warning: tw`text-status-warning-foreground-1`, 79 + success: tw`text-status-success-foreground-1`, 80 + }; 81 + 82 + // #endregion 83 + 84 + // #region component 85 + 86 + const FieldRoot = (props: FieldRootProps) => { 87 + const baseId = createUniqueId(); 88 + const controlId = `field-${baseId}-control`; 89 + const hintId = `field-${baseId}-hint`; 90 + const validationMessageId = `field-${baseId}-validation`; 91 + 92 + const size = () => props.size ?? 'medium'; 93 + const orientation = () => props.orientation ?? 'vertical'; 94 + const required = () => props.required ?? false; 95 + 96 + const validationState = createMemo((): FieldValidationState => { 97 + if (props.validationState) { 98 + return props.validationState; 99 + } 100 + // default to error if validationMessage is set 101 + return props.validationMessage ? 'error' : 'none'; 102 + }); 103 + 104 + const context: FieldContextValue = { 105 + controlId, 106 + validationMessageId, 107 + hintId, 108 + required, 109 + validationState, 110 + }; 111 + 112 + const ValidationIcon = () => { 113 + const state = validationState(); 114 + if (state === 'none') { 115 + return null; 116 + } 117 + 118 + const iconClass = `inline-block size-3 mr-1 align-[-1px] ${validationIconStyles[state]}`; 119 + 120 + switch (state) { 121 + case 'error': 122 + return <CentralCircleXSolid class={iconClass} />; 123 + case 'warning': 124 + return <CentralExclamationTriangleSolid class={iconClass} />; 125 + case 'success': 126 + return <CentralCircleCheckSolid class={iconClass} />; 127 + } 128 + }; 129 + 130 + return ( 131 + <FieldProvider value={context}> 132 + <div class={`${rootBaseStyles} ${rootOrientationStyles[orientation()]}`}> 133 + {/* label */} 134 + <Show when={props.label}> 135 + <label 136 + for={controlId} 137 + class={`${labelBaseStyles} ${labelSizeStyles[size()]} ${orientation() === 'vertical' ? labelVerticalStyles[size()] : labelHorizontalStyles[size()]}`} 138 + > 139 + {props.label} 140 + {props.required && <span class="text-status-danger-foreground-1">*</span>} 141 + </label> 142 + </Show> 143 + 144 + {/* control */} 145 + {props.children} 146 + 147 + {/* validation message */} 148 + <Show when={props.validationMessage}> 149 + <div 150 + id={validationMessageId} 151 + role={validationState() === 'error' || validationState() === 'warning' ? 'alert' : undefined} 152 + class={`${secondaryTextStyles} ${validationMessageStyles[validationState()]}`} 153 + > 154 + <ValidationIcon /> 155 + {props.validationMessage} 156 + </div> 157 + </Show> 158 + 159 + {/* hint */} 160 + <Show when={props.hint}> 161 + <div id={hintId} class={secondaryTextStyles}> 162 + {props.hint} 163 + </div> 164 + </Show> 165 + </div> 166 + </FieldProvider> 167 + ); 168 + }; 169 + 170 + export default FieldRoot; 171 + 172 + // #endregion
+142
src/primitives/input.tsx
··· 1 + import { splitProps, type JSX } from 'solid-js'; 2 + 3 + import { tw } from '../lib/classes'; 4 + 5 + import { useFieldContext } from './field'; 6 + 7 + // #region types 8 + 9 + export type InputSize = 'small' | 'medium' | 'large'; 10 + export type InputAppearance = 'outline' | 'underline'; 11 + 12 + export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'size'> { 13 + /** element rendered before the input text */ 14 + contentBefore?: JSX.Element; 15 + /** element rendered after the input text */ 16 + contentAfter?: JSX.Element; 17 + /** size variant */ 18 + size?: InputSize; 19 + /** visual style variant */ 20 + appearance?: InputAppearance; 21 + /** ref callback for the input element */ 22 + inputRef?: (el: HTMLInputElement) => void; 23 + } 24 + 25 + // #endregion 26 + 27 + // #region styles 28 + 29 + const rootBaseStyles = tw`inline-flex items-center bg-neutral-background-1 align-middle outline-2 -outline-offset-2 outline-transparent transition duration-100`; 30 + 31 + const rootSizeStyles: Record<InputSize, string> = { 32 + // minHeight 24px, gap 8px 33 + small: tw`min-h-6 gap-2 text-base-200`, 34 + // minHeight 32px, gap 8px 35 + medium: tw`min-h-8 gap-2 text-base-300`, 36 + // minHeight 40px, gap 8px 37 + large: tw`min-h-10 gap-2 text-base-400`, 38 + }; 39 + 40 + const rootAppearanceStyles: Record<InputAppearance, string> = { 41 + outline: tw`rounded-md border border-neutral-stroke-1 hover:border-neutral-stroke-1-hover has-focus-visible:outline-compound-brand-stroke`, 42 + underline: tw`rounded-none border-b border-neutral-stroke-1 hover:border-neutral-stroke-1-hover has-focus-visible:outline-compound-brand-stroke`, 43 + }; 44 + 45 + const rootDisabledStyles = tw`cursor-not-allowed border-neutral-stroke-disabled bg-transparent hover:border-neutral-stroke-disabled`; 46 + 47 + const rootInvalidStyles = tw`border-status-danger-border-2 hover:border-status-danger-border-2 has-focus-visible:outline-status-danger-border-2`; 48 + 49 + const inputBaseStyles = tw`min-w-0 grow bg-transparent outline-none selection:bg-brand-background selection:text-neutral-foreground-on-brand`; 50 + 51 + const inputPaddingStyles: Record<InputSize, { combined: string; withContent: string }> = { 52 + small: { combined: tw`px-2`, withContent: tw`px-0.5` }, 53 + medium: { combined: tw`px-2.5`, withContent: tw`px-0.5` }, 54 + large: { combined: tw`px-3`, withContent: tw`px-1.5` }, 55 + }; 56 + 57 + const contentBaseStyles = tw`flex items-center text-neutral-foreground-3`; 58 + 59 + const contentSizeStyles: Record<InputSize, string> = { 60 + small: tw`[&>svg]:size-3`, 61 + medium: tw`[&>svg]:size-4`, 62 + large: tw`[&>svg]:size-5`, 63 + }; 64 + 65 + const contentPaddingStyles: Record<InputSize, { before: string; after: string }> = { 66 + small: { before: tw`pl-1.5`, after: tw`pr-1.5` }, 67 + medium: { before: tw`pl-2.5`, after: tw`pr-2.5` }, 68 + large: { before: tw`pl-3`, after: tw`pr-3` }, 69 + }; 70 + 71 + // #endregion 72 + 73 + // #region component 74 + 75 + const Input = (props: InputProps) => { 76 + const fieldCtx = useFieldContext(); 77 + 78 + const [local, rest] = splitProps(props, [ 79 + 'contentBefore', 80 + 'contentAfter', 81 + 'size', 82 + 'appearance', 83 + 'class', 84 + 'disabled', 85 + 'inputRef', 86 + 'id', 87 + ]); 88 + 89 + const size = () => local.size ?? 'medium'; 90 + const appearance = () => local.appearance ?? 'outline'; 91 + const hasContentBefore = () => local.contentBefore !== undefined; 92 + const hasContentAfter = () => local.contentAfter !== undefined; 93 + 94 + // field context integration 95 + const inputId = () => local.id ?? fieldCtx?.controlId; 96 + const isInvalid = () => fieldCtx?.validationState() === 'error'; 97 + const isRequired = () => fieldCtx?.required() ?? false; 98 + 99 + const ariaDescribedBy = () => { 100 + if (!fieldCtx) { 101 + return undefined; 102 + } 103 + return `${fieldCtx.validationMessageId} ${fieldCtx.hintId}`; 104 + }; 105 + 106 + return ( 107 + <div 108 + class={`${rootBaseStyles} ${rootSizeStyles[size()]} ${rootAppearanceStyles[appearance()]} ${local.disabled ? rootDisabledStyles : isInvalid() ? rootInvalidStyles : ''} ${local.class ?? ''}`} 109 + > 110 + {local.contentBefore && ( 111 + <div 112 + class={`${contentBaseStyles} ${contentSizeStyles[size()]} ${contentPaddingStyles[size()].before}`} 113 + > 114 + {local.contentBefore} 115 + </div> 116 + )} 117 + 118 + <input 119 + ref={local.inputRef} 120 + id={inputId()} 121 + aria-describedby={ariaDescribedBy()} 122 + aria-invalid={isInvalid() || undefined} 123 + aria-required={isRequired() || undefined} 124 + class={`${inputBaseStyles} ${hasContentBefore() || hasContentAfter() ? inputPaddingStyles[size()].withContent : inputPaddingStyles[size()].combined} ${local.disabled ? 'cursor-not-allowed text-neutral-foreground-disabled placeholder:text-neutral-foreground-disabled' : 'text-neutral-foreground-1 placeholder:text-neutral-foreground-4'}`} 125 + disabled={local.disabled} 126 + {...rest} 127 + /> 128 + 129 + {local.contentAfter && ( 130 + <div 131 + class={`${contentBaseStyles} ${contentSizeStyles[size()]} ${contentPaddingStyles[size()].after}`} 132 + > 133 + {local.contentAfter} 134 + </div> 135 + )} 136 + </div> 137 + ); 138 + }; 139 + 140 + export default Input; 141 + 142 + // #endregion
+195
src/primitives/lib/create-active-descendant.ts
··· 1 + import { createSignal, type Accessor } from 'solid-js'; 2 + 3 + // #region types 4 + 5 + export interface ActiveDescendantOptions { 6 + /** callback when active descendant changes */ 7 + onActiveChange?: (id: string | null) => void; 8 + } 9 + 10 + export interface ActiveDescendantItem { 11 + id: string; 12 + disabled?: boolean; 13 + /** for character search (typeahead) */ 14 + textValue?: string; 15 + } 16 + 17 + export interface ActiveDescendantController { 18 + /** currently active item ID */ 19 + activeId: Accessor<string | null>; 20 + /** set active item by ID */ 21 + setActiveId: (id: string | null) => void; 22 + /** navigate to first non-disabled item */ 23 + first: () => string | null; 24 + /** navigate to last non-disabled item */ 25 + last: () => string | null; 26 + /** navigate to next non-disabled item (circular) */ 27 + next: () => string | null; 28 + /** navigate to prev non-disabled item (circular) */ 29 + prev: () => string | null; 30 + /** find item by character (typeahead) */ 31 + search: (char: string) => string | null; 32 + /** register an item */ 33 + register: (item: ActiveDescendantItem) => void; 34 + /** unregister an item */ 35 + unregister: (id: string) => void; 36 + /** clear all items */ 37 + clear: () => void; 38 + } 39 + 40 + // #endregion 41 + 42 + // #region implementation 43 + 44 + const SEARCH_TIMEOUT = 500; 45 + 46 + /** 47 + * creates a controller for managing aria-activedescendant navigation. 48 + * 49 + * for composite widgets where focus stays on a parent element (dropdowns, comboboxes). 50 + * 51 + * @param options configuration options 52 + * @returns controller for managing active descendant state 53 + */ 54 + export function createActiveDescendant(options?: ActiveDescendantOptions): ActiveDescendantController { 55 + const [activeId, setActiveIdInternal] = createSignal<string | null>(null); 56 + 57 + // maintain insertion order for navigation 58 + const items: ActiveDescendantItem[] = []; 59 + 60 + // typeahead state 61 + let searchBuffer = ''; 62 + let searchTimeout: ReturnType<typeof setTimeout> | undefined; 63 + 64 + const setActiveId = (id: string | null) => { 65 + setActiveIdInternal(id); 66 + options?.onActiveChange?.(id); 67 + }; 68 + 69 + const getEnabledItems = () => items.filter((item) => !item.disabled); 70 + 71 + const getActiveIndex = () => { 72 + const current = activeId(); 73 + if (!current) { 74 + return -1; 75 + } 76 + const enabled = getEnabledItems(); 77 + return enabled.findIndex((item) => item.id === current); 78 + }; 79 + 80 + const first = (): string | null => { 81 + const enabled = getEnabledItems(); 82 + if (enabled.length === 0) { 83 + return null; 84 + } 85 + const id = enabled[0].id; 86 + setActiveId(id); 87 + return id; 88 + }; 89 + 90 + const last = (): string | null => { 91 + const enabled = getEnabledItems(); 92 + if (enabled.length === 0) { 93 + return null; 94 + } 95 + const id = enabled[enabled.length - 1].id; 96 + setActiveId(id); 97 + return id; 98 + }; 99 + 100 + const next = (): string | null => { 101 + const enabled = getEnabledItems(); 102 + if (enabled.length === 0) { 103 + return null; 104 + } 105 + 106 + const currentIndex = getActiveIndex(); 107 + // circular: wrap to first if at end or no current 108 + const nextIndex = currentIndex === -1 || currentIndex >= enabled.length - 1 ? 0 : currentIndex + 1; 109 + const id = enabled[nextIndex].id; 110 + setActiveId(id); 111 + return id; 112 + }; 113 + 114 + const prev = (): string | null => { 115 + const enabled = getEnabledItems(); 116 + if (enabled.length === 0) { 117 + return null; 118 + } 119 + 120 + const currentIndex = getActiveIndex(); 121 + // circular: wrap to last if at start or no current 122 + const prevIndex = currentIndex <= 0 ? enabled.length - 1 : currentIndex - 1; 123 + const id = enabled[prevIndex].id; 124 + setActiveId(id); 125 + return id; 126 + }; 127 + 128 + const search = (char: string): string | null => { 129 + // accumulate characters within timeout 130 + if (searchTimeout) { 131 + clearTimeout(searchTimeout); 132 + } 133 + searchBuffer += char.toLowerCase(); 134 + searchTimeout = setTimeout(() => { 135 + searchBuffer = ''; 136 + }, SEARCH_TIMEOUT); 137 + 138 + const enabled = getEnabledItems(); 139 + if (enabled.length === 0) { 140 + return null; 141 + } 142 + 143 + // start search from item after current, wrapping around 144 + const currentIndex = getActiveIndex(); 145 + const startIndex = currentIndex === -1 ? 0 : currentIndex; 146 + 147 + for (let i = 0; i < enabled.length; i++) { 148 + const index = (startIndex + i) % enabled.length; 149 + const item = enabled[index]; 150 + const textValue = item.textValue?.toLowerCase() ?? ''; 151 + 152 + if (textValue.startsWith(searchBuffer)) { 153 + setActiveId(item.id); 154 + return item.id; 155 + } 156 + } 157 + 158 + return null; 159 + }; 160 + 161 + const register = (item: ActiveDescendantItem) => { 162 + items.push(item); 163 + }; 164 + 165 + const unregister = (id: string) => { 166 + const index = items.findIndex((item) => item.id === id); 167 + if (index !== -1) { 168 + items.splice(index, 1); 169 + } 170 + }; 171 + 172 + const clear = () => { 173 + items.length = 0; 174 + setActiveId(null); 175 + searchBuffer = ''; 176 + if (searchTimeout) { 177 + clearTimeout(searchTimeout); 178 + } 179 + }; 180 + 181 + return { 182 + activeId, 183 + setActiveId, 184 + first, 185 + last, 186 + next, 187 + prev, 188 + search, 189 + register, 190 + unregister, 191 + clear, 192 + }; 193 + } 194 + 195 + // #endregion
+206
src/primitives/lib/create-roving-tabindex.ts
··· 1 + import { createSignal, type Accessor } from 'solid-js'; 2 + 3 + // #region types 4 + 5 + export interface RovingTabindexOptions { 6 + /** callback when focused index changes */ 7 + onFocusChange?: (index: number) => void; 8 + } 9 + 10 + interface RovingTabindexItem { 11 + el: HTMLElement; 12 + disabled?: boolean; 13 + textValue?: string; 14 + } 15 + 16 + export interface RovingTabindexController { 17 + /** currently focused index (-1 if none) */ 18 + focusedIndex: Accessor<number>; 19 + /** set focused index and optionally focus the element */ 20 + setFocusedIndex: (index: number, focus?: boolean) => void; 21 + /** navigate and focus first non-disabled item */ 22 + first: () => void; 23 + /** navigate and focus last non-disabled item */ 24 + last: () => void; 25 + /** navigate and focus next non-disabled item (circular) */ 26 + next: () => void; 27 + /** navigate and focus prev non-disabled item (circular) */ 28 + prev: () => void; 29 + /** find and focus item by character (typeahead) */ 30 + search: (char: string) => void; 31 + /** register an item element */ 32 + register: (el: HTMLElement, disabled?: boolean, textValue?: string) => void; 33 + /** unregister an item element */ 34 + unregister: (el: HTMLElement) => void; 35 + /** check if an element is the focused one */ 36 + isFocused: (el: HTMLElement) => boolean; 37 + /** clear all items */ 38 + clear: () => void; 39 + } 40 + 41 + // #endregion 42 + 43 + // #region implementation 44 + 45 + const SEARCH_TIMEOUT = 500; 46 + 47 + /** 48 + * creates a controller for managing roving tabindex navigation. 49 + * 50 + * for composite widgets where DOM focus moves between items (menus, toolbars). 51 + * 52 + * @param options configuration options 53 + * @returns controller for managing roving tabindex state 54 + */ 55 + export function createRovingTabindex(options?: RovingTabindexOptions): RovingTabindexController { 56 + const [focusedIndex, setFocusedIndexInternal] = createSignal(-1); 57 + 58 + // maintain insertion order for navigation 59 + const items: RovingTabindexItem[] = []; 60 + 61 + // typeahead state 62 + let searchBuffer = ''; 63 + let searchTimeout: ReturnType<typeof setTimeout> | undefined; 64 + 65 + const setFocusedIndex = (index: number, focus = true) => { 66 + setFocusedIndexInternal(index); 67 + options?.onFocusChange?.(index); 68 + 69 + if (focus && index >= 0 && index < items.length) { 70 + items[index].el.focus(); 71 + } 72 + }; 73 + 74 + const getEnabledIndices = (): number[] => { 75 + const indices: number[] = []; 76 + for (let i = 0; i < items.length; i++) { 77 + if (!items[i].disabled) { 78 + indices.push(i); 79 + } 80 + } 81 + return indices; 82 + }; 83 + 84 + const first = () => { 85 + const enabled = getEnabledIndices(); 86 + if (enabled.length === 0) { 87 + return; 88 + } 89 + setFocusedIndex(enabled[0]); 90 + }; 91 + 92 + const last = () => { 93 + const enabled = getEnabledIndices(); 94 + if (enabled.length === 0) { 95 + return; 96 + } 97 + setFocusedIndex(enabled[enabled.length - 1]); 98 + }; 99 + 100 + const next = () => { 101 + const enabled = getEnabledIndices(); 102 + if (enabled.length === 0) { 103 + return; 104 + } 105 + 106 + const current = focusedIndex(); 107 + // find position in enabled array 108 + const currentPos = enabled.indexOf(current); 109 + // circular: wrap to first if at end or not found 110 + const nextPos = currentPos === -1 || currentPos >= enabled.length - 1 ? 0 : currentPos + 1; 111 + setFocusedIndex(enabled[nextPos]); 112 + }; 113 + 114 + const prev = () => { 115 + const enabled = getEnabledIndices(); 116 + if (enabled.length === 0) { 117 + return; 118 + } 119 + 120 + const current = focusedIndex(); 121 + // find position in enabled array 122 + const currentPos = enabled.indexOf(current); 123 + // circular: wrap to last if at start or not found 124 + const prevPos = currentPos <= 0 ? enabled.length - 1 : currentPos - 1; 125 + setFocusedIndex(enabled[prevPos]); 126 + }; 127 + 128 + const search = (char: string) => { 129 + // accumulate characters within timeout 130 + if (searchTimeout) { 131 + clearTimeout(searchTimeout); 132 + } 133 + searchBuffer += char.toLowerCase(); 134 + searchTimeout = setTimeout(() => { 135 + searchBuffer = ''; 136 + }, SEARCH_TIMEOUT); 137 + 138 + const enabled = getEnabledIndices(); 139 + if (enabled.length === 0) { 140 + return; 141 + } 142 + 143 + // start search from item after current, wrapping around 144 + const current = focusedIndex(); 145 + const currentPos = enabled.indexOf(current); 146 + const startPos = currentPos === -1 ? 0 : currentPos; 147 + 148 + for (let i = 0; i < enabled.length; i++) { 149 + const pos = (startPos + i) % enabled.length; 150 + const index = enabled[pos]; 151 + const item = items[index]; 152 + const textValue = item.textValue?.toLowerCase() ?? item.el.textContent?.toLowerCase() ?? ''; 153 + 154 + if (textValue.startsWith(searchBuffer)) { 155 + setFocusedIndex(index); 156 + return; 157 + } 158 + } 159 + }; 160 + 161 + const register = (el: HTMLElement, disabled?: boolean, textValue?: string) => { 162 + items.push({ el, disabled, textValue }); 163 + }; 164 + 165 + const unregister = (el: HTMLElement) => { 166 + const index = items.findIndex((item) => item.el === el); 167 + if (index !== -1) { 168 + items.splice(index, 1); 169 + // adjust focused index if needed 170 + const current = focusedIndex(); 171 + if (current >= index) { 172 + setFocusedIndexInternal(Math.max(-1, current - 1)); 173 + } 174 + } 175 + }; 176 + 177 + const isFocused = (el: HTMLElement): boolean => { 178 + const index = items.findIndex((item) => item.el === el); 179 + return index !== -1 && index === focusedIndex(); 180 + }; 181 + 182 + const clear = () => { 183 + items.length = 0; 184 + setFocusedIndexInternal(-1); 185 + searchBuffer = ''; 186 + if (searchTimeout) { 187 + clearTimeout(searchTimeout); 188 + } 189 + }; 190 + 191 + return { 192 + focusedIndex, 193 + setFocusedIndex, 194 + first, 195 + last, 196 + next, 197 + prev, 198 + search, 199 + register, 200 + unregister, 201 + isFocused, 202 + clear, 203 + }; 204 + } 205 + 206 + // #endregion
+31
src/primitives/menu-button.tsx
··· 1 + import { splitProps, type JSX } from 'solid-js'; 2 + 3 + import { LucideChevronDown } from '../icons/lucide'; 4 + 5 + import Button, { type ButtonProps } from './button'; 6 + 7 + // #region types 8 + 9 + export interface MenuButtonProps extends Omit<ButtonProps, 'children'> { 10 + /** button content */ 11 + children: JSX.Element; 12 + } 13 + 14 + // #endregion 15 + 16 + // #region component 17 + 18 + const MenuButton = (props: MenuButtonProps) => { 19 + const [local, rest] = splitProps(props, ['children']); 20 + 21 + return ( 22 + <Button {...rest}> 23 + {local.children} 24 + <LucideChevronDown class="size-4 text-neutral-foreground-3" /> 25 + </Button> 26 + ); 27 + }; 28 + 29 + export default MenuButton; 30 + 31 + // #endregion
+12
src/primitives/menu.tsx
··· 1 + export { default as Item } from './menu/menu-item'; 2 + export { default as List } from './menu/menu-list'; 3 + export { default as Popover } from './menu/menu-popover'; 4 + export { default as Root } from './menu/menu-root'; 5 + export { default as Trigger } from './menu/menu-trigger'; 6 + 7 + export type { MenuItemProps } from './menu/menu-item'; 8 + export type { MenuListProps } from './menu/menu-list'; 9 + export type { MenuPopoverProps } from './menu/menu-popover'; 10 + export type { MenuRootProps } from './menu/menu-root'; 11 + export type { MenuTriggerChildProps, MenuTriggerProps } from './menu/menu-trigger'; 12 + export type { MenuContextValue, MenuListContextValue } from './menu/context';
+66
src/primitives/menu/context.tsx
··· 1 + import type { Placement } from '@floating-ui/dom'; 2 + import { createContext, useContext, type Accessor } from 'solid-js'; 3 + 4 + import type { RovingTabindexController } from '../lib/create-roving-tabindex'; 5 + 6 + // #region types 7 + 8 + export interface MenuContextValue { 9 + /** whether the menu is open */ 10 + open: Accessor<boolean>; 11 + /** set the menu open state */ 12 + setOpen: (open: boolean) => void; 13 + /** the trigger element ref */ 14 + triggerRef: Accessor<HTMLElement | null>; 15 + /** set the trigger element ref */ 16 + setTriggerRef: (el: HTMLElement | null) => void; 17 + /** unique ID for the trigger */ 18 + triggerId: string; 19 + /** unique ID for the menu */ 20 + menuId: string; 21 + /** placement for the popover */ 22 + placement: () => Placement; 23 + /** roving tabindex controller for keyboard navigation */ 24 + rovingTabindex: RovingTabindexController; 25 + /** the popover element ref for scrolling */ 26 + popoverRef: Accessor<HTMLElement | null>; 27 + /** set the popover element ref */ 28 + setPopoverRef: (el: HTMLElement | null) => void; 29 + } 30 + 31 + export interface MenuListContextValue { 32 + /** whether menu items should reserve space for checkmarks */ 33 + hasCheckmarks: boolean; 34 + /** whether menu items should reserve space for icons */ 35 + hasIcons: boolean; 36 + } 37 + 38 + // #endregion 39 + 40 + // #region menu context 41 + 42 + const MenuContext = createContext<MenuContextValue>(); 43 + 44 + export const MenuProvider = MenuContext.Provider; 45 + 46 + export function useMenuContext(): MenuContextValue { 47 + const ctx = useContext(MenuContext); 48 + if (!ctx) { 49 + throw new Error('Menu components must be used within a Menu.Root'); 50 + } 51 + return ctx; 52 + } 53 + 54 + // #endregion 55 + 56 + // #region menu list context 57 + 58 + const MenuListContext = createContext<MenuListContextValue>(); 59 + 60 + export const MenuListProvider = MenuListContext.Provider; 61 + 62 + export function useMenuListContext(): MenuListContextValue | undefined { 63 + return useContext(MenuListContext); 64 + } 65 + 66 + // #endregion
+140
src/primitives/menu/menu-item.tsx
··· 1 + import { createEffect, onCleanup, Show, type JSX } from 'solid-js'; 2 + 3 + import { LucideCheck } from '../../icons/lucide'; 4 + import { modality } from '../../lib/modality'; 5 + 6 + import { useMenuContext, useMenuListContext } from './context'; 7 + 8 + // #region types 9 + 10 + export interface MenuItemProps { 11 + /** item content */ 12 + children: JSX.Element; 13 + /** icon to display before the content */ 14 + icon?: JSX.Element; 15 + /** whether the item is checked (shows checkmark) */ 16 + checked?: boolean; 17 + /** called when the item is selected */ 18 + onClick?: () => void; 19 + /** whether the item is disabled */ 20 + disabled?: boolean; 21 + /** whether clicking this item should keep the menu open */ 22 + persistOnClick?: boolean; 23 + } 24 + 25 + // #endregion 26 + 27 + // #region component 28 + 29 + const MenuItem = (props: MenuItemProps) => { 30 + const ctx = useMenuContext(); 31 + const listCtx = useMenuListContext(); 32 + 33 + const hasCheckmarks = () => listCtx?.hasCheckmarks ?? false; 34 + const hasIcons = () => listCtx?.hasIcons ?? false; 35 + 36 + // stable ref for isFocused check 37 + let itemRef: HTMLElement | undefined; 38 + 39 + const isFocused = () => (itemRef ? ctx.rovingTabindex.isFocused(itemRef) : false); 40 + 41 + const handleClick = () => { 42 + if (props.disabled) { 43 + return; 44 + } 45 + props.onClick?.(); 46 + if (!props.persistOnClick) { 47 + ctx.setOpen(false); 48 + } 49 + }; 50 + 51 + const handleKeyDown = (ev: KeyboardEvent) => { 52 + if (props.disabled) { 53 + return; 54 + } 55 + if (ev.key === 'Enter' || ev.key === ' ') { 56 + ev.preventDefault(); 57 + handleClick(); 58 + } 59 + }; 60 + 61 + const handleMouseMove = () => { 62 + if (modality() === 'pointer' && !props.disabled && itemRef) { 63 + // find the index of this item and focus it 64 + const items = ctx.popoverRef()?.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"]'); 65 + if (items) { 66 + const index = Array.from(items).indexOf(itemRef); 67 + if (index !== -1) { 68 + ctx.rovingTabindex.setFocusedIndex(index, true); 69 + } 70 + } 71 + } 72 + }; 73 + 74 + return ( 75 + <div 76 + ref={(el) => { 77 + itemRef = el; 78 + 79 + // register item with roving tabindex controller 80 + const textValue = typeof props.children === 'string' ? props.children : undefined; 81 + ctx.rovingTabindex.register(el, props.disabled, textValue); 82 + 83 + onCleanup(() => { 84 + ctx.rovingTabindex.unregister(el); 85 + }); 86 + 87 + // scroll into view when focused via keyboard 88 + createEffect(() => { 89 + const popover = ctx.popoverRef(); 90 + if (isFocused() && modality() === 'keyboard' && popover) { 91 + const padding = 4; 92 + const popoverRect = popover.getBoundingClientRect(); 93 + const itemRect = el.getBoundingClientRect(); 94 + 95 + if (itemRect.top < popoverRect.top + padding) { 96 + popover.scrollTop -= popoverRect.top + padding - itemRect.top; 97 + } else if (itemRect.bottom > popoverRect.bottom - padding) { 98 + popover.scrollTop += itemRect.bottom - (popoverRect.bottom - padding); 99 + } 100 + } 101 + }); 102 + }} 103 + role={hasCheckmarks() ? 'menuitemcheckbox' : 'menuitem'} 104 + tabIndex={isFocused() ? 0 : -1} 105 + aria-checked={hasCheckmarks() ? props.checked : undefined} 106 + aria-disabled={props.disabled} 107 + onClick={handleClick} 108 + onKeyDown={handleKeyDown} 109 + onMouseMove={handleMouseMove} 110 + class="box-border flex min-h-8 max-w-72.5 items-center gap-1 rounded-md px-1.5 py-1.5 text-base-300 outline-none select-none" 111 + classList={{ 112 + 'text-neutral-foreground-disabled cursor-not-allowed': props.disabled, 113 + 'text-neutral-foreground-2 hover:text-neutral-foreground-2-hover active:bg-neutral-background-1-pressed': 114 + !props.disabled, 115 + 'bg-neutral-background-1-hover': isFocused() && !props.disabled, 116 + }} 117 + > 118 + {/* checkmark slot - reserve space when hasCheckmarks is true */} 119 + <Show when={hasCheckmarks()}> 120 + <span class="flex size-4 shrink-0 items-center justify-center"> 121 + <Show when={props.checked}> 122 + <LucideCheck class="size-4" /> 123 + </Show> 124 + </span> 125 + </Show> 126 + 127 + {/* icon slot - reserve space when hasIcons is true */} 128 + <Show when={hasIcons()}> 129 + <span class="flex size-5 shrink-0 items-center justify-center">{props.icon}</span> 130 + </Show> 131 + 132 + {/* content */} 133 + <span class="grow">{props.children}</span> 134 + </div> 135 + ); 136 + }; 137 + 138 + export default MenuItem; 139 + 140 + // #endregion
+32
src/primitives/menu/menu-list.tsx
··· 1 + import type { JSX } from 'solid-js'; 2 + 3 + import { MenuListProvider } from './context'; 4 + 5 + // #region types 6 + 7 + export interface MenuListProps { 8 + /** menu items */ 9 + children: JSX.Element; 10 + /** whether menu items should reserve space for checkmarks */ 11 + hasCheckmarks?: boolean; 12 + /** whether menu items should reserve space for icons */ 13 + hasIcons?: boolean; 14 + } 15 + 16 + // #endregion 17 + 18 + // #region component 19 + 20 + const MenuList = (props: MenuListProps) => { 21 + return ( 22 + <MenuListProvider 23 + value={{ hasCheckmarks: props.hasCheckmarks ?? false, hasIcons: props.hasIcons ?? false }} 24 + > 25 + <div class="flex flex-col gap-0.5">{props.children}</div> 26 + </MenuListProvider> 27 + ); 28 + }; 29 + 30 + export default MenuList; 31 + 32 + // #endregion
+138
src/primitives/menu/menu-popover.tsx
··· 1 + import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'; 2 + import { createEffect, onCleanup, onMount, type JSX } from 'solid-js'; 3 + import { Portal } from 'solid-js/web'; 4 + 5 + import { useMenuContext } from './context'; 6 + 7 + // #region types 8 + 9 + export interface MenuPopoverProps { 10 + /** menu content (typically MenuList) */ 11 + children: JSX.Element; 12 + } 13 + 14 + // #endregion 15 + 16 + // #region component 17 + 18 + const MenuPopover = (props: MenuPopoverProps) => { 19 + const ctx = useMenuContext(); 20 + 21 + const handleKeyDown = (ev: KeyboardEvent) => { 22 + switch (ev.key) { 23 + case 'ArrowDown': { 24 + ev.preventDefault(); 25 + ctx.rovingTabindex.next(); 26 + break; 27 + } 28 + case 'ArrowUp': { 29 + ev.preventDefault(); 30 + ctx.rovingTabindex.prev(); 31 + break; 32 + } 33 + case 'Home': { 34 + ev.preventDefault(); 35 + ctx.rovingTabindex.first(); 36 + break; 37 + } 38 + case 'End': { 39 + ev.preventDefault(); 40 + ctx.rovingTabindex.last(); 41 + break; 42 + } 43 + case 'Escape': { 44 + ev.preventDefault(); 45 + ctx.setOpen(false); 46 + ctx.triggerRef()?.focus(); 47 + break; 48 + } 49 + default: { 50 + // character search (typeahead) 51 + if (ev.key.length === 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey) { 52 + ctx.rovingTabindex.search(ev.key); 53 + } 54 + } 55 + } 56 + }; 57 + 58 + return ( 59 + <> 60 + {ctx.open() && ( 61 + <Portal> 62 + <div 63 + ref={(el) => { 64 + ctx.setPopoverRef(el); 65 + 66 + onMount(() => { 67 + // focus first item after items are registered 68 + requestAnimationFrame(() => { 69 + ctx.rovingTabindex.first(); 70 + }); 71 + 72 + createEffect(() => { 73 + const trigger = ctx.triggerRef(); 74 + if (!trigger) { 75 + return; 76 + } 77 + 78 + const placement = ctx.placement(); 79 + 80 + const updatePosition = async () => { 81 + const { x, y } = await computePosition(trigger, el, { 82 + placement: placement, 83 + strategy: 'absolute', 84 + middleware: [offset(4), flip(), shift({ padding: 8 })], 85 + }); 86 + 87 + Object.assign(el.style, { 88 + position: 'absolute', 89 + left: `${x}px`, 90 + top: `${y}px`, 91 + }); 92 + }; 93 + 94 + onCleanup(autoUpdate(trigger, el, updatePosition)); 95 + }); 96 + 97 + { 98 + // handle click outside to close 99 + const handleClickOutside = (ev: MouseEvent) => { 100 + const currentTrigger = ctx.triggerRef(); 101 + if ( 102 + !el.contains(ev.target as Node) && 103 + currentTrigger && 104 + !currentTrigger.contains(ev.target as Node) 105 + ) { 106 + ctx.setOpen(false); 107 + } 108 + }; 109 + 110 + document.addEventListener('mousedown', handleClickOutside); 111 + 112 + onCleanup(() => { 113 + document.removeEventListener('mousedown', handleClickOutside); 114 + }); 115 + } 116 + }); 117 + 118 + onCleanup(() => { 119 + ctx.setPopoverRef(null); 120 + }); 121 + }} 122 + id={ctx.menuId} 123 + role="menu" 124 + aria-labelledby={ctx.triggerId} 125 + onKeyDown={handleKeyDown} 126 + class="fixed z-50 box-border max-w-75 min-w-34.5 overflow-hidden rounded-md border border-transparent-stroke bg-neutral-background-1 p-1 text-base-300 text-neutral-foreground-1 shadow-16" 127 + > 128 + {props.children} 129 + </div> 130 + </Portal> 131 + )} 132 + </> 133 + ); 134 + }; 135 + 136 + export default MenuPopover; 137 + 138 + // #endregion
+73
src/primitives/menu/menu-root.tsx
··· 1 + import type { Placement } from '@floating-ui/dom'; 2 + import { createEffect, createMemo, createSignal, createUniqueId, type JSX } from 'solid-js'; 3 + 4 + import { createRovingTabindex } from '../lib/create-roving-tabindex'; 5 + 6 + import { MenuProvider, type MenuContextValue } from './context'; 7 + 8 + // #region types 9 + 10 + export interface MenuRootProps { 11 + /** menu content (MenuTrigger and MenuPopover) */ 12 + children: JSX.Element; 13 + /** controlled open state */ 14 + open?: boolean; 15 + /** default open state for uncontrolled usage */ 16 + defaultOpen?: boolean; 17 + /** callback when open state changes */ 18 + onOpenChange?: (open: boolean) => void; 19 + /** placement of the menu popover relative to trigger */ 20 + placement?: Placement; 21 + } 22 + 23 + // #endregion 24 + 25 + // #region component 26 + 27 + const MenuRoot = (props: MenuRootProps) => { 28 + const triggerId = createUniqueId(); 29 + const menuId = createUniqueId(); 30 + 31 + const [triggerRef, setTriggerRef] = createSignal<HTMLElement | null>(null); 32 + const [popoverRef, setPopoverRef] = createSignal<HTMLElement | null>(null); 33 + 34 + // support both controlled and uncontrolled modes 35 + const [internalOpen, setInternalOpen] = createSignal(props.defaultOpen ?? false); 36 + 37 + const open = () => props.open ?? internalOpen(); 38 + const setOpen = (value: boolean) => { 39 + if (props.open === undefined) { 40 + setInternalOpen(value); 41 + } 42 + props.onOpenChange?.(value); 43 + }; 44 + 45 + // roving tabindex for keyboard navigation 46 + const rovingTabindex = createRovingTabindex(); 47 + 48 + // when closing, clear items 49 + createEffect(() => { 50 + if (!open()) { 51 + rovingTabindex.clear(); 52 + } 53 + }); 54 + 55 + const context: MenuContextValue = { 56 + open, 57 + setOpen, 58 + triggerRef, 59 + setTriggerRef, 60 + triggerId, 61 + menuId, 62 + placement: createMemo(() => props.placement ?? 'bottom-start'), 63 + rovingTabindex, 64 + popoverRef, 65 + setPopoverRef, 66 + }; 67 + 68 + return <MenuProvider value={context}>{props.children}</MenuProvider>; 69 + }; 70 + 71 + export default MenuRoot; 72 + 73 + // #endregion
+71
src/primitives/menu/menu-trigger.tsx
··· 1 + import type { JSX } from 'solid-js'; 2 + 3 + import { useMenuContext } from './context'; 4 + 5 + // #region types 6 + 7 + export interface MenuTriggerChildProps { 8 + ref: (el: HTMLElement) => void; 9 + id: string; 10 + 'aria-haspopup': 'menu'; 11 + 'aria-expanded': boolean; 12 + 'aria-controls': string | undefined; 13 + onClick: () => void; 14 + onKeyDown: (ev: KeyboardEvent) => void; 15 + } 16 + 17 + export interface MenuTriggerProps { 18 + /** render prop that receives trigger props to spread onto your element */ 19 + children: (props: MenuTriggerChildProps) => JSX.Element; 20 + } 21 + 22 + // #endregion 23 + 24 + // #region component 25 + 26 + const MenuTrigger = (props: MenuTriggerProps) => { 27 + const ctx = useMenuContext(); 28 + 29 + const handleClick = () => { 30 + ctx.setOpen(!ctx.open()); 31 + }; 32 + 33 + const handleKeyDown = (ev: KeyboardEvent) => { 34 + switch (ev.key) { 35 + case 'Enter': 36 + case ' ': 37 + case 'ArrowDown': { 38 + ev.preventDefault(); 39 + ctx.setOpen(true); 40 + break; 41 + } 42 + case 'Escape': { 43 + if (ctx.open()) { 44 + ev.preventDefault(); 45 + ctx.setOpen(false); 46 + } 47 + break; 48 + } 49 + } 50 + }; 51 + 52 + const childProps: MenuTriggerChildProps = { 53 + ref: (el: HTMLElement) => ctx.setTriggerRef(el), 54 + id: ctx.triggerId, 55 + 'aria-haspopup': 'menu', 56 + get 'aria-expanded'() { 57 + return ctx.open(); 58 + }, 59 + get 'aria-controls'() { 60 + return ctx.open() ? ctx.menuId : undefined; 61 + }, 62 + onClick: handleClick, 63 + onKeyDown: handleKeyDown, 64 + }; 65 + 66 + return <>{props.children(childProps)}</>; 67 + }; 68 + 69 + export default MenuTrigger; 70 + 71 + // #endregion
+184
src/primitives/tooltip.tsx
··· 1 + import { autoUpdate, computePosition, flip, offset, shift, type Placement } from '@floating-ui/dom'; 2 + import { createEffect, createSignal, createUniqueId, onCleanup, type JSX } from 'solid-js'; 3 + import { Portal } from 'solid-js/web'; 4 + 5 + // #region types 6 + 7 + export type TooltipRelationship = 'label' | 'description' | 'inaccessible'; 8 + 9 + export interface TooltipTriggerProps { 10 + ref: (el: HTMLElement) => void; 11 + onPointerEnter: () => void; 12 + onPointerLeave: () => void; 13 + onFocus: () => void; 14 + onBlur: () => void; 15 + 'aria-labelledby'?: string | undefined; 16 + 'aria-describedby'?: string | undefined; 17 + } 18 + 19 + export interface TooltipProps { 20 + /** render prop that receives trigger props to spread onto your element */ 21 + children: (props: TooltipTriggerProps) => JSX.Element; 22 + /** tooltip content */ 23 + content: JSX.Element; 24 + /** how the tooltip relates to its trigger for accessibility */ 25 + relationship: TooltipRelationship; 26 + /** positioning relative to trigger */ 27 + placement?: Placement; 28 + /** delay before showing in ms */ 29 + showDelay?: number; 30 + /** delay before hiding in ms */ 31 + hideDelay?: number; 32 + } 33 + 34 + // #endregion 35 + 36 + // #region component 37 + 38 + const Tooltip = (props: TooltipProps) => { 39 + const tooltipId = createUniqueId(); 40 + 41 + let showTimeout: ReturnType<typeof setTimeout> | undefined; 42 + let hideTimeout: ReturnType<typeof setTimeout> | undefined; 43 + 44 + const [visible, setVisible] = createSignal(false); 45 + const [triggerEl, setTriggerEl] = createSignal<HTMLElement>(); 46 + 47 + const showDelay = () => props.showDelay ?? 250; 48 + const hideDelay = () => props.hideDelay ?? 250; 49 + const placement = () => props.placement ?? 'top'; 50 + 51 + const clearTimeouts = () => { 52 + if (showTimeout) { 53 + clearTimeout(showTimeout); 54 + showTimeout = undefined; 55 + } 56 + if (hideTimeout) { 57 + clearTimeout(hideTimeout); 58 + hideTimeout = undefined; 59 + } 60 + }; 61 + 62 + const scheduleShow = () => { 63 + clearTimeouts(); 64 + showTimeout = setTimeout(() => { 65 + setVisible(true); 66 + }, showDelay()); 67 + }; 68 + 69 + const scheduleHide = () => { 70 + clearTimeouts(); 71 + hideTimeout = setTimeout(() => { 72 + setVisible(false); 73 + }, hideDelay()); 74 + }; 75 + 76 + const handlePointerEnter = () => { 77 + scheduleShow(); 78 + }; 79 + 80 + const handlePointerLeave = () => { 81 + scheduleHide(); 82 + }; 83 + 84 + const handleFocus = () => { 85 + scheduleShow(); 86 + }; 87 + 88 + const handleBlur = () => { 89 + // hide immediately on blur 90 + clearTimeouts(); 91 + setVisible(false); 92 + }; 93 + 94 + // keep tooltip open when hovering over it 95 + const handleTooltipPointerEnter = () => { 96 + clearTimeouts(); 97 + }; 98 + 99 + const handleTooltipPointerLeave = () => { 100 + scheduleHide(); 101 + }; 102 + 103 + // cleanup timeouts on unmount 104 + onCleanup(clearTimeouts); 105 + 106 + const triggerProps: TooltipTriggerProps = { 107 + ref: (el: HTMLElement) => { 108 + setTriggerEl(el); 109 + }, 110 + onPointerEnter: handlePointerEnter, 111 + onPointerLeave: handlePointerLeave, 112 + onFocus: handleFocus, 113 + onBlur: handleBlur, 114 + get 'aria-labelledby'() { 115 + return props.relationship === 'label' && visible() ? tooltipId : undefined; 116 + }, 117 + get 'aria-describedby'() { 118 + return props.relationship === 'description' && visible() ? tooltipId : undefined; 119 + }, 120 + }; 121 + 122 + return ( 123 + <> 124 + {props.children(triggerProps)} 125 + 126 + {visible() && ( 127 + <Portal> 128 + <div 129 + ref={(el) => { 130 + createEffect(() => { 131 + const $trigger = triggerEl(); 132 + if (!$trigger) { 133 + return; 134 + } 135 + 136 + const $placement = placement(); 137 + 138 + const updatePosition = async () => { 139 + const { x, y } = await computePosition($trigger, el, { 140 + placement: $placement, 141 + strategy: 'absolute', 142 + middleware: [offset(4), flip(), shift({ padding: 8 })], 143 + }); 144 + 145 + Object.assign(el.style, { 146 + position: 'absolute', 147 + left: `${x}px`, 148 + top: `${y}px`, 149 + }); 150 + }; 151 + 152 + onCleanup(autoUpdate($trigger, el, updatePosition)); 153 + }); 154 + 155 + const handleKeyDown = (ev: KeyboardEvent) => { 156 + if (ev.key === 'Escape') { 157 + clearTimeouts(); 158 + setVisible(false); 159 + } 160 + }; 161 + 162 + document.addEventListener('keydown', handleKeyDown); 163 + 164 + onCleanup(() => { 165 + document.removeEventListener('keydown', handleKeyDown); 166 + }); 167 + }} 168 + id={tooltipId} 169 + role="tooltip" 170 + class="rounded fixed z-50 box-border max-w-60 cursor-default border border-transparent-stroke bg-neutral-background-1 pt-1 pr-2.5 pb-1.5 pl-2.5 text-base-200 wrap-break-word text-neutral-foreground-1 shadow-4" 171 + onPointerEnter={handleTooltipPointerEnter} 172 + onPointerLeave={handleTooltipPointerLeave} 173 + > 174 + {props.content} 175 + </div> 176 + </Portal> 177 + )} 178 + </> 179 + ); 180 + }; 181 + 182 + export default Tooltip; 183 + 184 + // #endregion
+29
tsconfig.app.json
··· 1 + { 2 + "compilerOptions": { 3 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 + "target": "ES2022", 5 + "useDefineForClassFields": true, 6 + "module": "ESNext", 7 + "lib": ["ESNext", "DOM", "DOM.Iterable"], 8 + "types": ["vite/client"], 9 + "skipLibCheck": true, 10 + 11 + /* Bundler mode */ 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "moduleDetection": "force", 16 + "noEmit": true, 17 + "jsx": "preserve", 18 + "jsxImportSource": "solid-js", 19 + 20 + /* Linting */ 21 + "strict": true, 22 + "noUnusedLocals": true, 23 + "noUnusedParameters": true, 24 + "erasableSyntaxOnly": true, 25 + "noFallthroughCasesInSwitch": true, 26 + "noUncheckedSideEffectImports": true 27 + }, 28 + "include": ["src"] 29 + }
+4
tsconfig.json
··· 1 + { 2 + "files": [], 3 + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 + }
+26
tsconfig.node.json
··· 1 + { 2 + "compilerOptions": { 3 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 + "target": "ES2023", 5 + "lib": ["ES2023"], 6 + "module": "ESNext", 7 + "types": ["node"], 8 + "skipLibCheck": true, 9 + 10 + /* Bundler mode */ 11 + "moduleResolution": "bundler", 12 + "allowImportingTsExtensions": true, 13 + "verbatimModuleSyntax": true, 14 + "moduleDetection": "force", 15 + "noEmit": true, 16 + 17 + /* Linting */ 18 + "strict": true, 19 + "noUnusedLocals": true, 20 + "noUnusedParameters": true, 21 + "erasableSyntaxOnly": true, 22 + "noFallthroughCasesInSwitch": true, 23 + "noUncheckedSideEffectImports": true 24 + }, 25 + "include": ["vite.config.ts"] 26 + }
+19
vite.config.ts
··· 1 + import tailwindcss from '@tailwindcss/vite'; 2 + import { defineConfig } from 'vite'; 3 + import solid from 'vite-plugin-solid'; 4 + 5 + export default defineConfig({ 6 + plugins: [tailwindcss(), solid()], 7 + optimizeDeps: { 8 + exclude: ['@rolldown/browser'], 9 + }, 10 + worker: { 11 + format: 'es', 12 + }, 13 + server: { 14 + headers: { 15 + 'Cross-Origin-Opener-Policy': 'same-origin', 16 + 'Cross-Origin-Embedder-Policy': 'require-corp', 17 + }, 18 + }, 19 + });
+7
vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + globals: true, 6 + }, 7 + });
+9
wrangler.jsonc
··· 1 + { 2 + "$schema": "https://unpkg.com/wrangler@latest/config-schema.json", 3 + "name": "teardown", 4 + "compatibility_date": "2026-01-20", 5 + "assets": { 6 + "directory": "dist", 7 + "not_found_handling": "single-page-application", 8 + }, 9 + }