diff --git a/.changeset/empty-knives-walk.md b/.changeset/empty-knives-walk.md new file mode 100644 index 000000000..dbfa077b1 --- /dev/null +++ b/.changeset/empty-knives-walk.md @@ -0,0 +1,5 @@ +--- +'@fuzdev/fuz_css': minor +--- + +implement CSS literal classes diff --git a/.gitignore b/.gitignore index bdef460f4..5117c8d01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,15 @@ # Deps -node_modules +node_modules/ # Output -/.svelte-kit -/build -/dist -/dist_* -/target -/.gro -/.zzz +.svelte-kit/ +build/ +dist/ +dist_*/ +target/ +.gro/ +.fuz/ +.zzz/ # Env .env* diff --git a/CLAUDE.md b/CLAUDE.md index 2086a1452..7f1d6be49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,22 @@ -# Fuz CSS framework and design system +# fuz_css framework and design system -CSS framework and design system built around **semantic styles** and **style variables** (design tokens as CSS custom properties). Early alpha with breaking changes ahead. +CSS framework and design system for semantic HTML. +It styles HTML elements by default and integrates +custom properties, themes, and utility classes into a complete system. +Early alpha with breaking changes ahead. For code style, see the `fuz-stack` skill. For UI components (themes, color scheme controls), see `@fuzdev/fuz_ui`. +## Gro commands + +```bash +gro check # typecheck, test, lint, format check (run before committing) +gro typecheck # typecheck only (faster iteration) +gro test # run tests (SKIP_EXAMPLE_TESTS=1 to skip slow integration tests) +gro gen # regenerate theme.css and other .gen files +gro build # build the package for production +``` + ## Design decisions ### Two core concepts @@ -26,7 +39,56 @@ For code style, see the `fuz-stack` skill. For UI components (themes, color sche ### Smart utility class generation -[gen_fuz_css.ts](src/lib/gen_fuz_css.ts) scans source files with regex extractors, collects class names, and outputs only CSS for classes actually used. Dynamic [interpreters](src/lib/css_class_interpreters.ts) handle pattern-based classes like `opacity_50`, `font_weight_700`, `z_index_100`. +Two generators available, both using AST-based extraction ([css_class_extractor.ts](src/lib/css_class_extractor.ts)) and per-file caching with content hash validation: + +1. **Gro generator** - [gen_fuz_css.ts](src/lib/gen_fuz_css.ts) for SvelteKit projects using Gro +2. **Vite plugin** - [vite_plugin_fuz_css.ts](src/lib/vite_plugin_fuz_css.ts) for Svelte/React/Preact/Solid via `virtual:fuz.css` + +Both output only CSS for classes actually used. Supports Svelte 5.16+ class syntax, JSX `className`, clsx/cn calls, and `// @fuz-classes` comment hints. + +**Shared options (both generators):** + +- `filter_file` - Which files to extract from (default: `.svelte`, `.html`, `.ts`, `.js`, `.tsx`, `.jsx`, excluding tests/gen files) +- `class_definitions` - Additional definitions to merge with builtins (user takes precedence) +- `class_interpreters` - Custom interpreters (replaces builtins if provided) +- `include_classes` - Classes to always include (for dynamic class names) +- `exclude_classes` - Classes to exclude (filter false positives) +- `acorn_plugins` - Additional acorn plugins (use `acorn-jsx` for React/Preact/Solid) +- `on_error` - `'log'` (default) or `'throw'` for CSS-literal errors +- `cache_dir` - Cache directory (default: `.fuz/cache/css`) + +**Gro-only options:** + +- `include_stats` - Include file statistics in output +- `project_root` - Project root directory (default: `process.cwd()`) +- `concurrency` - Max concurrent file processing (default: 8) +- `cache_io_concurrency` - Max concurrent cache I/O (default: 50) + +**Key implementation differences:** + +- **HMR**: Vite plugin has HMR with 10ms debouncing; Gro regenerates on file change +- **Cache writes**: Vite uses fire-and-forget; Gro awaits with concurrency control +- **External files**: Both use hashed paths in `_external/` subdirectory for files outside project root (e.g., symlinked deps with pnpm) +- **Error types**: Both throw `CssGenerationError` with `diagnostics` property for programmatic error access +- **CI behavior**: Both skip cache writes on CI + +### Three class types + +- **Token classes** - Map to style variables: `p_md`, `color_a_5`, `gap_lg` +- **Composite classes** - Multi-property shortcuts: `box`, `row`, `ellipsis` +- **Literal classes** - CSS `property:value` syntax: `display:flex`, `opacity:50%` + +All class types support modifiers: responsive (`md:`), state (`hover:`), color-scheme (`dark:`), pseudo-element (`before:`). + +### CSS-literal syntax + +Literal classes use `property:value` syntax that maps 1:1 to CSS: + +- `display:flex` → `display: flex;` +- `hover:opacity:80%` → `:hover { opacity: 80%; }` +- `md:dark:hover:opacity:80%` → nested media/ancestor/state wrappers + +Space encoding uses `~` for multi-value properties (`margin:0~auto`). Arbitrary breakpoints via `min-width(800px):` and `max-width(600px):`. ## Variable naming @@ -44,11 +106,30 @@ See [variables.ts](src/lib/variables.ts) for definitions, [variable_data.ts](src ## Usage -Import [style.css](src/lib/style.css) + [theme.css](src/lib/theme.css) for base styles. Optionally generate project-specific `fuz.css` using [gen_fuz_css()](src/lib/gen_fuz_css.ts) in a Gro generator. +Import [style.css](src/lib/style.css) + [theme.css](src/lib/theme.css) for base styles. Generate utility classes via: + +**SvelteKit (Gro):** Use [gen_fuz_css()](src/lib/gen_fuz_css.ts) in a `.gen.css.ts` file. + +**Vite (Svelte/React/Preact/Solid):** + +```ts +// vite.config.ts +import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.js'; +import jsx from 'acorn-jsx'; // only needed for JSX frameworks + +export default defineConfig({ + plugins: [vite_plugin_fuz_css({acorn_plugins: [jsx()]})], +}); + +// main.ts +import '@fuzdev/fuz_css/style.css'; +import '@fuzdev/fuz_css/theme.css'; // or bring your own +import 'virtual:fuz.css'; +``` ## Docs -[src/routes/docs/](src/routes/docs/) has pages for: colors, themes, variables, classes, typography, buttons, forms, elements, layout, borders, shadows, shading. See [tomes.ts](src/routes/docs/tomes.ts) for structure. +[src/routes/docs/](src/routes/docs/) has pages for: introduction, api, examples, semantic, themes, variables, classes, colors, buttons, elements, forms, typography, borders, shading, shadows, layout. See [tomes.ts](src/routes/docs/tomes.ts) for structure. ## File organization @@ -58,18 +139,35 @@ Import [style.css](src/lib/style.css) + [theme.css](src/lib/theme.css) for base - [variables.ts](src/lib/variables.ts) - All style variable definitions (~250+) - [variable.ts](src/lib/variable.ts) - `StyleVariable` type and validation -- [variable_data.ts](src/lib/variable_data.ts) - Size variants, color intensities, CSS data values +- [variable_data.ts](src/lib/variable_data.ts) - Size, color, and border variant definitions - [theme.ts](src/lib/theme.ts) - Theme rendering, `ColorScheme` type, `render_theme_style()` - [themes.ts](src/lib/themes.ts) - Theme definitions (base, low/high contrast) +**CSS extraction:** + +- [css_class_extractor.ts](src/lib/css_class_extractor.ts) - AST-based class extraction from Svelte/TS/JSX files +- [file_filter.ts](src/lib/file_filter.ts) - `FileFilter` type and `filter_file_default` for filtering extractable files +- [diagnostics.ts](src/lib/diagnostics.ts) - `SourceLocation`, `ExtractionDiagnostic`, `GenerationDiagnostic` types + **CSS generation:** -- [gen_fuz_css.ts](src/lib/gen_fuz_css.ts) - Main generator API for Gro -- [css_classes.ts](src/lib/css_classes.ts) - All static class definitions (~1000+) -- [css_class_generators.ts](src/lib/css_class_generators.ts) - Class template generation functions -- [css_class_composites.ts](src/lib/css_class_composites.ts) - Composite classes (`.box`, `.row`, `.column`, `.ellipsis`) -- [css_class_interpreters.ts](src/lib/css_class_interpreters.ts) - Dynamic interpreters for opacity, font-weight, z-index, border-radius -- [css_class_helpers.ts](src/lib/css_class_helpers.ts) - CSS class extraction, `CssClasses` collection, `generate_classes_css()` +- [gen_fuz_css.ts](src/lib/gen_fuz_css.ts) - Gro generator API with per-file caching +- [vite_plugin_fuz_css.ts](src/lib/vite_plugin_fuz_css.ts) - Vite plugin for Svelte/React/Preact/Solid, `virtual:fuz.css` virtual module with HMR +- [css_cache.ts](src/lib/css_cache.ts) - Cache infrastructure (`.fuz/cache/css/`) +- [css_classes.ts](src/lib/css_classes.ts) - `CssClasses` collection for tracking classes per-file +- [css_class_generation.ts](src/lib/css_class_generation.ts) - `CssClassDefinition` types, `generate_classes_css()` +- [css_class_definitions.ts](src/lib/css_class_definitions.ts) - Combined token + composite class registry +- [css_class_generators.ts](src/lib/css_class_generators.ts) - Token class template generators +- [css_class_composites.ts](src/lib/css_class_composites.ts) - Composite classes (`.box`, `.row`, `.column`, `.ellipsis`, `.pane`, `.panel`) +- [css_class_resolution.ts](src/lib/css_class_resolution.ts) - `resolve_composes()` for composing definitions +- [css_class_interpreters.ts](src/lib/css_class_interpreters.ts) - `modified_class_interpreter` and `css_literal_interpreter` +- [css_literal.ts](src/lib/css_literal.ts) - CSS-literal parser and validator +- [css_ruleset_parser.ts](src/lib/css_ruleset_parser.ts) - CSS ruleset parsing, selector modification +- [modifiers.ts](src/lib/modifiers.ts) - Modifier definitions (breakpoints, states, pseudo-elements) + +**Example utilities:** + +- [example_class_utilities.ts](src/lib/example_class_utilities.ts) - Demo exports for testing node_modules extraction **Stylesheets:** @@ -83,7 +181,31 @@ Import [style.css](src/lib/style.css) + [theme.css](src/lib/theme.css) for base - [fuz.css](src/routes/fuz.css) - Generated optimized utility classes for this site - [fuz.gen.css.ts](src/routes/fuz.gen.css.ts) - Generator using `gen_fuz_css()` +### Examples - [examples/](examples/) + +Vite plugin examples: + +- [vite-svelte/](examples/vite-svelte/) - Svelte 5 example +- [vite-react/](examples/vite-react/) - React 19 example +- [vite-preact/](examples/vite-preact/) - Preact example +- [vite-solid/](examples/vite-solid/) - Solid example + +Each demonstrates token, composite, and literal classes with responsive/hover/dark modifiers. Uses classes from [example_class_utilities.ts](src/lib/example_class_utilities.ts) to verify node_modules extraction. + +**Important:** All 4 example App files must be kept in sync. When updating one, update all others with equivalent changes (accounting for framework differences like React's `className` vs others' `class`). The [vite_plugin_examples.test.ts](src/test/vite_plugin_examples.test.ts) verifies all examples produce the same CSS classes. + ### Tests - [src/test/](src/test/) +- [variable.test.ts](src/test/variable.test.ts) - StyleVariable type validation - [variables.test.ts](src/test/variables.test.ts) - Variable consistency (no duplicates, valid names) -- [css_class_helpers.test.ts](src/test/css_class_helpers.test.ts) - CSS extraction from Svelte/JS patterns +- [styles.test.ts](src/test/styles.test.ts) - Style output tests +- [css_classes.test.ts](src/test/css_classes.test.ts) - CssClasses collection tests +- [css_cache.test.ts](src/test/css_cache.test.ts) - Cache save/load, version invalidation, atomic writes +- [css_class_generation.test.ts](src/test/css_class_generation.test.ts) - CSS escaping, generation, interpreters +- [css_class_resolution.test.ts](src/test/css_class_resolution.test.ts) - Class resolution, cycle detection +- [css_class_extractor.test.ts](src/test/css_class_extractor.test.ts) - AST extraction, location tracking +- [css_class_extractor.jsx.test.ts](src/test/css_class_extractor.jsx.test.ts) - JSX-specific extraction +- [css_literal.test.ts](src/test/css_literal.test.ts) - CSS-literal parsing, validation, modifiers +- [css_ruleset_parser.test.ts](src/test/css_ruleset_parser.test.ts) - Ruleset parsing, selector modification +- [diagnostics.test.ts](src/test/diagnostics.test.ts) - Diagnostic formatting tests +- [vite_plugin_examples.test.ts](src/test/vite_plugin_examples.test.ts) - Integration tests building examples (skip with `SKIP_EXAMPLE_TESTS=1`) diff --git a/README.md b/README.md index 743335684..1835d4be9 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ -# Fuz CSS +# fuz_css [a fuzzy tuft of green moss](https://css.fuz.dev/) -> CSS framework and design system ðŸŒŋ magical organic stylesheets +> CSS with more utility ðŸŠī + +fuz_css is a CSS framework and design system for semantic HTML. +It styles elements by default and +integrates custom properties, themes, and utility classes into a complete system. +It's Svelte-first but works with plain HTML/JS/TS, React, Preact, Solid, and other JSX frameworks. -Fuz CSS is a CSS framework and design system built around semantic styles and style variables. It's in early alpha with breaking changes ahead. +Feedback and design input in +the [discussions](https://github.com/fuzdev/fuz_css/discussions). -- view the [docs](https://css.fuz.dev/docs) at [css.fuz.dev](https://css.fuz.dev/) -- help with feedback and design in the [issues](https://github.com/fuzdev/fuz_css/issues) - and [discussions](https://github.com/fuzdev/fuz_css/discussions) -- more about the stack at [fuz.dev](https://www.fuz.dev/) +View the [docs](https://css.fuz.dev/docs) at [css.fuz.dev](https://css.fuz.dev/). +More about the stack at [fuz.dev](https://www.fuz.dev/) ## License [ðŸĶ](https://wikipedia.org/wiki/Free_and_open-source_software) diff --git a/eslint.config.js b/eslint.config.js index 75527ca8a..c59655eb8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,4 +2,5 @@ import {configs, ts_config} from '@ryanatkn/eslint-config'; ts_config.rules['no-console'] = 1; -export default configs; +// Ignore examples directory - each example has its own tsconfig +export default [{ignores: ['examples/**']}, ...configs]; diff --git a/examples/vite-preact/index.html b/examples/vite-preact/index.html new file mode 100644 index 000000000..75bf0acd5 --- /dev/null +++ b/examples/vite-preact/index.html @@ -0,0 +1,12 @@ + + + + + + fuz_css + Preact + + +
+ + + diff --git a/examples/vite-preact/package-lock.json b/examples/vite-preact/package-lock.json new file mode 100644 index 000000000..f0bd659c1 --- /dev/null +++ b/examples/vite-preact/package-lock.json @@ -0,0 +1,2158 @@ +{ + "name": "vite-preact-fuz-css-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite-preact-fuz-css-example", + "dependencies": { + "preact": "^10" + }, + "devDependencies": { + "@fuzdev/fuz_css": "file:../..", + "@preact/preset-vite": "^2", + "acorn-jsx": "^5", + "typescript": "^5", + "vite": "^7" + } + }, + "../..": { + "name": "@fuzdev/fuz_css", + "version": "0.43.0", + "dev": true, + "license": "MIT", + "devDependencies": { + "@changesets/changelog-git": "^0.2.1", + "@fuzdev/fuz_code": "^0.39.0", + "@fuzdev/fuz_ui": "^0.177.1", + "@fuzdev/fuz_util": "^0.45.3", + "@ryanatkn/eslint-config": "^0.9.0", + "@ryanatkn/gro": "^0.185.0", + "@sveltejs/acorn-typescript": "^1.0.8", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/node": "^24.10.1", + "@webref/css": "^8.1.3", + "acorn-jsx": "^5.3.2", + "eslint": "^9.39.1", + "eslint-plugin-svelte": "^3.13.1", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vitest": "^4.0.15", + "zimmerframe": "^1.1.4", + "zod": "^4.1.13" + }, + "engines": { + "node": ">=22.15" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + }, + "peerDependencies": { + "@fuzdev/fuz_util": ">=0.42.0", + "@sveltejs/acorn-typescript": "^1", + "@webref/css": "^8", + "acorn-jsx": "^5", + "zimmerframe": "^1" + }, + "peerDependenciesMeta": { + "@fuzdev/fuz_util": { + "optional": true + }, + "@sveltejs/acorn-typescript": { + "optional": true + }, + "@webref/css": { + "optional": true + }, + "acorn-jsx": { + "optional": true + }, + "zimmerframe": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fuzdev/fuz_css": { + "resolved": "../..", + "link": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@preact/preset-vite": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.2.tgz", + "integrity": "sha512-K9wHlJOtkE+cGqlyQ5v9kL3Ge0Ql4LlIZjkUTL+1zf3nNdF88F9UZN6VTV8jdzBX9Fl7WSzeNMSDG7qECPmSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@prefresh/vite": "^2.4.1", + "@rollup/pluginutils": "^4.1.1", + "babel-plugin-transform-hook-names": "^1.0.2", + "debug": "^4.3.4", + "picocolors": "^1.1.1", + "vite-prerender-plugin": "^0.5.3" + }, + "peerDependencies": { + "@babel/core": "7.x", + "vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x" + } + }, + "node_modules/@prefresh/babel-plugin": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.2.tgz", + "integrity": "sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/core": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.5.9.tgz", + "integrity": "sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "preact": "^10.0.0 || ^11.0.0-0" + } + }, + "node_modules/@prefresh/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/vite": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@prefresh/vite/-/vite-2.4.11.tgz", + "integrity": "sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "0.5.2", + "@prefresh/core": "^1.5.0", + "@prefresh/utils": "^1.2.0", + "@rollup/pluginutils": "^4.2.1" + }, + "peerDependencies": { + "preact": "^10.4.0 || ^11.0.0-0", + "vite": ">=2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/babel-plugin-transform-hook-names": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.12.10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/simple-code-frame": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/simple-code-frame/-/simple-code-frame-1.3.0.tgz", + "integrity": "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.6.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "1.0.0-pre2", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz", + "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-prerender-plugin": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.12.tgz", + "integrity": "sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.8.0", + "magic-string": "0.x >= 0.26.0", + "node-html-parser": "^6.1.12", + "simple-code-frame": "^1.3.0", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" + }, + "peerDependencies": { + "vite": "5.x || 6.x || 7.x" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/examples/vite-preact/package.json b/examples/vite-preact/package.json new file mode 100644 index 000000000..a386926a6 --- /dev/null +++ b/examples/vite-preact/package.json @@ -0,0 +1,20 @@ +{ + "name": "vite-preact-fuz-css-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "preact": "^10" + }, + "devDependencies": { + "@fuzdev/fuz_css": "file:../..", + "@preact/preset-vite": "^2", + "acorn-jsx": "^5", + "typescript": "^5", + "vite": "^7" + } +} diff --git a/examples/vite-preact/src/App.tsx b/examples/vite-preact/src/App.tsx new file mode 100644 index 000000000..945b434e9 --- /dev/null +++ b/examples/vite-preact/src/App.tsx @@ -0,0 +1,152 @@ +import {useState} from 'preact/hooks'; + +// Import from node_modules to verify extraction works for dependencies +import { + // Naming patterns (all CLASS_NAME_PATTERN suffix variants) + demoClass, + demo_class, + demoClasses, + demo_classes, + demoClassName, + demo_class_name, + demoClassNames, + demo_class_names, + demoClassList, + demo_class_list, + demoClassLists, + demo_class_lists, + DEMO_CLASS, + // Expression patterns + ternaryClass, + logicalClass, + arrayClasses, + objectClasses, + // Comment hint examples + fromComment, +} from '@fuzdev/fuz_css/example_class_utilities.js'; + +export const App = () => { + const [count, setCount] = useState(0); + + return ( +
+
+
+

fuz_css + Preact

+

Utility classes generated on-demand via Vite plugin (docs, source)

+
+ + {/* Class types */} +
+

Class types

+ +
+

Token classes

+
.p_md .bg_d_2
+
.pl_xl5 .font_size_lg
+
.shadow_sm
+
+ +
+

Composite classes

+
.box
+
+ .ellipsis -- this text truncates with ellipsis when it overflows +
+
+ +
+

Literal classes

+
.opacity:60%
+
.color:var(--color_j_5)
+
.box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
+
+ + {/* Modifiers */} +
+

Modifiers

+ +
+

Responsive

+
+
+

.column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens

+
+
+

Resize the window to see the layout switch from column to row

+
+
+

.min-width(543px):font_size_lg -- arbitrary breakpoint

+
+ +
+

Interactive

+
+ + .hover:border_color_b .hover:outline_color_b .active:border_color_d .active:outline_color_d +
+
+ + .hover:border_color_g .hover:outline_color_g .active:border_color_h .active:outline_color_h +
+
+
+ + {/* Extraction */} +
+

Extraction

+

Classes detected via naming conventions, expressions, and comments (examples imported from node_modules to verify dependency scanning)

+ +
+

Naming patterns

+
demoClass: .{demoClass}
+
demo_class: .{demo_class}
+
DEMO_CLASS: .{DEMO_CLASS}
+
demoClasses: .mb_xs2 .ml_xs
+
demo_classes: .mb_xs .ml_sm
+
demoClassName: .{demoClassName}
+
demo_class_name: .{demo_class_name}
+
demoClassNames: .mb_lg .ml_md
+
demo_class_names: .mb_xl .ml_lg
+
demoClassList: .{demoClassList}
+
demo_class_list: .{demo_class_list}
+
demoClassLists: .mb_xl4 .ml_xl
+
demo_class_lists: .mb_xl5 .ml_xl2
+
+ +
+

Expression patterns

+
+ {`true ? 'mt_xs' : 'mt_sm'`} → .{ternaryClass} (both branches extracted) +
+
+ {`true && 'mt_md'`} → .{logicalClass} +
+
+ {`['mt_lg', 'mt_xl']`} → .{arrayClasses.join(', .')} +
+
+ {`{ mt_xl2: 'mt_xl2', mt_xl3: 'mt_xl3' }`} → keys extracted from object +
+
+ +
+

Comment hints

+
+ // @fuz-classes {fromComment} → .{fromComment} +
+
+
+ + +
+
+ ); +}; diff --git a/examples/vite-preact/src/main.tsx b/examples/vite-preact/src/main.tsx new file mode 100644 index 000000000..7e2989347 --- /dev/null +++ b/examples/vite-preact/src/main.tsx @@ -0,0 +1,8 @@ +import '@fuzdev/fuz_css/style.css'; +import '@fuzdev/fuz_css/theme.css'; +import 'virtual:fuz.css'; + +import {render} from 'preact'; +import {App} from './App.tsx'; + +render(, document.getElementById('root')!); diff --git a/examples/vite-preact/src/vite-env.d.ts b/examples/vite-preact/src/vite-env.d.ts new file mode 100644 index 000000000..241d205b0 --- /dev/null +++ b/examples/vite-preact/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module 'virtual:fuz.css' { + const css: string; + export default css; +} diff --git a/examples/vite-preact/tsconfig.json b/examples/vite-preact/tsconfig.json new file mode 100644 index 000000000..79c4a1d61 --- /dev/null +++ b/examples/vite-preact/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/examples/vite-preact/vite.config.ts b/examples/vite-preact/vite.config.ts new file mode 100644 index 000000000..88f82c483 --- /dev/null +++ b/examples/vite-preact/vite.config.ts @@ -0,0 +1,14 @@ +import {defineConfig} from 'vite'; +import preact from '@preact/preset-vite'; +import jsx from 'acorn-jsx'; +import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.js'; + +export default defineConfig({ + plugins: [ + // Plugin order doesn't matter for preact (fuz_css uses enforce: 'pre') + preact(), + vite_plugin_fuz_css({ + acorn_plugins: [jsx()], + }), + ], +}); diff --git a/examples/vite-react/index.html b/examples/vite-react/index.html new file mode 100644 index 000000000..7ede35528 --- /dev/null +++ b/examples/vite-react/index.html @@ -0,0 +1,12 @@ + + + + + + fuz_css + React + + +
+ + + diff --git a/examples/vite-react/package-lock.json b/examples/vite-react/package-lock.json new file mode 100644 index 000000000..6ad9b4ae6 --- /dev/null +++ b/examples/vite-react/package-lock.json @@ -0,0 +1,1909 @@ +{ + "name": "vite-react-fuz-css-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite-react-fuz-css-example", + "dependencies": { + "react": "^19", + "react-dom": "^19" + }, + "devDependencies": { + "@fuzdev/fuz_css": "file:../..", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^5", + "acorn-jsx": "^5", + "typescript": "^5", + "vite": "^7" + } + }, + "../..": { + "name": "@fuzdev/fuz_css", + "version": "0.43.0", + "dev": true, + "license": "MIT", + "devDependencies": { + "@changesets/changelog-git": "^0.2.1", + "@fuzdev/fuz_code": "^0.39.0", + "@fuzdev/fuz_ui": "^0.177.1", + "@fuzdev/fuz_util": "^0.45.3", + "@ryanatkn/eslint-config": "^0.9.0", + "@ryanatkn/gro": "^0.185.0", + "@sveltejs/acorn-typescript": "^1.0.8", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/node": "^24.10.1", + "@webref/css": "^8.1.3", + "acorn-jsx": "^5.3.2", + "eslint": "^9.39.1", + "eslint-plugin-svelte": "^3.13.1", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vitest": "^4.0.15", + "zimmerframe": "^1.1.4", + "zod": "^4.1.13" + }, + "engines": { + "node": ">=22.15" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + }, + "peerDependencies": { + "@fuzdev/fuz_util": ">=0.42.0", + "@sveltejs/acorn-typescript": "^1", + "@webref/css": "^8", + "acorn-jsx": "^5", + "zimmerframe": "^1" + }, + "peerDependenciesMeta": { + "@fuzdev/fuz_util": { + "optional": true + }, + "@sveltejs/acorn-typescript": { + "optional": true + }, + "@webref/css": { + "optional": true + }, + "acorn-jsx": { + "optional": true + }, + "zimmerframe": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fuzdev/fuz_css": { + "resolved": "../..", + "link": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/examples/vite-react/package.json b/examples/vite-react/package.json new file mode 100644 index 000000000..30f1fb351 --- /dev/null +++ b/examples/vite-react/package.json @@ -0,0 +1,23 @@ +{ + "name": "vite-react-fuz-css-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19", + "react-dom": "^19" + }, + "devDependencies": { + "@fuzdev/fuz_css": "file:../..", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^5", + "acorn-jsx": "^5", + "typescript": "^5", + "vite": "^7" + } +} diff --git a/examples/vite-react/src/App.tsx b/examples/vite-react/src/App.tsx new file mode 100644 index 000000000..c586a2ae5 --- /dev/null +++ b/examples/vite-react/src/App.tsx @@ -0,0 +1,152 @@ +import {useState} from 'react'; + +// Import from node_modules to verify extraction works for dependencies +import { + // Naming patterns (all CLASS_NAME_PATTERN suffix variants) + demoClass, + demo_class, + demoClasses, + demo_classes, + demoClassName, + demo_class_name, + demoClassNames, + demo_class_names, + demoClassList, + demo_class_list, + demoClassLists, + demo_class_lists, + DEMO_CLASS, + // Expression patterns + ternaryClass, + logicalClass, + arrayClasses, + objectClasses, + // Comment hint examples + fromComment, +} from '@fuzdev/fuz_css/example_class_utilities.js'; + +export const App = () => { + const [count, setCount] = useState(0); + + return ( +
+
+
+

fuz_css + React

+

Utility classes generated on-demand via Vite plugin (docs, source)

+
+ + {/* Class types */} +
+

Class types

+ +
+

Token classes

+
.p_md .bg_d_2
+
.pl_xl5 .font_size_lg
+
.shadow_sm
+
+ +
+

Composite classes

+
.box
+
+ .ellipsis -- this text truncates with ellipsis when it overflows +
+
+ +
+

Literal classes

+
.opacity:60%
+
.color:var(--color_j_5)
+
.box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
+
+ + {/* Modifiers */} +
+

Modifiers

+ +
+

Responsive

+
+
+

.column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens

+
+
+

Resize the window to see the layout switch from column to row

+
+
+

.min-width(543px):font_size_lg -- arbitrary breakpoint

+
+ +
+

Interactive

+
+ + .hover:border_color_b .hover:outline_color_b .active:border_color_d .active:outline_color_d +
+
+ + .hover:border_color_g .hover:outline_color_g .active:border_color_h .active:outline_color_h +
+
+
+ + {/* Extraction */} +
+

Extraction

+

Classes detected via naming conventions, expressions, and comments (examples imported from node_modules to verify dependency scanning)

+ +
+

Naming patterns

+
demoClass: .{demoClass}
+
demo_class: .{demo_class}
+
DEMO_CLASS: .{DEMO_CLASS}
+
demoClasses: .mb_xs2 .ml_xs
+
demo_classes: .mb_xs .ml_sm
+
demoClassName: .{demoClassName}
+
demo_class_name: .{demo_class_name}
+
demoClassNames: .mb_lg .ml_md
+
demo_class_names: .mb_xl .ml_lg
+
demoClassList: .{demoClassList}
+
demo_class_list: .{demo_class_list}
+
demoClassLists: .mb_xl4 .ml_xl
+
demo_class_lists: .mb_xl5 .ml_xl2
+
+ +
+

Expression patterns

+
+ {`true ? 'mt_xs' : 'mt_sm'`} → .{ternaryClass} (both branches extracted) +
+
+ {`true && 'mt_md'`} → .{logicalClass} +
+
+ {`['mt_lg', 'mt_xl']`} → .{arrayClasses.join(', .')} +
+
+ {`{ mt_xl2: 'mt_xl2', mt_xl3: 'mt_xl3' }`} → keys extracted from object +
+
+ +
+

Comment hints

+
+ // @fuz-classes {fromComment} → .{fromComment} +
+
+
+ +
+

This demos a subset of features.
See the docs and source code for more.

+
+
+
+ ); +}; diff --git a/examples/vite-react/src/main.tsx b/examples/vite-react/src/main.tsx new file mode 100644 index 000000000..4f3067a01 --- /dev/null +++ b/examples/vite-react/src/main.tsx @@ -0,0 +1,8 @@ +import '@fuzdev/fuz_css/style.css'; +import '@fuzdev/fuz_css/theme.css'; +import 'virtual:fuz.css'; + +import {createRoot} from 'react-dom/client'; +import {App} from './App.tsx'; + +createRoot(document.getElementById('root')!).render(); diff --git a/examples/vite-react/src/vite-env.d.ts b/examples/vite-react/src/vite-env.d.ts new file mode 100644 index 000000000..241d205b0 --- /dev/null +++ b/examples/vite-react/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module 'virtual:fuz.css' { + const css: string; + export default css; +} diff --git a/examples/vite-react/tsconfig.json b/examples/vite-react/tsconfig.json new file mode 100644 index 000000000..a31bbb0e8 --- /dev/null +++ b/examples/vite-react/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/examples/vite-react/vite.config.ts b/examples/vite-react/vite.config.ts new file mode 100644 index 000000000..fa39ab941 --- /dev/null +++ b/examples/vite-react/vite.config.ts @@ -0,0 +1,14 @@ +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; +import jsx from 'acorn-jsx'; +import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.js'; + +export default defineConfig({ + plugins: [ + // Plugin order doesn't matter for react (fuz_css uses enforce: 'pre') + react(), + vite_plugin_fuz_css({ + acorn_plugins: [jsx()], + }), + ], +}); diff --git a/examples/vite-solid/index.html b/examples/vite-solid/index.html new file mode 100644 index 000000000..7e289f88e --- /dev/null +++ b/examples/vite-solid/index.html @@ -0,0 +1,12 @@ + + + + + + fuz_css + Solid + + +
+ + + diff --git a/examples/vite-solid/package-lock.json b/examples/vite-solid/package-lock.json new file mode 100644 index 000000000..02beccdcb --- /dev/null +++ b/examples/vite-solid/package-lock.json @@ -0,0 +1,2007 @@ +{ + "name": "vite-solid-fuz-css-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite-solid-fuz-css-example", + "dependencies": { + "solid-js": "^1" + }, + "devDependencies": { + "@fuzdev/fuz_css": "file:../..", + "acorn-jsx": "^5", + "typescript": "^5", + "vite": "^7", + "vite-plugin-solid": "^2" + } + }, + "../..": { + "name": "@fuzdev/fuz_css", + "version": "0.43.0", + "dev": true, + "license": "MIT", + "devDependencies": { + "@changesets/changelog-git": "^0.2.1", + "@fuzdev/fuz_code": "^0.39.0", + "@fuzdev/fuz_ui": "^0.177.1", + "@fuzdev/fuz_util": "^0.45.3", + "@ryanatkn/eslint-config": "^0.9.0", + "@ryanatkn/gro": "^0.185.0", + "@sveltejs/acorn-typescript": "^1.0.8", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/node": "^24.10.1", + "@webref/css": "^8.1.3", + "acorn-jsx": "^5.3.2", + "eslint": "^9.39.1", + "eslint-plugin-svelte": "^3.13.1", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vitest": "^4.0.15", + "zimmerframe": "^1.1.4", + "zod": "^4.1.13" + }, + "engines": { + "node": ">=22.15" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + }, + "peerDependencies": { + "@fuzdev/fuz_util": ">=0.42.0", + "@sveltejs/acorn-typescript": "^1", + "@webref/css": "^8", + "acorn-jsx": "^5", + "zimmerframe": "^1" + }, + "peerDependenciesMeta": { + "@fuzdev/fuz_util": { + "optional": true + }, + "@sveltejs/acorn-typescript": { + "optional": true + }, + "@webref/css": { + "optional": true + }, + "acorn-jsx": { + "optional": true + }, + "zimmerframe": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fuzdev/fuz_css": { + "resolved": "../..", + "link": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.3.tgz", + "integrity": "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.20.7", + "html-entities": "2.3.3", + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.20.12" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/babel-preset-solid": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.10.tgz", + "integrity": "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jsx-dom-expressions": "^0.40.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "solid-js": "^1.9.10" + }, + "peerDependenciesMeta": { + "solid-js": { + "optional": true + } + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", + "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/solid-js": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", + "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.0" + } + }, + "node_modules/solid-refresh": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz", + "integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.23.6", + "@babel/helper-module-imports": "^7.22.15", + "@babel/types": "^7.23.6" + }, + "peerDependencies": { + "solid-js": "^1.3" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-solid": { + "version": "2.11.10", + "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.10.tgz", + "integrity": "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.3", + "@types/babel__core": "^7.20.4", + "babel-preset-solid": "^1.8.4", + "merge-anything": "^5.1.7", + "solid-refresh": "^0.6.3", + "vitefu": "^1.0.4" + }, + "peerDependencies": { + "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", + "solid-js": "^1.7.2", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@testing-library/jest-dom": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/examples/vite-solid/package.json b/examples/vite-solid/package.json new file mode 100644 index 000000000..3c521ae07 --- /dev/null +++ b/examples/vite-solid/package.json @@ -0,0 +1,20 @@ +{ + "name": "vite-solid-fuz-css-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "solid-js": "^1" + }, + "devDependencies": { + "@fuzdev/fuz_css": "file:../..", + "acorn-jsx": "^5", + "typescript": "^5", + "vite": "^7", + "vite-plugin-solid": "^2" + } +} diff --git a/examples/vite-solid/src/App.tsx b/examples/vite-solid/src/App.tsx new file mode 100644 index 000000000..dc4582a5e --- /dev/null +++ b/examples/vite-solid/src/App.tsx @@ -0,0 +1,152 @@ +import {createSignal, For} from 'solid-js'; + +// Import from node_modules to verify extraction works for dependencies +import { + // Naming patterns (all CLASS_NAME_PATTERN suffix variants) + demoClass, + demo_class, + demoClasses, + demo_classes, + demoClassName, + demo_class_name, + demoClassNames, + demo_class_names, + demoClassList, + demo_class_list, + demoClassLists, + demo_class_lists, + DEMO_CLASS, + // Expression patterns + ternaryClass, + logicalClass, + arrayClasses, + objectClasses, + // Comment hint examples + fromComment, +} from '@fuzdev/fuz_css/example_class_utilities.js'; + +export const App = () => { + const [count, setCount] = createSignal(0); + + return ( +
+
+
+

fuz_css + Solid

+

Utility classes generated on-demand via Vite plugin (docs, source)

+
+ + {/* Class types */} +
+

Class types

+ +
+

Token classes

+
.p_md .bg_d_2
+
.pl_xl5 .font_size_lg
+
.shadow_sm
+
+ +
+

Composite classes

+
.box
+
+ .ellipsis -- this text truncates with ellipsis when it overflows +
+
+ +
+

Literal classes

+
.opacity:60%
+
.color:var(--color_j_5)
+
.box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
+
+ + {/* Modifiers */} +
+

Modifiers

+ +
+

Responsive

+
+
+

.column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens

+
+
+

Resize the window to see the layout switch from column to row

+
+
+

.min-width(543px):font_size_lg -- arbitrary breakpoint

+
+ +
+

Interactive

+
+ + .hover:border_color_b .hover:outline_color_b .active:border_color_d .active:outline_color_d +
+
+ + .hover:border_color_g .hover:outline_color_g .active:border_color_h .active:outline_color_h +
+
+
+ + {/* Extraction */} +
+

Extraction

+

Classes detected via naming conventions, expressions, and comments (examples imported from node_modules to verify dependency scanning)

+ +
+

Naming patterns

+
demoClass: .{demoClass}
+
demo_class: .{demo_class}
+
DEMO_CLASS: .{DEMO_CLASS}
+
demoClasses: .mb_xs2 .ml_xs
+
demo_classes: .mb_xs .ml_sm
+
demoClassName: .{demoClassName}
+
demo_class_name: .{demo_class_name}
+
demoClassNames: .mb_lg .ml_md
+
demo_class_names: .mb_xl .ml_lg
+
demoClassList: .{demoClassList}
+
demo_class_list: .{demo_class_list}
+
demoClassLists: .mb_xl4 .ml_xl
+
demo_class_lists: .mb_xl5 .ml_xl2
+
+ +
+

Expression patterns

+
+ {`true ? 'mt_xs' : 'mt_sm'`} → .{ternaryClass} (both branches extracted) +
+
+ {`true && 'mt_md'`} → .{logicalClass} +
+
+ {`['mt_lg', 'mt_xl']`} → .{arrayClasses.join(', .')} +
+
+ {`{ mt_xl2: 'mt_xl2', mt_xl3: 'mt_xl3' }`} → keys extracted from object +
+
+ +
+

Comment hints

+
+ // @fuz-classes {fromComment} → .{fromComment} +
+
+
+ +
+

This demos a subset of features.
See the docs and source code for more.

+
+
+
+ ); +}; diff --git a/examples/vite-solid/src/main.tsx b/examples/vite-solid/src/main.tsx new file mode 100644 index 000000000..d4ca9082f --- /dev/null +++ b/examples/vite-solid/src/main.tsx @@ -0,0 +1,8 @@ +import '@fuzdev/fuz_css/style.css'; +import '@fuzdev/fuz_css/theme.css'; +import 'virtual:fuz.css'; + +import {render} from 'solid-js/web'; +import {App} from './App.tsx'; + +render(() => , document.getElementById('root')!); diff --git a/examples/vite-solid/src/vite-env.d.ts b/examples/vite-solid/src/vite-env.d.ts new file mode 100644 index 000000000..241d205b0 --- /dev/null +++ b/examples/vite-solid/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module 'virtual:fuz.css' { + const css: string; + export default css; +} diff --git a/examples/vite-solid/tsconfig.json b/examples/vite-solid/tsconfig.json new file mode 100644 index 000000000..afbd76a06 --- /dev/null +++ b/examples/vite-solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/examples/vite-solid/vite.config.ts b/examples/vite-solid/vite.config.ts new file mode 100644 index 000000000..2b31a7fad --- /dev/null +++ b/examples/vite-solid/vite.config.ts @@ -0,0 +1,14 @@ +import {defineConfig} from 'vite'; +import solid from 'vite-plugin-solid'; +import jsx from 'acorn-jsx'; +import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.js'; + +export default defineConfig({ + plugins: [ + // fuz_css must be listed first for solid (enforce: 'pre' alone isn't sufficient) + vite_plugin_fuz_css({ + acorn_plugins: [jsx()], + }), + solid(), + ], +}); diff --git a/examples/vite-svelte/index.html b/examples/vite-svelte/index.html new file mode 100644 index 000000000..d3652ec98 --- /dev/null +++ b/examples/vite-svelte/index.html @@ -0,0 +1,12 @@ + + + + + + fuz_css + Svelte + + +
+ + + diff --git a/examples/vite-svelte/package-lock.json b/examples/vite-svelte/package-lock.json new file mode 100644 index 000000000..bc9525ad8 --- /dev/null +++ b/examples/vite-svelte/package-lock.json @@ -0,0 +1,1439 @@ +{ + "name": "vite-svelte-fuz-css-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite-svelte-fuz-css-example", + "dependencies": { + "svelte": "^5" + }, + "devDependencies": { + "@fuzdev/fuz_css": "file:../..", + "@sveltejs/vite-plugin-svelte": "^6", + "typescript": "^5", + "vite": "^7" + } + }, + "../..": { + "name": "@fuzdev/fuz_css", + "version": "0.43.0", + "dev": true, + "license": "MIT", + "devDependencies": { + "@changesets/changelog-git": "^0.2.1", + "@fuzdev/fuz_code": "^0.39.0", + "@fuzdev/fuz_ui": "^0.177.1", + "@fuzdev/fuz_util": "^0.45.3", + "@ryanatkn/eslint-config": "^0.9.0", + "@ryanatkn/gro": "^0.185.0", + "@sveltejs/acorn-typescript": "^1.0.8", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/node": "^24.10.1", + "@webref/css": "^8.1.3", + "acorn-jsx": "^5.3.2", + "eslint": "^9.39.1", + "eslint-plugin-svelte": "^3.13.1", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vitest": "^4.0.15", + "zimmerframe": "^1.1.4", + "zod": "^4.1.13" + }, + "engines": { + "node": ">=22.15" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + }, + "peerDependencies": { + "@fuzdev/fuz_util": ">=0.42.0", + "@sveltejs/acorn-typescript": "^1", + "@webref/css": "^8", + "acorn-jsx": "^5", + "zimmerframe": "^1" + }, + "peerDependenciesMeta": { + "@fuzdev/fuz_util": { + "optional": true + }, + "@sveltejs/acorn-typescript": { + "optional": true + }, + "@webref/css": { + "optional": true + }, + "acorn-jsx": { + "optional": true + }, + "zimmerframe": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fuzdev/fuz_css": { + "resolved": "../..", + "link": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", + "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz", + "integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.1.tgz", + "integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.46.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.3.tgz", + "integrity": "sha512-Y5juST3x+/ySty5tYJCVWa6Corkxpt25bUZQHqOceg9xfMUtDsFx6rCsG6cYf1cA6vzDi66HIvaki0byZZX95A==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.5.0", + "esm-env": "^1.2.1", + "esrap": "^2.2.1", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/examples/vite-svelte/package.json b/examples/vite-svelte/package.json new file mode 100644 index 000000000..6595c48c4 --- /dev/null +++ b/examples/vite-svelte/package.json @@ -0,0 +1,19 @@ +{ + "name": "vite-svelte-fuz-css-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "svelte": "^5" + }, + "devDependencies": { + "@fuzdev/fuz_css": "file:../..", + "@sveltejs/vite-plugin-svelte": "^6", + "typescript": "^5", + "vite": "^7" + } +} diff --git a/examples/vite-svelte/src/App.svelte b/examples/vite-svelte/src/App.svelte new file mode 100644 index 000000000..1d7fb611c --- /dev/null +++ b/examples/vite-svelte/src/App.svelte @@ -0,0 +1,178 @@ + + +
+
+
+

fuz_css + Svelte

+

+ Utility classes generated on-demand via Vite plugin (docs, source) +

+
+ + +
+

Class types

+ +
+

Token classes

+
.p_md .bg_d_2
+
.pl_xl5 .font_size_lg
+
.shadow_sm
+
+ +
+

Composite classes

+
.box
+
+ .ellipsis -- this text truncates with ellipsis when it overflows +
+
+ +
+

Literal classes

+
.opacity:60%
+
.color:var(--color_j_5)
+
+ .box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces) +
+
+
+ + +
+

Modifiers

+ +
+

Responsive

+
+
+

.column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens

+
+
+

Resize the window to see the layout switch from column to row

+
+
+

+ .min-width(543px):font_size_lg -- arbitrary breakpoint +

+
+ +
+

Interactive

+
+ + .hover:border_color_b .hover:outline_color_b .active:border_color_d + .active:outline_color_d +
+
+ + .hover:border_color_g .hover:outline_color_g .active:border_color_h + .active:outline_color_h +
+
+
+ + +
+

Extraction

+

+ Classes detected via naming conventions, expressions, and comments (examples imported from node_modules to verify dependency scanning) +

+ +
+

Naming patterns

+
demoClass: .{demoClass}
+
demo_class: .{demo_class}
+
DEMO_CLASS: .{DEMO_CLASS}
+
demoClasses: .mb_xs2 .ml_xs
+
demo_classes: .mb_xs .ml_sm
+
demoClassName: .{demoClassName}
+
demo_class_name: .{demo_class_name}
+
demoClassNames: .mb_lg .ml_md
+
demo_class_names: .mb_xl .ml_lg
+
demoClassList: .{demoClassList}
+
demo_class_list: .{demo_class_list}
+
demoClassLists: .mb_xl4 .ml_xl
+
demo_class_lists: .mb_xl5 .ml_xl2
+
+ +
+

Expression patterns

+
+ {"true ? 'mt_xs' : 'mt_sm'"} → .{ternaryClass} (both branches extracted) +
+
+ {"true && 'mt_md'"} → .{logicalClass} +
+
+ {"['mt_lg', 'mt_xl']"} → .{arrayClasses.join(', .')} +
+
+ {`{ mt_xl2: 'mt_xl2', mt_xl3: 'mt_xl3' }`} → keys extracted from object +
+
+ +
+

Comment hints

+
+ // @fuz-classes {fromComment} → .{fromComment} +
+
+
+ +
+

+ This demos a subset of features.
See the + docs + and + source code for + more. +

+
+
+
diff --git a/examples/vite-svelte/src/main.ts b/examples/vite-svelte/src/main.ts new file mode 100644 index 000000000..f50b05321 --- /dev/null +++ b/examples/vite-svelte/src/main.ts @@ -0,0 +1,8 @@ +import '@fuzdev/fuz_css/style.css'; +import '@fuzdev/fuz_css/theme.css'; +import 'virtual:fuz.css'; + +import {mount} from 'svelte'; +import App from './App.svelte'; + +mount(App, {target: document.getElementById('root')!}); diff --git a/examples/vite-svelte/src/vite-env.d.ts b/examples/vite-svelte/src/vite-env.d.ts new file mode 100644 index 000000000..241d205b0 --- /dev/null +++ b/examples/vite-svelte/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module 'virtual:fuz.css' { + const css: string; + export default css; +} diff --git a/examples/vite-svelte/tsconfig.json b/examples/vite-svelte/tsconfig.json new file mode 100644 index 000000000..394f77eb2 --- /dev/null +++ b/examples/vite-svelte/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/examples/vite-svelte/vite.config.ts b/examples/vite-svelte/vite.config.ts new file mode 100644 index 000000000..d94ba0216 --- /dev/null +++ b/examples/vite-svelte/vite.config.ts @@ -0,0 +1,11 @@ +import {defineConfig} from 'vite'; +import {svelte} from '@sveltejs/vite-plugin-svelte'; +import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.js'; + +export default defineConfig({ + plugins: [ + // fuz_css must be listed first for svelte (enforce: 'pre' alone isn't sufficient) + vite_plugin_fuz_css(), + svelte(), + ], +}); diff --git a/package-lock.json b/package-lock.json index 4d9e22e8c..e5f1ffea6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,19 @@ "license": "MIT", "devDependencies": { "@changesets/changelog-git": "^0.2.1", - "@fuzdev/fuz_code": "^0.38.0", - "@fuzdev/fuz_ui": "^0.177.0", - "@fuzdev/fuz_util": "^0.45.1", + "@fuzdev/fuz_code": "^0.39.0", + "@fuzdev/fuz_ui": "^0.177.1", + "@fuzdev/fuz_util": "^0.45.3", "@ryanatkn/eslint-config": "^0.9.0", - "@ryanatkn/gro": "^0.184.0", + "@ryanatkn/gro": "^0.185.0", + "@sveltejs/acorn-typescript": "^1.0.8", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.49.1", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@types/node": "^24.10.1", + "@webref/css": "^8.1.3", + "acorn-jsx": "^5.3.2", "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.1", "prettier": "^3.7.4", @@ -30,6 +33,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", "vitest": "^4.0.15", + "zimmerframe": "^1.1.4", "zod": "^4.1.13" }, "engines": { @@ -39,11 +43,27 @@ "url": "https://www.ryanatkn.com/funding" }, "peerDependencies": { - "@fuzdev/fuz_util": ">=0.42.0" + "@fuzdev/fuz_util": ">=0.42.0", + "@sveltejs/acorn-typescript": "^1", + "@webref/css": "^8", + "acorn-jsx": "^5", + "zimmerframe": "^1" }, "peerDependenciesMeta": { "@fuzdev/fuz_util": { "optional": true + }, + "@sveltejs/acorn-typescript": { + "optional": true + }, + "@webref/css": { + "optional": true + }, + "acorn-jsx": { + "optional": true + }, + "zimmerframe": { + "optional": true } } }, @@ -724,9 +744,9 @@ } }, "node_modules/@fuzdev/fuz_code": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.38.0.tgz", - "integrity": "sha512-11ow5NZbgdDnvNH46LDYaVdWjjo1yQSJRpkYr2OhZGcxc2FiZ/eU16T0xT6OhSJg+pRM9SRUPyNLI2k3jiW7Ig==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.39.0.tgz", + "integrity": "sha512-0YlInOGBNlnCAp3TKKv6gSjoyco3JAI9e/epplmnQMnAAHRTHvblJDWv8ePiFUJBQJJqGsJ2NldxswK9Vp7LXA==", "dev": true, "license": "MIT", "engines": { @@ -771,9 +791,9 @@ } }, "node_modules/@fuzdev/fuz_ui": { - "version": "0.177.0", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.177.0.tgz", - "integrity": "sha512-b9LEzjL78NHtEI0yDOCTz/XyUjm/sdrCDSQ4jNHOtpANOF8cRijemldXWcfs9UEoexnIK8wDg8WV4OnpD5Hvqw==", + "version": "0.177.1", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.177.1.tgz", + "integrity": "sha512-pdrqh3K4/9gTO1tz5EOv28GN0f8wCAd1ufZFd5ckMi82TMrS7bM/rJZOktzFomjV0WuyVlqsLw9VsSROiF9cBw==", "dev": true, "license": "MIT", "engines": { @@ -810,9 +830,9 @@ } }, "node_modules/@fuzdev/fuz_util": { - "version": "0.45.1", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.45.1.tgz", - "integrity": "sha512-szJ6FPXkeuNzoqxXwiC1q9xMiWZM37wiCyGdFLVVBq8FxwIpG1MAup/ZFyQC22QLg8sdybCbPadWiaYYUhhtUA==", + "version": "0.45.3", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.45.3.tgz", + "integrity": "sha512-N0xaUwFxGG1FuEkcVqB4t8Gqs2ReCVmmQf1kI7gErGuyKV9mRycHSFWFyT7/hQ9K4/0epsZj4cejj5fYIjkG/Q==", "dev": true, "license": "MIT", "engines": { @@ -1541,9 +1561,9 @@ } }, "node_modules/@ryanatkn/gro": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/@ryanatkn/gro/-/gro-0.184.0.tgz", - "integrity": "sha512-J8Us3xspyjVBCuOG36+4X9qZiH5ydrjRRSN9Pfwk+xNgJsH5v11GXTl9PLkKvHJVHJ8qb4KFX/XA1k8bAmIM5g==", + "version": "0.185.0", + "resolved": "https://registry.npmjs.org/@ryanatkn/gro/-/gro-0.185.0.tgz", + "integrity": "sha512-Sr2Jrkz/RcyPP8N4BXzyRQhABgGAgsMJ0OpMpNVfN4WozKDbrN6YUZsHgZpF/byT5YSUz+6Hlhsx+4xrTMObKg==", "dev": true, "license": "MIT", "dependencies": { @@ -1552,8 +1572,8 @@ "esm-env": "^1.2.2", "mri": "^1.2.0", "oxc-parser": "^0.99.0", - "prettier": "^3.6.2", - "prettier-plugin-svelte": "^3.4.0", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.1", "ts-blank-space": "^0.6.2", "tslib": "^2.8.1", "zod": "^4.1.13" @@ -1625,9 +1645,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", + "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2203,6 +2223,16 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webref/css": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@webref/css/-/css-8.1.3.tgz", + "integrity": "sha512-lSniLpIAm3G5o0NJxUr1Ci0mXCxIwQ31cyvdU5L0jnWXb41I6Sg0KWqxZXRVh6Jgf03egoTCaqhAn4S3VRzCZQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "css-tree": "^3.1.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2429,6 +2459,21 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3142,6 +3187,14 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0", + "peer": true + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4718,9 +4771,9 @@ } }, "node_modules/zimmerframe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index f98ae64da..cd7d7a3ba 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@fuzdev/fuz_css", "version": "0.43.0", - "description": "CSS framework and design system", - "motto": "magical organic stylesheets", - "glyph": "ðŸŒŋ", + "description": "CSS framework and design system for semantic HTML", + "motto": "CSS with more utility", + "glyph": "ðŸŠī", "logo": "logo.svg", "logo_alt": "a fuzzy tuft of green moss", "license": "MIT", @@ -30,25 +30,44 @@ "node": ">=22.15" }, "peerDependencies": { - "@fuzdev/fuz_util": ">=0.42.0" + "@fuzdev/fuz_util": ">=0.42.0", + "@sveltejs/acorn-typescript": "^1", + "@webref/css": "^8", + "acorn-jsx": "^5", + "zimmerframe": "^1" }, "peerDependenciesMeta": { "@fuzdev/fuz_util": { "optional": true + }, + "@sveltejs/acorn-typescript": { + "optional": true + }, + "@webref/css": { + "optional": true + }, + "acorn-jsx": { + "optional": true + }, + "zimmerframe": { + "optional": true } }, "devDependencies": { "@changesets/changelog-git": "^0.2.1", - "@fuzdev/fuz_code": "^0.38.0", - "@fuzdev/fuz_ui": "^0.177.0", - "@fuzdev/fuz_util": "^0.45.1", + "@fuzdev/fuz_code": "^0.39.0", + "@fuzdev/fuz_ui": "^0.177.1", + "@fuzdev/fuz_util": "^0.45.3", "@ryanatkn/eslint-config": "^0.9.0", - "@ryanatkn/gro": "^0.184.0", + "@ryanatkn/gro": "^0.185.0", + "@sveltejs/acorn-typescript": "^1.0.8", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.49.1", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@types/node": "^24.10.1", + "@webref/css": "^8.1.3", + "acorn-jsx": "^5.3.2", "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.1", "prettier": "^3.7.4", @@ -59,6 +78,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", "vitest": "^4.0.15", + "zimmerframe": "^1.1.4", "zod": "^4.1.13" }, "prettier": { diff --git a/src/lib/css_cache.ts b/src/lib/css_cache.ts new file mode 100644 index 000000000..410bdbfd1 --- /dev/null +++ b/src/lib/css_cache.ts @@ -0,0 +1,178 @@ +/** + * Cache infrastructure for incremental CSS class extraction. + * + * Provides per-file caching with content hash validation to avoid + * re-extracting classes from unchanged files. + * + * @module + */ + +import {mkdir, readFile, writeFile, unlink, rename} from 'node:fs/promises'; +import {dirname, join} from 'node:path'; + +import type {SourceLocation, ExtractionDiagnostic} from './diagnostics.js'; + +/** + * Computes SHA-256 hash of content using Web Crypto API. + */ +export const compute_hash = async (content: string): Promise => { + const encoder = new TextEncoder(); + const buffer = encoder.encode(content); + const digested = await crypto.subtle.digest('SHA-256', buffer); + const bytes = Array.from(new Uint8Array(digested)); + let hex = ''; + for (const h of bytes) { + hex += h.toString(16).padStart(2, '0'); + } + return hex; +}; + +/** + * Default cache directory relative to project root. + */ +export const DEFAULT_CACHE_DIR = '.fuz/cache/css'; + +/** + * Cache version. Bump when any of these change: + * - `CachedExtraction` schema + * - `extract_css_classes_with_locations()` logic or output + * - `ExtractionDiagnostic` or `SourceLocation` structure + * + * v2: Use null instead of empty arrays for classes/diagnostics + * v3: Add explicit_classes for @fuz-classes warnings + */ +const CACHE_VERSION = 1; + +/** + * Cached extraction result for a single file. + * Uses `null` instead of empty arrays to avoid allocation overhead. + */ +export interface CachedExtraction { + /** Cache version - invalidates cache when bumped */ + v: number; + /** SHA-256 hash of the source file contents */ + content_hash: string; + /** Classes as [name, locations] tuples, or null if none */ + classes: Array<[string, Array]> | null; + /** Classes from @fuz-classes comments, or null if none */ + explicit_classes: Array | null; + /** Extraction diagnostics, or null if none */ + diagnostics: Array | null; +} + +/** + * Computes the cache file path for a source file. + * Cache structure mirrors source tree: `src/lib/Foo.svelte` → `.fuz/cache/css/src/lib/Foo.svelte.json` + * + * @param source_path - Absolute path to the source file + * @param cache_dir - Absolute path to the cache directory + * @param project_root - Normalized project root (must end with `/`) + */ +export const get_cache_path = ( + source_path: string, + cache_dir: string, + project_root: string, +): string => { + if (!source_path.startsWith(project_root)) { + throw new Error(`Source path "${source_path}" is not under project root "${project_root}"`); + } + const relative = source_path.slice(project_root.length); + return join(cache_dir, relative + '.json'); +}; + +/** + * Loads a cached extraction result from disk. + * Returns `null` if the cache is missing, corrupted, or has a version mismatch. + * This makes the cache self-healing: any error triggers re-extraction. + * + * @param cache_path - Absolute path to the cache file + */ +export const load_cached_extraction = async ( + cache_path: string, +): Promise => { + try { + const content = await readFile(cache_path, 'utf8'); + const cached = JSON.parse(content) as CachedExtraction; + + // Invalidate if version mismatch + if (cached.v !== CACHE_VERSION) { + return null; + } + + return cached; + } catch { + // Handles: file not found, invalid JSON, truncated file, permission errors + // All cases: return null to trigger re-extraction (self-healing) + return null; + } +}; + +/** + * Saves an extraction result to the cache. + * Uses atomic write (temp file + rename) for crash safety. + * Converts empty arrays to null to avoid allocation overhead on load. + * + * @param cache_path - Absolute path to the cache file + * @param content_hash - SHA-256 hash of the source file contents + * @param classes - Extracted classes with their locations, or null if none + * @param explicit_classes - Classes from @fuz-classes comments, or null if none + * @param diagnostics - Extraction diagnostics, or null if none + */ +export const save_cached_extraction = async ( + cache_path: string, + content_hash: string, + classes: Map> | null, + explicit_classes: Set | null, + diagnostics: Array | null, +): Promise => { + // Convert to null if empty to save allocation on load + const classes_array = classes && classes.size > 0 ? Array.from(classes.entries()) : null; + const explicit_array = + explicit_classes && explicit_classes.size > 0 ? Array.from(explicit_classes) : null; + const diagnostics_array = diagnostics && diagnostics.length > 0 ? diagnostics : null; + + const data: CachedExtraction = { + v: CACHE_VERSION, + content_hash, + classes: classes_array, + explicit_classes: explicit_array, + diagnostics: diagnostics_array, + }; + + // Atomic write: temp file + rename + // Include pid + timestamp to avoid conflicts with concurrent writes + await mkdir(dirname(cache_path), {recursive: true}); + const temp_path = cache_path + '.tmp.' + process.pid + '.' + Date.now(); + await writeFile(temp_path, JSON.stringify(data)); + await rename(temp_path, cache_path); +}; + +/** + * Deletes a cached extraction file. + * Silently succeeds if the file doesn't exist. + * + * @param cache_path - Absolute path to the cache file + */ +export const delete_cached_extraction = async (cache_path: string): Promise => { + await unlink(cache_path).catch(() => { + // Ignore if already gone + }); +}; + +/** + * Converts a cached extraction back to the runtime format. + * Preserves null semantics (null = empty). + * + * @param cached - Cached extraction data + */ +export const from_cached_extraction = ( + cached: CachedExtraction, +): { + classes: Map> | null; + explicit_classes: Set | null; + diagnostics: Array | null; +} => ({ + classes: cached.classes ? new Map(cached.classes) : null, + explicit_classes: cached.explicit_classes ? new Set(cached.explicit_classes) : null, + diagnostics: cached.diagnostics, +}); diff --git a/src/lib/css_class_composites.ts b/src/lib/css_class_composites.ts index e583b096a..afece8f26 100644 --- a/src/lib/css_class_composites.ts +++ b/src/lib/css_class_composites.ts @@ -1,6 +1,6 @@ -import type {CssClassDeclaration} from './css_class_helpers.js'; +import type {CssClassDefinition} from './css_class_generation.js'; -export const css_class_composites: Record = { +export const css_class_composites: Record = { pixelated: { declaration: ` image-rendering: -webkit-optimize-contrast; /* Safari */ @@ -9,206 +9,38 @@ export const css_class_composites: Record = { + // Composite classes go first, so they can be overridden by the more specific classes. + ...css_class_composites, + + /* + + typography + + */ + font_family_sans: {declaration: 'font-family: var(--font_family_sans);'}, + font_family_serif: {declaration: 'font-family: var(--font_family_serif);'}, + font_family_mono: {declaration: 'font-family: var(--font_family_mono);'}, + + ...generate_property_classes( + 'line-height', + line_height_variants, + (v) => `var(--line_height_${v})`, + ), + ...generate_property_classes( + 'font-size', + font_size_variants, + (v) => `var(--font_size_${v}); --font_size: var(--font_size_${v})`, + ), + ...generate_property_classes( + 'font-size', + icon_size_variants, + (v) => `var(--icon_size_${v}); --font_size: var(--icon_size_${v})`, + 'icon_size', + ), + + /* + + colors + + */ + ...generate_property_classes( + 'color', + text_color_variants.map(String), + (v) => `var(--text_color_${v})`, + 'text_color', + ), + ...generate_property_classes( + 'background-color', + intensity_variants.map(String), + (v) => `var(--darken_${v})`, + 'darken', + ), + ...generate_property_classes( + 'background-color', + intensity_variants.map(String), + (v) => `var(--lighten_${v})`, + 'lighten', + ), + bg: {declaration: 'background-color: var(--bg);'}, + fg: {declaration: 'background-color: var(--fg);'}, + ...generate_property_classes( + 'background-color', + intensity_variants.map(String), + (v) => `var(--bg_${v})`, + 'bg', + ), + ...generate_property_classes( + 'background-color', + intensity_variants.map(String), + (v) => `var(--fg_${v})`, + 'fg', + ), + ...generate_property_classes( + 'color', + intensity_variants.map(String), + (v) => `var(--darken_${v})`, + 'color_darken', + ), + ...generate_property_classes( + 'color', + intensity_variants.map(String), + (v) => `var(--lighten_${v})`, + 'color_lighten', + ), + color_bg: {declaration: 'color: var(--bg);'}, + color_fg: {declaration: 'color: var(--fg);'}, + ...generate_property_classes( + 'color', + intensity_variants.map(String), + (v) => `var(--bg_${v})`, + 'color_bg', + ), + ...generate_property_classes( + 'color', + intensity_variants.map(String), + (v) => `var(--fg_${v})`, + 'color_fg', + ), + ...generate_classes( + (hue: string) => ({ + name: `hue_${hue}`, + css: `--hue: var(--hue_${hue});`, + }), + color_variants, + ), + ...generate_classes( + (hue: string, intensity: number) => ({ + name: `color_${hue}_${intensity}`, + css: `color: var(--color_${hue}_${intensity});`, + }), + color_variants, + intensity_variants, + ), + ...generate_classes( + (hue: string, intensity: number) => ({ + name: `bg_${hue}_${intensity}`, + css: `background-color: var(--color_${hue}_${intensity});`, + }), + color_variants, + intensity_variants, + ), + + /* + + borders + + */ + ...generate_property_classes( + 'border-color', + border_color_intensity_variants.map(String), + (v) => `var(--border_color_${v})`, + ), + ...generate_property_classes('border-color', color_variants, (v) => `var(--border_color_${v})`), + ...generate_property_classes( + 'outline-color', + border_color_intensity_variants.map(String), + (v) => `var(--border_color_${v})`, + ), + ...generate_property_classes('outline-color', color_variants, (v) => `var(--border_color_${v})`), + + ...generate_property_classes( + 'border-width', + border_width_variants.map(String), + (v) => `var(--border_width_${v})`, + ), + ...generate_property_classes( + 'outline-width', + border_width_variants.map(String), + (v) => `var(--border_width_${v})`, + ), + outline_width_focus: {declaration: 'outline-width: var(--outline_width_focus);'}, + outline_width_active: {declaration: 'outline-width: var(--outline_width_active);'}, + + ...generate_property_classes( + 'border-radius', + border_radius_variants, + (v) => `var(--border_radius_${v})`, + ), + ...generate_border_radius_corners(border_radius_variants, (v) => `var(--border_radius_${v})`), + + /* + + shadows + + */ + ...generate_shadow_classes(shadow_size_variants, { + xs: '1', + sm: '2', + md: '3', + lg: '4', + xl: '5', + }), + ...generate_classes( + (value: string) => ({ + name: `shadow_color_${value}`, + css: `--shadow_color: var(--shadow_color_${value});`, + }), + shadow_semantic_values, + ), + ...generate_classes( + (hue: string) => ({ + name: `shadow_color_${hue}`, + css: `--shadow_color: var(--shadow_color_${hue});`, + }), + color_variants, + ), + ...generate_classes( + (alpha: number) => ({ + name: `shadow_alpha_${alpha}`, + css: `--shadow_alpha: var(--shadow_alpha_${alpha});`, + }), + shadow_alpha_variants, + ), + + /* + + layout + + */ + ...generate_property_classes('width', space_variants, (v) => `var(--space_${v})`), + ...generate_property_classes('height', space_variants, (v) => `var(--space_${v})`), + + ...generate_classes( + (v: string) => ({ + name: `width_atmost_${v}`, + css: `width: 100%; max-width: var(--distance_${v});`, + }), + distance_variants, + ), + ...generate_classes( + (v: string) => ({ + name: `width_atleast_${v}`, + css: `width: 100%; min-width: var(--distance_${v});`, + }), + distance_variants, + ), + ...generate_classes( + (v: string) => ({ + name: `height_atmost_${v}`, + css: `height: 100%; max-height: var(--distance_${v});`, + }), + distance_variants, + ), + ...generate_classes( + (v: string) => ({ + name: `height_atleast_${v}`, + css: `height: 100%; min-height: var(--distance_${v});`, + }), + distance_variants, + ), + + ...generate_property_classes('top', space_variants, format_spacing_value), + ...generate_property_classes('right', space_variants, format_spacing_value), + ...generate_property_classes('bottom', space_variants, format_spacing_value), + ...generate_property_classes('left', space_variants, format_spacing_value), + ...generate_property_classes('inset', space_variants, format_spacing_value), + + ...generate_directional_classes('padding', ['0', ...space_variants], format_spacing_value), + ...generate_directional_classes('margin', ['0', 'auto', ...space_variants], format_spacing_value), + ...generate_property_classes('gap', space_variants, format_spacing_value), + ...generate_property_classes('column-gap', space_variants, format_spacing_value), + ...generate_property_classes('row-gap', space_variants, format_spacing_value), +}; diff --git a/src/lib/css_class_extractor.ts b/src/lib/css_class_extractor.ts new file mode 100644 index 000000000..43fe6b9d6 --- /dev/null +++ b/src/lib/css_class_extractor.ts @@ -0,0 +1,1035 @@ +/** + * AST-based CSS class extraction for Svelte and TypeScript files. + * + * Replaces regex-based extraction with proper parsing to handle: + * - `class="display:flex"` - string attributes + * - `class={{ active, disabled: !enabled }}` - object attributes (Svelte 5.16+) + * - `class={[cond && 'box', 'display:flex']}` - array attributes (Svelte 5.16+) + * - `class:active={cond}` - class directives + * - `clsx('foo', { bar: true })` - class utility function calls + * - Variables with class-related names + * - `// @fuz-classes class1 class2` - comment hints for dynamic classes + * + * @module + */ + +import {parse as parse_svelte, type AST} from 'svelte/compiler'; +import {walk, type Visitors} from 'zimmerframe'; +import {Parser, type Node} from 'acorn'; +import {tsPlugin} from '@sveltejs/acorn-typescript'; + +import {type SourceLocation, type ExtractionDiagnostic} from './diagnostics.js'; + +// +// Types +// + +/** + * Extraction result with classes mapped to their source locations. + * Uses `null` instead of empty collections to avoid allocation overhead. + * + * Uses embedded diagnostics (rather than a Result type) because file extraction + * can partially succeed: some classes may be extracted while others produce errors. + * This differs from {@link CssLiteralParseResult} which uses a discriminated union + * because single-class parsing is binary success/failure. + */ +export interface ExtractionResult { + /** + * Map from class name to locations where it was used, or null if none. + * Keys = unique classes, values = locations for diagnostics/IDE integration. + */ + classes: Map> | null; + /** + * Classes explicitly annotated via `@fuz-classes` comments, or null if none. + * These should produce warnings if they can't be resolved during generation. + */ + explicit_classes: Set | null; + /** Variables that were used in class contexts, or null if none */ + tracked_vars: Set | null; + /** Diagnostics from the extraction phase, or null if none */ + diagnostics: Array | null; +} + +/** + * Acorn plugin type - a function that extends the Parser class. + */ +export type AcornPlugin = (BaseParser: typeof Parser) => typeof Parser; + +/** + * Options for CSS class extraction. + */ +export interface ExtractCssClassesOptions { + /** + * File path used to determine extraction method (Svelte vs TS) + * and for location tracking in diagnostics. + */ + filename?: string; + /** + * Additional acorn plugins to use when parsing TS/JS files. + * Useful for adding JSX support via `acorn-jsx` for React projects. + * + * @example + * ```ts + * import jsx from 'acorn-jsx'; + * extract_css_classes(source, { acorn_plugins: [jsx()] }); + * ``` + */ + acorn_plugins?: Array; +} + +// +// Utilities +// + +/** + * Helper class for converting character offsets to line/column positions. + * Svelte template nodes (Comment, Text, ExpressionTag) only have char offsets, + * so this class enables efficient conversion. + * + * Build: O(n) where n = source length + * Lookup: O(log m) where m = number of lines (binary search) + */ +export class SourceIndex { + private line_starts: Array; + + constructor(source: string) { + this.line_starts = [0]; + for (let i = 0; i < source.length; i++) { + if (source[i] === '\n') this.line_starts.push(i + 1); + } + } + + /** + * Converts a character offset to a source location. + * + * @param offset - 0-based character offset in the source + * @param file - File path for the location + * @returns SourceLocation with 1-based line and column + */ + get_location(offset: number, file: string): SourceLocation { + // Binary search for line + let low = 0; + let high = this.line_starts.length - 1; + while (low < high) { + const mid = Math.ceil((low + high) / 2); + if (this.line_starts[mid]! <= offset) low = mid; + else high = mid - 1; + } + return {file, line: low + 1, column: offset - this.line_starts[low]! + 1}; + } +} + +/** + * Adds a class with its location to the extraction result. + * + * @param classes - Map of classes to locations + * @param class_name - Class name to add + * @param location - Source location where the class was found + * @mutates classes - Adds or appends to the map entry + */ +const add_class_with_location = ( + classes: Map>, + class_name: string, + location: SourceLocation, +): void => { + const existing = classes.get(class_name); + if (existing) { + existing.push(location); + } else { + classes.set(class_name, [location]); + } +}; + +// Known class utility function names +const CLASS_UTILITY_FUNCTIONS = new Set([ + 'clsx', // clsx package + 'cn', // common alias (shadcn/ui convention) + 'classNames', // classnames package + 'classnames', // lowercase variant + 'cx', // emotion and other libs +]); + +// Svelte 5 runes that wrap expressions we should extract from +const SVELTE_RUNES = new Set(['$derived', '$state']); + +// Pattern for variables with class-related names +const CLASS_NAME_PATTERN = /(class|classes|class_?names?|class_?lists?)$/i; + +/** + * State maintained during AST walking. + */ +interface WalkState { + /** Map from class name to locations */ + classes: Map>; + /** Classes explicitly annotated via @fuz-classes comments */ + explicit_classes: Set; + /** Variables used in class contexts */ + tracked_vars: Set; + /** Variables with class-like names, mapped to their initializers */ + class_name_vars: Map; + /** Whether we're in a class context (for tracking variable usage) */ + in_class_context: boolean; + /** File path for creating locations */ + file: string; + /** Source index for char offset → line/column conversion (template only) */ + source_index: SourceIndex | null; + /** Diagnostics collected during extraction */ + diagnostics: Array; +} + +/** + * Adds a class to the state with a location. + */ +const add_class = (state: WalkState, class_name: string, location: SourceLocation): void => { + add_class_with_location(state.classes, class_name, location); +}; + +/** + * Creates a location from a character offset using the source index. + */ +const location_from_offset = (state: WalkState, offset: number): SourceLocation => { + if (state.source_index) { + return state.source_index.get_location(offset, state.file); + } + // Fallback for script context (should have line/column from AST) + return {file: state.file, line: 1, column: 1}; +}; + +/** + * Extracts CSS classes from a Svelte file using AST parsing. + * + * @param source - The Svelte file source code + * @param file - File path for location tracking + * @returns Extraction result with classes, tracked variables, and diagnostics + */ +export const extract_from_svelte = (source: string, file = ''): ExtractionResult => { + const classes: Map> = new Map(); + const explicit_classes: Set = new Set(); + const tracked_vars: Set = new Set(); + const class_name_vars: Map = new Map(); + const diagnostics: Array = []; + const source_index = new SourceIndex(source); + + let ast: AST.Root; + try { + ast = parse_svelte(source, {modern: true}); + } catch (err) { + // Emit diagnostic about parse failure + diagnostics.push({ + phase: 'extraction', + level: 'warning', + message: `Failed to parse Svelte file: ${err instanceof Error ? err.message : 'unknown error'}`, + suggestion: 'Check for syntax errors in the file', + location: {file, line: 1, column: 1}, + }); + // Return with diagnostics (classes/tracked_vars empty, so null) + return {classes: null, explicit_classes: null, tracked_vars: null, diagnostics}; + } + + const state: WalkState = { + classes, + explicit_classes, + tracked_vars, + class_name_vars, + in_class_context: false, + file, + source_index, + diagnostics, + }; + + // Extract from @fuz-classes comments via AST (Svelte Comment nodes) + extract_fuz_classes_from_svelte_comments(ast, state); + + // Walk the template AST + walk_template(ast.fragment, state); + + // Walk the script AST (module and instance) including @fuz-classes comments + if (ast.instance) { + extract_fuz_classes_from_script(ast.instance, source, state); + walk_script(ast.instance.content, state); + } + if (ast.module) { + extract_fuz_classes_from_script(ast.module, source, state); + walk_script(ast.module.content, state); + } + + // Second pass: extract from tracked variables that weren't already processed + if (tracked_vars.size > 0 && (ast.instance || ast.module)) { + extract_from_tracked_vars(ast, state); + } + + // Convert empty to null + return { + classes: classes.size > 0 ? classes : null, + explicit_classes: explicit_classes.size > 0 ? explicit_classes : null, + tracked_vars: tracked_vars.size > 0 ? tracked_vars : null, + diagnostics: diagnostics.length > 0 ? diagnostics : null, + }; +}; + +/** + * Parses @fuz-classes content from a comment, handling the colon variant. + * Returns the list of class names or null if not a @fuz-classes comment. + */ +const FUZ_CLASSES_PATTERN = /^\s*@fuz-classes(:?)\s+(.+?)\s*$/; + +const parse_fuz_classes_comment = ( + content: string, + location: SourceLocation, + diagnostics: Array, +): Array | null => { + // Match @fuz-classes with optional colon + const match = FUZ_CLASSES_PATTERN.exec(content); + if (!match) return null; + + const has_colon = match[1] === ':'; + const class_list = match[2]!; + + // Emit warning if colon was used (unnecessary but supported) + if (has_colon) { + diagnostics.push({ + phase: 'extraction', + level: 'warning', + message: '@fuz-classes: colon is unnecessary', + suggestion: 'Use @fuz-classes without the colon', + location, + }); + } + + return class_list.split(/\s+/).filter(Boolean); +}; + +/** + * Extracts @fuz-classes from Svelte HTML Comment nodes. + */ +const extract_fuz_classes_from_svelte_comments = (ast: AST.Root, state: WalkState): void => { + // Walk the fragment looking for Comment nodes + const visitors: Visitors = { + Comment(node, {state}) { + const location = location_from_offset(state, node.start); + const classes = parse_fuz_classes_comment(node.data, location, state.diagnostics); + if (classes) { + for (const cls of classes) { + add_class(state, cls, location); + state.explicit_classes.add(cls); + } + } + }, + }; + + walk(ast.fragment as AST.SvelteNode, state, visitors); +}; + +/** + * Extracts @fuz-classes from script blocks by re-parsing with acorn. + * Svelte's parser doesn't expose JS comments, so we parse the + * script source separately to get comments via acorn's onComment callback. + */ +const extract_fuz_classes_from_script = ( + script: AST.Script, + source: string, + state: WalkState, +): void => { + // Extract the script source using start/end positions + // Svelte AST nodes have start/end but the TypeScript types don't include them + const content = script.content as unknown as {start: number; end: number}; + const script_source = source.slice(content.start, content.end); + + // Calculate the line offset for proper location reporting + // Count newlines before the script content starts + let line_offset = 0; + for (let i = 0; i < content.start; i++) { + if (source[i] === '\n') line_offset++; + } + + // Collect comments via acorn's onComment callback + const comments: Array<{value: string; loc: {start: {line: number; column: number}}}> = []; + + try { + const parser = Parser.extend(tsPlugin()); + parser.parse(script_source, { + ecmaVersion: 'latest', + sourceType: 'module', + locations: true, + onComment: ( + _block: boolean, + text: string, + _start: number, + _end: number, + startLoc?: {line: number; column: number}, + ) => { + if (startLoc) { + comments.push({value: text, loc: {start: startLoc}}); + } + }, + }); + } catch { + // If parsing fails, we can't extract comments + return; + } + + // Process @fuz-classes comments + for (const comment of comments) { + const location: SourceLocation = { + file: state.file, + line: comment.loc.start.line + line_offset, + column: comment.loc.start.column + 1, + }; + const class_list = parse_fuz_classes_comment(comment.value, location, state.diagnostics); + if (class_list) { + for (const cls of class_list) { + add_class(state, cls, location); + state.explicit_classes.add(cls); + } + } + } +}; + +/** + * Extracts CSS classes from a TypeScript/JS file using AST parsing. + * + * @param source - The TS/JS file source code + * @param file - File path for location tracking + * @param acorn_plugins - Additional acorn plugins (e.g., acorn-jsx for React) + * @returns Extraction result with classes, tracked variables, and diagnostics + */ +export const extract_from_ts = ( + source: string, + file = '', + acorn_plugins?: Array, +): ExtractionResult => { + const classes: Map> = new Map(); + const explicit_classes: Set = new Set(); + const tracked_vars: Set = new Set(); + const class_name_vars: Map = new Map(); + const diagnostics: Array = []; + + // Collect comments via acorn's onComment callback + const comments: Array<{value: string; loc: {start: {line: number; column: number}}}> = []; + + let ast: Node; + try { + // Use acorn with TypeScript plugin, plus any additional plugins (e.g., jsx) + const plugins: Array = [tsPlugin(), ...(acorn_plugins ?? [])]; + const parser = plugins.reduce((p, plugin) => plugin(p), Parser); + ast = parser.parse(source, { + ecmaVersion: 'latest', + sourceType: 'module', + locations: true, + onComment: ( + _block: boolean, + text: string, + _start: number, + _end: number, + startLoc?: {line: number; column: number}, + ) => { + if (startLoc) { + comments.push({value: text, loc: {start: startLoc}}); + } + }, + }); + } catch (err) { + // Emit diagnostic about parse failure + diagnostics.push({ + phase: 'extraction', + level: 'warning', + message: `Failed to parse TypeScript/JS file: ${err instanceof Error ? err.message : 'unknown error'}`, + suggestion: 'Check for syntax errors in the file', + location: {file, line: 1, column: 1}, + }); + // Return with diagnostics (classes/tracked_vars empty, so null) + return {classes: null, explicit_classes: null, tracked_vars: null, diagnostics}; + } + + // Process @fuz-classes comments + for (const comment of comments) { + const location: SourceLocation = { + file, + line: comment.loc.start.line, + column: comment.loc.start.column + 1, + }; + const class_list = parse_fuz_classes_comment(comment.value, location, diagnostics); + if (class_list) { + for (const cls of class_list) { + add_class_with_location(classes, cls, location); + explicit_classes.add(cls); + } + } + } + + const state: WalkState = { + classes, + explicit_classes, + tracked_vars, + class_name_vars, + in_class_context: false, + file, + source_index: null, // Not needed for TS files - acorn provides locations + diagnostics, + }; + + walk_script(ast, state); + + // Second pass: extract from tracked variables that weren't already processed + // This handles JSX patterns where className={foo} is encountered after const foo = '...' + if (tracked_vars.size > 0) { + extract_from_tracked_vars_in_script(ast, state); + } + + // Convert empty to null + return { + classes: classes.size > 0 ? classes : null, + explicit_classes: explicit_classes.size > 0 ? explicit_classes : null, + tracked_vars: tracked_vars.size > 0 ? tracked_vars : null, + diagnostics: diagnostics.length > 0 ? diagnostics : null, + }; +}; + +/** + * Unified extraction function that auto-detects file type. + * Returns just the class names as a Set. + * + * @param source - The file source code + * @param options - Extraction options + * @returns Set of class names + */ +export const extract_css_classes = ( + source: string, + options: ExtractCssClassesOptions = {}, +): Set => { + const result = extract_css_classes_with_locations(source, options); + return result.classes ? new Set(result.classes.keys()) : new Set(); +}; + +/** + * Unified extraction function that auto-detects file type. + * Returns full extraction result with locations and diagnostics. + * + * @param source - The file source code + * @param options - Extraction options + * @returns Full extraction result with classes, tracked variables, and diagnostics + */ +export const extract_css_classes_with_locations = ( + source: string, + options: ExtractCssClassesOptions = {}, +): ExtractionResult => { + const {filename, acorn_plugins} = options; + const ext = filename ? filename.slice(filename.lastIndexOf('.')) : ''; + const file = filename ?? ''; + + if (ext === '.svelte' || ext === '.html') { + return extract_from_svelte(source, file); + } else if (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx') { + return extract_from_ts(source, file, acorn_plugins); + } + + // Default to Svelte-style extraction (handles both) + const svelte_result = extract_from_svelte(source, file); + if (svelte_result.classes) { + return svelte_result; + } + return extract_from_ts(source, file, acorn_plugins); +}; + +// Template AST walking + +/** + * Walks the Svelte template AST to extract class names. + */ +const walk_template = (fragment: AST.Fragment, state: WalkState): void => { + const visitors: Visitors = { + // Handle regular elements and components + RegularElement(node, {state, next}) { + process_element_attributes(node.attributes, state); + next(); + }, + SvelteElement(node, {state, next}) { + process_element_attributes(node.attributes, state); + next(); + }, + SvelteComponent(node, {state, next}) { + process_element_attributes(node.attributes, state); + next(); + }, + Component(node, {state, next}) { + process_element_attributes(node.attributes, state); + next(); + }, + SvelteFragment(node, {state, next}) { + process_element_attributes(node.attributes, state); + next(); + }, + }; + + walk(fragment as AST.SvelteNode, state, visitors); +}; + +/** + * Processes attributes on an element to extract class names. + */ +const process_element_attributes = ( + attributes: Array, + state: WalkState, +): void => { + for (const attr of attributes) { + // Handle class attribute + if (attr.type === 'Attribute' && attr.name === 'class') { + extract_from_attribute_value(attr.value, state); + } + + // Handle class: directive + if (attr.type === 'ClassDirective') { + add_class(state, attr.name, location_from_offset(state, attr.start)); + } + } +}; + +/** + * Extracts classes from an attribute value. + * Handles string literals, expressions, objects, and arrays. + */ +const extract_from_attribute_value = (value: AST.Attribute['value'], state: WalkState): void => { + if (value === true) { + // Boolean attribute, no classes + return; + } + + // Handle array of Text and ExpressionTag (e.g., class="foo {expr} bar") + if (Array.isArray(value)) { + for (const part of value) { + if (part.type === 'Text') { + // Static text: split on whitespace + const location = location_from_offset(state, part.start); + for (const cls of part.data.split(/\s+/).filter(Boolean)) { + add_class(state, cls, location); + } + } else { + // ExpressionTag: extract from the expression + state.in_class_context = true; + extract_from_expression(part.expression, state); + state.in_class_context = false; + } + } + return; + } + + // Handle single ExpressionTag (e.g., class={expression}) + // At this point, value is an ExpressionTag + state.in_class_context = true; + extract_from_expression(value.expression, state); + state.in_class_context = false; +}; + +/** + * Extracts classes from a TS expression. + * Handles strings, arrays, objects, conditionals, and function calls. + */ +const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void => { + // Get location from the expression's start offset + const get_location = (): SourceLocation => { + const node = expr as unknown as {start?: number; loc?: {start: {line: number; column: number}}}; + if (node.loc) { + return {file: state.file, line: node.loc.start.line, column: node.loc.start.column + 1}; + } + if (node.start !== undefined) { + return location_from_offset(state, node.start); + } + return {file: state.file, line: 1, column: 1}; + }; + + switch (expr.type) { + case 'Literal': { + // String literal + const node = expr as unknown as {value: unknown}; + if (typeof node.value === 'string') { + const location = get_location(); + for (const cls of node.value.split(/\s+/).filter(Boolean)) { + add_class(state, cls, location); + } + } + break; + } + + case 'TemplateLiteral': { + // Template literal - extract static parts that are complete tokens + // Only extract tokens that are whitespace-bounded (not fragments like `icon-` from `icon-${size}`) + const node = expr as unknown as { + quasis: Array<{ + value: {raw: string}; + start?: number; + loc?: {start: {line: number; column: number}}; + }>; + expressions: Array; + }; + + const has_expressions = node.expressions.length > 0; + + for (let i = 0; i < node.quasis.length; i++) { + const quasi = node.quasis[i]!; + if (!quasi.value.raw) continue; + + const location = quasi.loc + ? {file: state.file, line: quasi.loc.start.line, column: quasi.loc.start.column + 1} + : quasi.start !== undefined + ? location_from_offset(state, quasi.start) + : get_location(); + + const raw = quasi.value.raw; + + if (!has_expressions) { + // No expressions - extract all tokens (pure static template literal) + for (const cls of raw.split(/\s+/).filter(Boolean)) { + add_class(state, cls, location); + } + } else { + // Has expressions - only extract complete tokens + // A token is complete if bounded by whitespace/string-boundary on both sides + const is_first = i === 0; + const is_last = i === node.quasis.length - 1; + const has_expr_before = !is_first; + const has_expr_after = !is_last; + + // Split preserving info about boundaries + const tokens = raw.split(/\s+/); + const starts_with_ws = /^\s/.test(raw); + const ends_with_ws = /\s$/.test(raw); + + for (let j = 0; j < tokens.length; j++) { + const token = tokens[j]; + if (!token) continue; + + const is_first_token = j === 0; + const is_last_token = j === tokens.length - 1; + + // Token is bounded on the left if: + // - It's the first token AND (is_first quasi OR starts_with_ws) + // - OR it's not the first token (preceded by whitespace from split) + const bounded_left = !is_first_token || is_first || (has_expr_before && starts_with_ws); + + // Token is bounded on the right if: + // - It's the last token AND (is_last quasi OR ends_with_ws) + // - OR it's not the last token (followed by whitespace from split) + const bounded_right = !is_last_token || is_last || (has_expr_after && ends_with_ws); + + if (bounded_left && bounded_right) { + add_class(state, token, location); + } + } + } + } + + // Also extract from expressions (e.g., ternaries inside the template) + for (const subexpr of node.expressions) { + extract_from_expression(subexpr as AST.SvelteNode, state); + } + break; + } + + case 'ArrayExpression': { + // Array: extract from each element + const node = expr as unknown as {elements: Array}; + for (const element of node.elements) { + if (element) { + extract_from_expression(element as AST.SvelteNode, state); + } + } + break; + } + + case 'ObjectExpression': { + // Object: extract keys (values are conditions) + const node = expr as unknown as { + properties: Array<{ + type: string; + key: unknown; + computed: boolean; + start?: number; + loc?: {start: {line: number; column: number}}; + }>; + }; + for (const prop of node.properties) { + if (prop.type === 'Property' && !prop.computed) { + // Non-computed key - extract the key as a class name + const key = prop.key as {type: string; name?: string; value?: string}; + const location = prop.loc + ? {file: state.file, line: prop.loc.start.line, column: prop.loc.start.column + 1} + : prop.start !== undefined + ? location_from_offset(state, prop.start) + : get_location(); + if (key.type === 'Identifier') { + add_class(state, key.name!, location); + } else if (key.type === 'Literal' && typeof key.value === 'string') { + // Handle string keys like { 'display:flex': condition } + for (const cls of key.value.split(/\s+/).filter(Boolean)) { + add_class(state, cls, location); + } + } + } + } + break; + } + + case 'ConditionalExpression': { + // Ternary: extract from both branches + const node = expr as unknown as {consequent: unknown; alternate: unknown}; + extract_from_expression(node.consequent as AST.SvelteNode, state); + extract_from_expression(node.alternate as AST.SvelteNode, state); + break; + } + + case 'LogicalExpression': { + // && or ||: extract from both sides + const node = expr as unknown as {left: unknown; right: unknown}; + extract_from_expression(node.left as AST.SvelteNode, state); + extract_from_expression(node.right as AST.SvelteNode, state); + break; + } + + case 'CallExpression': { + // Function call: check if it's a class utility function or Svelte rune + const node = expr as unknown as { + callee: {type: string; name?: string; object?: {name?: string}; property?: {name?: string}}; + arguments: Array; + }; + + let should_extract = false; + + if (node.callee.type === 'Identifier') { + // Direct call: clsx(), cn(), $derived(), etc. + should_extract = + CLASS_UTILITY_FUNCTIONS.has(node.callee.name!) || SVELTE_RUNES.has(node.callee.name!); + } else if (node.callee.type === 'MemberExpression') { + // Member call: $derived.by(), etc. + const obj = node.callee.object; + if (obj?.name && SVELTE_RUNES.has(obj.name)) { + should_extract = true; + } + } + + if (should_extract) { + for (const arg of node.arguments) { + extract_from_expression(arg as AST.SvelteNode, state); + } + } + break; + } + + case 'Identifier': { + // Variable reference: track it for later extraction + const node = expr as unknown as {name: string}; + if (state.in_class_context) { + state.tracked_vars.add(node.name); + } + break; + } + + case 'MemberExpression': { + // Member access like obj.prop - we can't statically extract + // but track the root identifier + break; + } + + case 'TaggedTemplateExpression': { + // Tagged template literal like css`class-name` - extract from the template + const node = expr as unknown as {quasi: unknown}; + if (node.quasi) { + extract_from_expression(node.quasi as AST.SvelteNode, state); + } + break; + } + + case 'ArrowFunctionExpression': + case 'FunctionExpression': { + // Function expression: extract from the body + // Handles $derived.by(() => cond ? 'a' : 'b') + const node = expr as unknown as {body: unknown}; + if (node.body) { + extract_from_expression(node.body as AST.SvelteNode, state); + } + break; + } + + case 'BlockStatement': { + // Block statement: walk all statements looking for return statements + const node = expr as unknown as {body: Array}; + for (const stmt of node.body) { + extract_from_expression(stmt as AST.SvelteNode, state); + } + break; + } + + case 'ReturnStatement': { + // Return statement: extract from the argument + const node = expr as unknown as {argument: unknown}; + if (node.argument) { + extract_from_expression(node.argument as AST.SvelteNode, state); + } + break; + } + + default: + // Other expression types we don't handle + break; + } +}; + +// Script AST walking + +/** + * Extracts classes from a JSX attribute value (React className, Preact/Solid class, Solid classList). + * Handles string literals and expression containers. + * Sets in_class_context to enable variable tracking. + */ +const extract_from_jsx_attribute_value = (value: unknown, state: WalkState): void => { + const node = value as { + type: string; + value?: string; + expression?: unknown; + loc?: {start: {line: number; column: number}}; + }; + + if (node.type === 'Literal' && typeof node.value === 'string') { + // Static className="foo bar" + const location: SourceLocation = node.loc + ? {file: state.file, line: node.loc.start.line, column: node.loc.start.column + 1} + : {file: state.file, line: 1, column: 1}; + for (const cls of node.value.split(/\s+/).filter(Boolean)) { + add_class(state, cls, location); + } + } else if (node.type === 'JSXExpressionContainer' && node.expression) { + // Dynamic className={expr} - enable variable tracking + const prev_context = state.in_class_context; + state.in_class_context = true; + extract_from_expression(node.expression as AST.SvelteNode, state); + state.in_class_context = prev_context; + } +}; + +/** + * Walks a TypeScript/JS AST to extract class names. + */ +const walk_script = (ast: unknown, state: WalkState): void => { + const visitors: Visitors = { + // Variable declarations + VariableDeclarator(node, {state, next}) { + const declarator = node as unknown as {id: {type: string; name: string}; init: unknown}; + if (declarator.id.type === 'Identifier') { + const name = declarator.id.name; + // Check if variable name matches class pattern + if (CLASS_NAME_PATTERN.test(name)) { + state.class_name_vars.set(name, declarator.init); + if (declarator.init) { + extract_from_expression(declarator.init as AST.SvelteNode, state); + } + } + // Also check if this variable was tracked from usage + if (state.tracked_vars.has(name) && declarator.init) { + extract_from_expression(declarator.init as AST.SvelteNode, state); + } + } + next(); + }, + + // Call expressions (for clsx/cn calls outside of class attributes) + CallExpression(node, {state, next}) { + const call = node as unknown as { + callee: {type: string; name?: string}; + arguments: Array; + }; + if (call.callee.type === 'Identifier' && CLASS_UTILITY_FUNCTIONS.has(call.callee.name!)) { + for (const arg of call.arguments) { + extract_from_expression(arg as AST.SvelteNode, state); + } + } + next(); + }, + + // Object properties with class-related keys + Property(node, {state, next}) { + const prop = node as unknown as { + key: {type: string; name?: string; value?: string}; + value: unknown; + computed: boolean; + }; + if (!prop.computed) { + // Extract key name from identifier or string literal + let key_name: string | undefined; + if (prop.key.type === 'Identifier') { + key_name = prop.key.name; + } else if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') { + key_name = prop.key.value; + } + if ( + key_name && + (key_name === 'class' || key_name === 'className' || CLASS_NAME_PATTERN.test(key_name)) + ) { + extract_from_expression(prop.value as AST.SvelteNode, state); + } + } + next(); + }, + + // JSX elements (React/Preact/Solid) - extract class-related attributes + // These are only present when acorn-jsx plugin is used + JSXElement(node, {state, next}) { + const element = node as unknown as { + openingElement: { + attributes: Array<{ + type: string; + name?: {type: string; name?: string}; + value?: unknown; + }>; + }; + }; + for (const attr of element.openingElement.attributes) { + if (attr.type === 'JSXAttribute' && attr.value) { + const attr_name = attr.name?.name; + // className (React), class (Preact/Solid) + if (attr_name === 'className' || attr_name === 'class') { + extract_from_jsx_attribute_value(attr.value, state); + } + // classList (Solid) - object syntax like classList={{ active: isActive }} + else if (attr_name === 'classList') { + extract_from_jsx_attribute_value(attr.value, state); + } + } + } + next(); + }, + }; + + walk(ast as Node, state, visitors); +}; + +/** + * Second pass to extract from tracked variables in Svelte scripts. + */ +const extract_from_tracked_vars = (ast: AST.Root, state: WalkState): void => { + const scripts = [ast.instance?.content, ast.module?.content].filter(Boolean); + + for (const script of scripts) { + extract_from_tracked_vars_in_script(script as unknown as Node, state); + } +}; + +/** + * Second pass to extract from tracked variables in a standalone script AST. + * Used for both Svelte scripts and standalone TS/JS files (including JSX). + */ +const extract_from_tracked_vars_in_script = (ast: Node, state: WalkState): void => { + const find_visitors: Visitors = { + VariableDeclarator(node, {state}) { + const declarator = node as unknown as {id: {type: string; name: string}; init: unknown}; + if ( + declarator.id.type === 'Identifier' && + state.tracked_vars.has(declarator.id.name) && + !state.class_name_vars.has(declarator.id.name) && + declarator.init + ) { + extract_from_expression(declarator.init as AST.SvelteNode, state); + } + }, + }; + + walk(ast, state, find_visitors); +}; diff --git a/src/lib/css_class_generation.ts b/src/lib/css_class_generation.ts new file mode 100644 index 000000000..ce6310188 --- /dev/null +++ b/src/lib/css_class_generation.ts @@ -0,0 +1,361 @@ +/** + * CSS class generation utilities. + * + * Produces CSS output from class definitions, handles interpretation of + * dynamic classes, and provides collection management for extracted classes. + * + * @module + */ + +import type {Logger} from '@fuzdev/fuz_util/log.js'; + +import { + type SourceLocation, + type InterpreterDiagnostic, + type GenerationDiagnostic, + create_generation_diagnostic, +} from './diagnostics.js'; +import { + parse_ruleset, + is_single_selector_ruleset, + ruleset_contains_class, +} from './css_ruleset_parser.js'; +import {resolve_class_definition} from './css_class_resolution.js'; +import {get_modifier} from './modifiers.js'; + +// +// CSS Utilities +// + +/** + * Escapes special characters in a CSS class name for use in a selector. + * CSS selectors require escaping of characters like `:`, `%`, `(`, `)`, etc. + * + * @example + * escape_css_selector('display:flex') // 'display\\:flex' + * escape_css_selector('opacity:80%') // 'opacity\\:80\\%' + * escape_css_selector('nth-child(2n)') // 'nth-child\\(2n\\)' + */ +export const escape_css_selector = (name: string): string => { + return name.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&'); +}; + +// +// Class Definitions +// + +export interface CssClassDefinitionBase { + comment?: string; +} + +/** Pure utility composition (composes only). */ +export interface CssClassDefinitionComposition extends CssClassDefinitionBase { + composes: Array; + declaration?: never; + ruleset?: never; +} + +/** Custom CSS declaration (optionally seeded with composes). */ +export interface CssClassDefinitionDeclaration extends CssClassDefinitionBase { + declaration: string; + composes?: Array; + ruleset?: never; +} + +/** Full ruleset with selectors. */ +export interface CssClassDefinitionRuleset extends CssClassDefinitionBase { + ruleset: string; + classes?: never; + declaration?: never; +} + +/** Static definitions (not interpreter-based). */ +export type CssClassDefinitionStatic = + | CssClassDefinitionComposition + | CssClassDefinitionDeclaration + | CssClassDefinitionRuleset; + +/** Full union including interpreters. */ +export type CssClassDefinition = CssClassDefinitionStatic | CssClassDefinitionInterpreter; + +/** + * Context passed to CSS class interpreters. + * Provides access to logging, diagnostics collection, and the class registry. + */ +export interface CssClassInterpreterContext { + /** Optional logger for warnings/errors */ + log?: Logger; + /** Diagnostics array to collect warnings and errors */ + diagnostics: Array; + /** All known CSS class definitions (token + composite classes) */ + class_definitions: Record; + /** Valid CSS properties for literal validation, or null to skip validation */ + css_properties: Set | null; +} + +/** Interpreter for dynamic CSS class generation based on pattern matching. */ +export interface CssClassDefinitionInterpreter extends CssClassDefinitionBase { + pattern: RegExp; + /** + * @mutates ctx.diagnostics - Implementations push errors/warnings to the diagnostics array + */ + interpret: (matched: RegExpMatchArray, ctx: CssClassInterpreterContext) => string | null; +} + +// +// CSS Generation +// + +/** + * Result from CSS class generation. + */ +export interface GenerateClassesCssResult { + css: string; + diagnostics: Array; +} + +export interface GenerateClassesCssOptions { + class_names: Iterable; + class_definitions: Record; + interpreters: Array; + /** Valid CSS properties for literal validation, or null to skip validation */ + css_properties: Set | null; + log?: Logger; + class_locations?: Map | null>; + /** + * Classes that were explicitly annotated (via @fuz-classes or include_classes). + * Unresolved explicit classes produce warnings. + */ + explicit_classes?: Set | null; +} + +export const generate_classes_css = ( + options: GenerateClassesCssOptions, +): GenerateClassesCssResult => { + const { + class_names, + class_definitions, + interpreters, + css_properties, + log, + class_locations, + explicit_classes, + } = options; + const interpreter_diagnostics: Array = []; + const diagnostics: Array = []; + + // Create interpreter context with access to all class definitions + const ctx: CssClassInterpreterContext = { + log, + diagnostics: interpreter_diagnostics, + class_definitions, + css_properties, + }; + + // Create a map that has the index of each class name as the key + const indexes: Map = new Map(); + const keys = Object.keys(class_definitions); + for (let i = 0; i < keys.length; i++) { + indexes.set(keys[i]!, i); + } + + // Helper to get the maximum state modifier order from a class name + // This ensures proper cascade: hover (5) comes before active (6) + const get_state_modifier_order = (class_name: string): number => { + const parts = class_name.split(':'); + let max_order = 0; + for (const part of parts.slice(0, -1)) { + // Check each potential modifier prefix + const modifier = get_modifier(part); + if (modifier?.type === 'state' && modifier.order !== undefined) { + max_order = Math.max(max_order, modifier.order); + } + } + return max_order; + }; + + // Sort classes: first by definition index, then by state modifier order, then alphabetically + const sorted_classes = (Array.isArray(class_names) ? class_names : Array.from(class_names)).sort( + (a, b) => { + const index_a = indexes.get(a) ?? Number.MAX_VALUE; + const index_b = indexes.get(b) ?? Number.MAX_VALUE; + if (index_a !== index_b) return index_a - index_b; + // For classes with modifiers, sort by state modifier order (for proper cascade) + const order_a = get_state_modifier_order(a); + const order_b = get_state_modifier_order(b); + if (order_a !== order_b) return order_a - order_b; + return a.localeCompare(b); // alphabetic tiebreaker for stable sort + }, + ); + + let css = ''; + for (const c of sorted_classes) { + let v = class_definitions[c]; + + // Track diagnostics count before this class + const diag_count_before = interpreter_diagnostics.length; + + // If not found statically, try interpreters + let interpreter_matched = false; + if (!v) { + for (const interpreter of interpreters) { + const matched = c.match(interpreter.pattern); + if (matched) { + interpreter_matched = true; + const result = interpreter.interpret(matched, ctx); + if (result) { + // Check if the result is a full ruleset (contains braces) + // or just a declaration + if (result.includes('{')) { + // Full ruleset - use as-is + v = {ruleset: result, comment: interpreter.comment}; + } else { + // Simple declaration + v = {declaration: result, comment: interpreter.comment}; + } + break; + } + } + } + } + + // Convert any new interpreter diagnostics to GenerationDiagnostic with locations + // For non-explicit classes, downgrade CSS property errors to warnings (may be from other CSS systems) + // Structural errors (circular refs, unknown composes) remain errors regardless + for (let i = diag_count_before; i < interpreter_diagnostics.length; i++) { + const diag = interpreter_diagnostics[i]!; + const locations = class_locations?.get(diag.class_name) ?? null; + const is_explicit = explicit_classes?.has(diag.class_name) ?? false; + const is_css_property_error = + diag.level === 'error' && diag.message.startsWith('Unknown CSS property'); + const level = is_css_property_error && !is_explicit ? 'warning' : diag.level; + diagnostics.push(create_generation_diagnostic({...diag, level}, locations)); + } + + if (!v) { + // Error if this was an explicitly requested class (via @fuz-classes or include_classes) + // but only if no interpreter pattern matched (if one matched but failed, error already reported) + if (explicit_classes?.has(c) && !interpreter_matched) { + const locations = class_locations?.get(c) ?? null; + diagnostics.push({ + phase: 'generation', + level: 'error', + message: 'No matching class definition found', + class_name: c, + suggestion: 'Check spelling or add a custom class definition', + locations, + }); + } + continue; + } + + const {comment} = v; + + if (comment) { + const trimmed = comment.trim(); + if (trimmed.includes('\n')) { + // Multi-line CSS comment + const lines = trimmed.split('\n').map((line) => line.trim()); + css += `/*\n${lines.join('\n')}\n*/\n`; + } else { + css += `/* ${trimmed} */\n`; + } + } + + // Handle composes-based or declaration-based definitions + if ('composes' in v || 'declaration' in v) { + const resolution_result = resolve_class_definition(v, c, class_definitions, css_properties); + if (!resolution_result.ok) { + // Add error diagnostic and skip this class + diagnostics.push({ + phase: 'generation', + level: 'error', + message: resolution_result.error.message, + class_name: c, + suggestion: resolution_result.error.suggestion, + locations: class_locations?.get(c) ?? null, + }); + continue; + } + // Add warnings if any + if (resolution_result.warnings) { + for (const warning of resolution_result.warnings) { + diagnostics.push(create_generation_diagnostic(warning, class_locations?.get(c) ?? null)); + } + } + if (resolution_result.declaration) { + css += `.${escape_css_selector(c)} { ${resolution_result.declaration} }\n`; + } + } else if ('ruleset' in v) { + // Check for empty ruleset + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + if (!v.ruleset || !v.ruleset.trim()) { + diagnostics.push({ + phase: 'generation', + level: 'warning', + message: `Ruleset "${c}" is empty`, + class_name: c, + suggestion: `Add CSS rules or remove the empty ruleset definition`, + locations: class_locations?.get(c) ?? null, + }); + continue; + } + + css += v.ruleset.trim() + '\n'; + + // Validate ruleset and emit warnings + try { + const parsed = parse_ruleset(v.ruleset); + // Use CSS-escaped class name for matching (handles special chars like colons) + const escaped_class = escape_css_selector(c); + + // Warn if no selector contains the expected class name + if (!ruleset_contains_class(parsed.rules, escaped_class)) { + diagnostics.push({ + phase: 'generation', + level: 'warning', + message: `Ruleset "${c}" has no selectors containing ".${c}"`, + class_name: c, + suggestion: `Ensure at least one selector uses ".${c}" so the class works when applied`, + locations: class_locations?.get(c) ?? null, + }); + } + + // Warn if this ruleset could be converted to declaration format + // Skip for interpreter-generated rulesets (e.g., CSS literals) - they intentionally use rulesets + // Skip if ruleset has at-rules (e.g., @media) - these need the wrapper + // Strip comments before checking (/* ... */ can precede @media) + const ruleset_without_comments = v.ruleset.replace(/\/\*[\s\S]*?\*\//g, '').trim(); + const has_at_rules = ruleset_without_comments.startsWith('@'); + if ( + !interpreter_matched && + !has_at_rules && + is_single_selector_ruleset(parsed.rules, escaped_class) + ) { + diagnostics.push({ + phase: 'generation', + level: 'warning', + message: `Ruleset "${c}" has a single selector and could be converted to declaration format for modifier support`, + class_name: c, + suggestion: `Convert to: { declaration: '${parsed.rules[0]?.declarations.replace(/\s+/g, ' ').trim()}' }`, + locations: class_locations?.get(c) ?? null, + }); + } + } catch (e) { + // Warn about parse errors so users can investigate + const error_message = e instanceof Error ? e.message : String(e); + diagnostics.push({ + phase: 'generation', + level: 'warning', + message: `Failed to parse ruleset for "${c}": ${error_message}`, + class_name: c, + suggestion: 'Check for CSS syntax errors in the ruleset', + locations: class_locations?.get(c) ?? null, + }); + } + } + // Note: Interpreted types are converted to declaration above, so no else clause needed + } + + return {css, diagnostics}; +}; diff --git a/src/lib/css_class_generators.ts b/src/lib/css_class_generators.ts index 7e0f4f862..7af06de25 100644 --- a/src/lib/css_class_generators.ts +++ b/src/lib/css_class_generators.ts @@ -1,14 +1,14 @@ -import type {CssClassDeclaration} from './css_class_helpers.js'; +import type {CssClassDefinition} from './css_class_generation.js'; -export type ClassTemplateResult = { +export type GeneratedClassResult = { name: string; css: string; } | null; export type ClassTemplateFn = - | ((v1: T1) => ClassTemplateResult) - | ((v1: T1, v2: T2) => ClassTemplateResult) - | ((v1: T1, v2: T2, v3: T3) => ClassTemplateResult); + | ((v1: T1) => GeneratedClassResult) + | ((v1: T1, v2: T2) => GeneratedClassResult) + | ((v1: T1, v2: T2, v3: T3) => GeneratedClassResult); /** * Generates CSS class declarations from templates. @@ -39,8 +39,8 @@ export const generate_classes = ( values: Iterable, secondary?: Iterable, tertiary?: Iterable, -): Record => { - const result: Record = {}; +): Record => { + const result: Record = {}; for (const v1 of values) { if (secondary) { @@ -70,33 +70,11 @@ export const generate_classes = ( return result; }; -// TODO refactor with `src/lib/variable_data.ts`, we may want `css_data.ts` or something -export const CSS_GLOBALS = ['inherit', 'initial', 'revert', 'revert_layer', 'unset'] as const; -export type CssGlobal = (typeof CSS_GLOBALS)[number]; - export const CSS_DIRECTIONS = ['top', 'right', 'bottom', 'left'] as const; export type CssDirection = (typeof CSS_DIRECTIONS)[number]; -// TODO add '0' and '10' ? -export const COLOR_INTENSITIES = ['1', '2', '3', '4', '5', '6', '7', '8', '9'] as const; -export type ColorIntensity = (typeof COLOR_INTENSITIES)[number]; - -// Helper to convert snake_case to kebab-case for CSS property values -export const to_kebab = (str: string): string => str.replace(/_/g, '-'); - // Helper to convert any string to a valid CSS variable name (snake_case) -export const to_variable_name = (str: string): string => str.replace(/[-\s]+/g, '_'); - -// Helper to generate global value classes for any CSS property -export const generate_global_classes = (property: string): Record => { - return generate_classes( - (global: (typeof CSS_GLOBALS)[number]) => ({ - name: `${to_variable_name(property)}_${global}`, - css: `${property}: ${to_kebab(global)};`, - }), - CSS_GLOBALS, - ); -}; +export const format_variable_name = (str: string): string => str.replace(/[-\s]+/g, '_'); /** * Format spacing values for CSS (handles 0, auto, percentages, pixels, and CSS variables). @@ -132,9 +110,8 @@ export const format_dimension_value = (value: string): string => { /** * Generate classes for a single CSS property with various values. - * This is the most common pattern, used by display, visibility, float, etc. * - * @param property - The CSS property name (e.g. 'display', 'gap') + * @param property - The CSS property name (e.g. 'font-size', 'gap') * @param values - The values to generate classes for * @param formatter - Optional function to format values (e.g. v => `var(--space_${v})`) * @param prefix - Optional class name prefix (defaults to property with dashes replaced by underscores) @@ -143,11 +120,11 @@ export const generate_property_classes = ( property: string, values: Iterable, formatter?: (value: string) => string, - prefix: string = to_variable_name(property), -): Record => { + prefix: string = format_variable_name(property), +): Record => { return generate_classes( (value: string) => ({ - name: `${prefix}_${to_variable_name(value)}`, + name: `${prefix}_${format_variable_name(value)}`, css: `${property}: ${formatter?.(value) ?? value};`, }), values, @@ -166,7 +143,7 @@ export const generate_directional_classes = ( property: string, values: Iterable, formatter?: (v: string) => string, -): Record => { +): Record => { const prefix = property[0]; // 'm' for margin, 'p' for padding return generate_classes( @@ -175,20 +152,29 @@ export const generate_directional_classes = ( // Map variants to their configurations const configs: Record = { - '': {name: `${prefix}_${to_variable_name(value)}`, css: `${property}: ${formatted};`}, - t: {name: `${prefix}t_${to_variable_name(value)}`, css: `${property}-top: ${formatted};`}, - r: {name: `${prefix}r_${to_variable_name(value)}`, css: `${property}-right: ${formatted};`}, + '': {name: `${prefix}_${format_variable_name(value)}`, css: `${property}: ${formatted};`}, + t: { + name: `${prefix}t_${format_variable_name(value)}`, + css: `${property}-top: ${formatted};`, + }, + r: { + name: `${prefix}r_${format_variable_name(value)}`, + css: `${property}-right: ${formatted};`, + }, b: { - name: `${prefix}b_${to_variable_name(value)}`, + name: `${prefix}b_${format_variable_name(value)}`, css: `${property}-bottom: ${formatted};`, }, - l: {name: `${prefix}l_${to_variable_name(value)}`, css: `${property}-left: ${formatted};`}, + l: { + name: `${prefix}l_${format_variable_name(value)}`, + css: `${property}-left: ${formatted};`, + }, x: { - name: `${prefix}x_${to_variable_name(value)}`, + name: `${prefix}x_${format_variable_name(value)}`, css: `${property}-left: ${formatted};\t${property}-right: ${formatted};`, }, y: { - name: `${prefix}y_${to_variable_name(value)}`, + name: `${prefix}y_${format_variable_name(value)}`, css: `${property}-top: ${formatted};\t${property}-bottom: ${formatted};`, }, }; @@ -200,30 +186,6 @@ export const generate_directional_classes = ( ); }; -/** - * Generate classes for properties with axis variants (e.g. overflow, overflow-x, overflow-y). - * - * @param property - The base CSS property name (e.g. 'overflow') - * @param values - The values to generate classes for - */ -export const generate_property_with_axes = ( - property: string, - values: Iterable, -): Record => { - return generate_classes( - (axis: string, value: string) => { - const prop = axis === '' ? property : `${property}-${axis}`; - const prefix = axis === '' ? property : `${property}_${axis}`; - return { - name: `${to_variable_name(prefix)}_${to_variable_name(value)}`, - css: `${prop}: ${value};`, - }; - }, - ['', 'x', 'y'], - values, - ); -}; - /** * Generate border radius corner classes for all four corners. * Creates classes for top-left, top-right, bottom-left, bottom-right corners. @@ -234,7 +196,7 @@ export const generate_property_with_axes = ( export const generate_border_radius_corners = ( values: Iterable, formatter?: (value: string) => string, -): Record => { +): Record => { const corners = [ {prop: 'border-top-left-radius', name: 'border_top_left_radius'}, {prop: 'border-top-right-radius', name: 'border_top_right_radius'}, @@ -244,7 +206,7 @@ export const generate_border_radius_corners = ( return generate_classes( (corner: (typeof corners)[0], value: string) => ({ - name: `${corner.name}_${to_variable_name(value)}`, + name: `${corner.name}_${format_variable_name(value)}`, css: `${corner.prop}: ${formatter?.(value) ?? value};`, }), corners, @@ -263,7 +225,7 @@ export const generate_border_radius_corners = ( export const generate_shadow_classes = ( sizes: Iterable, alpha_mapping: Record, -): Record => { +): Record => { const shadow_types = [ {prefix: 'shadow', var_prefix: 'shadow'}, {prefix: 'shadow_top', var_prefix: 'shadow_top'}, diff --git a/src/lib/css_class_helpers.ts b/src/lib/css_class_helpers.ts deleted file mode 100644 index 3051d076a..000000000 --- a/src/lib/css_class_helpers.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type {Logger} from '@fuzdev/fuz_util/log.js'; - -// TODO maybe just use the Svelte (and Oxc?) parser instead of this regexp madness? - -export interface CssExtractor { - matcher: RegExp; - mapper: (matched: RegExpExecArray) => Array; -} - -const CSS_CLASS_EXTRACTORS: Array = [ - // `class:a` - { - matcher: /(?]+)/gi, - mapper: (matched) => [matched[1]!], - }, // initial capture group is fake just because the second regexp uses a capture group for its back reference - - // `class="a"`, `classes="a"`, `classes = 'a b'`, `classes: 'a b'` with any whitespace around the `=`/`:` - { - matcher: /(? - matched[2]! - .replace( - // omit all expressions with best-effort - it's not perfect especially - // around double quote strings in JS in Svelte expressions, but using single quotes is better imo - /\S*{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*}\S*/g, - // same failures: - // /\S*{(?:[^{}]*|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.|\$\{(?:[^{}]*|{[^{}]*})*\})*`|{(?:[^{}]*|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.|\$\{(?:[^{}]*|{[^{}]*})*\})*`)*})*}\S*/g, - // 3 failures: - // /\S*{(?:[^{}`'"]*|[`'"]((?:[^\\`'"]|\\.|\$\{[^}]*\})*)[`'"]|{[^{}]*})*}\S*/g, - '', - ) - .split(/\s+/) - .filter(Boolean), - }, - // arrays like `class: ['a', 'b']`, `classes = ['a', 'b']`, `classes={['a', 'b']` - { - matcher: /(? => { - const content = matched[1]!; - if (content.includes('[')) return []; // TODO @many ideally fix instead of bailing, but maybe we need a real JS parser? - const items = content.split(',').map((item) => item.trim()); - - return items - .reduce((classes: Array, item: string) => { - // Match string literals within each item - const string_literals = item.match(/(['"`])((?:(?!\1)[^\\]|\\.)*?)\1/g); - if (!string_literals) return classes; - - // Check if the item contains concatenation - const has_concatenation = /\s*\+\s*/.test(item); - - if (!has_concatenation && string_literals.length === 1) { - const content = string_literals[0].slice(1, -1); // remove quotes - if (!content.includes('${')) { - classes.push(content.replace(/\\(['"`])/g, '$1').trim()); - } - } - - return classes; - }, []) - .filter(Boolean); // Filter out empty strings - }, - }, -]; - -/** - * Returns a Set of CSS classes from a string of HTML/Svelte/JS/TS content. - * Handles class attributes, directives, and various forms of CSS class declarations. - */ -export const collect_css_classes = ( - contents: string, - extractors: Array = CSS_CLASS_EXTRACTORS, -): Set => { - const all_classes: Set = new Set(); - - for (const extractor of extractors) { - for (const c of extract_classes(contents, extractor)) { - all_classes.add(c); - } - } - - return all_classes; -}; - -const extract_classes = (contents: string, {matcher, mapper}: CssExtractor): Set => { - const classes: Set = new Set(); - let matched: RegExpExecArray | null; - while ((matched = matcher.exec(contents)) !== null) { - for (const c of mapper(matched)) { - classes.add(c); - } - } - return classes; -}; - -export class CssClasses { - include_classes: Set | null; - - #all: Set = new Set(); - - #by_id: Map> = new Map(); - - #dirty = true; - - constructor(include_classes: Set | null = null) { - this.include_classes = include_classes; - } - - add(id: string, classes: Set): void { - this.#dirty = true; - this.#by_id.set(id, classes); - } - - delete(id: string): void { - this.#dirty = true; - this.#by_id.delete(id); - } - - get(): Set { - if (this.#dirty) { - this.#dirty = false; - this.#recalculate(); - } - return this.#all; - } - - #recalculate(): void { - this.#all.clear(); - if (this.include_classes) { - for (const c of this.include_classes) { - this.#all.add(c); - } - } - for (const classes of this.#by_id.values()) { - for (const c of classes) { - this.#all.add(c); - } - } - } -} - -export type CssClassDeclaration = - | CssClassDeclarationItem - | CssClassDeclarationGroup - | CssClassDeclarationInterpreter; - -export interface CssClassDeclarationBase { - comment?: string; -} - -export interface CssClassDeclarationItem extends CssClassDeclarationBase { - declaration: string; -} -export interface CssClassDeclarationGroup extends CssClassDeclarationBase { - ruleset: string; -} -export interface CssClassDeclarationInterpreter extends CssClassDeclarationBase { - pattern: RegExp; - interpret: (matched: RegExpMatchArray, log?: Logger) => string | null; -} - -export const generate_classes_css = ( - classes: Iterable, - classes_by_name: Record, - interpreters: Array, - log?: Logger, -): string => { - // TODO when the API is redesigned this kind of thing should be cached - // Create a map that has the index of each class name as the key - const indexes: Map = new Map(); - const keys = Object.keys(classes_by_name); - for (let i = 0; i < keys.length; i++) { - indexes.set(keys[i]!, i); - } - - // If any classes are unknown, just put them at the end - const sorted_classes = (Array.isArray(classes) ? classes : Array.from(classes)).sort((a, b) => { - const index_a = indexes.get(a) ?? Number.MAX_VALUE; - const index_b = indexes.get(b) ?? Number.MAX_VALUE; - if (index_a !== index_b) return index_a - index_b; - return a.localeCompare(b); // alphabetic tiebreaker for stable sort - }); - - let css = ''; - for (const c of sorted_classes) { - let v = classes_by_name[c]; - - // If not found statically, try interpreters - if (!v) { - for (const interpreter of interpreters) { - const matched = c.match(interpreter.pattern); - if (matched) { - const declaration = interpreter.interpret(matched, log); - if (declaration) { - v = {declaration, comment: interpreter.comment}; - break; - } - } - } - } - - if (!v) { - // diagnostic - // if (!/^[a-z_0-9]+$/.test(c)) { - // console.error('invalid class detected, fix the regexps', c); - // } - continue; - } - - const {comment} = v; - - if (comment) { - css += comment.includes('\n') ? `/*\n${comment}\n*/\n` : `/** ${comment} */\n`; - } - - if ('declaration' in v) { - css += `.${c} { ${v.declaration} }\n`; - } else if ('ruleset' in v) { - css += v.ruleset + '\n'; - } - // Note: Interpreted types are converted to declaration above, so no else clause needed - } - - return css; -}; diff --git a/src/lib/css_class_interpreters.ts b/src/lib/css_class_interpreters.ts index 9139bb22f..3cd5b09a3 100644 --- a/src/lib/css_class_interpreters.ts +++ b/src/lib/css_class_interpreters.ts @@ -1,99 +1,191 @@ -import type {CssClassDeclarationInterpreter} from './css_class_helpers.js'; -import {Z_INDEX_MAX} from './variable_data.js'; +import {escape_css_selector, type CssClassDefinitionInterpreter} from './css_class_generation.js'; +import { + is_possible_css_literal, + interpret_css_literal, + generate_css_literal_simple, + extract_segments, + extract_and_validate_modifiers, + type CssLiteralOutput, +} from './css_literal.js'; +import {generate_modified_ruleset} from './css_ruleset_parser.js'; +import {resolve_class_definition} from './css_class_resolution.js'; /** - * Interpreter for opacity classes (opacity_0 through opacity_100). + * Interpreter for modified token/composite classes (e.g., `hover:p_md`, `md:box`, `dark:hover:panel`). + * Applies modifiers to existing declaration-based or ruleset-based classes. + * + * This interpreter must run BEFORE css_literal_interpreter to handle cases like `hover:box` + * where `box` is a known class (not a CSS property). */ -export const opacity_interpreter: CssClassDeclarationInterpreter = { - pattern: /^opacity_(\d+)$/, - interpret: (matched, log) => { - const value = parseInt(matched[1]!, 10); - if (value < 0 || value > 100) { - log?.warn(`Invalid opacity value: ${value}. Must be between 0 and 100.`); +export const modified_class_interpreter: CssClassDefinitionInterpreter = { + pattern: /^.+:.+$/, + interpret: (matched, ctx) => { + const class_name = matched[0]; + const segments = extract_segments(class_name); + + // Extract modifiers from the front + const result = extract_and_validate_modifiers(segments, class_name); + + if (!result.ok) { + // Modifier validation error - let css_literal_interpreter try return null; } - return `opacity: ${value === 0 ? '0' : value === 100 ? '1' : `${value}%`};`; - }, - // comment: 'Interpreted opacity value', -}; -/** - * Interpreter for font-weight classes, - * `font_weight_1` through `font_weight_1000` following the CSS spec. - */ -export const font_weight_interpreter: CssClassDeclarationInterpreter = { - pattern: /^font_weight_(\d+)$/, - interpret: (matched, log) => { - const value = parseInt(matched[1]!, 10); - if (value < 1 || value > 1000) { - log?.warn(`Invalid font-weight value: ${value}. Must be between 1 and 1000.`); + const {modifiers, remaining} = result; + + // Must have exactly one remaining segment (the base class name) + if (remaining.length !== 1) { return null; } - return `font-weight: ${value}; --font_weight: ${value};`; - }, -}; -/** - * Interpreter for border-radius percentage classes, - * `border_radius_0` through `border_radius_100`. - */ -export const border_radius_interpreter: CssClassDeclarationInterpreter = { - pattern: /^border_radius_(\d+)$/, - interpret: (matched, log) => { - const value = parseInt(matched[1]!, 10); - if (value < 0 || value > 100) { - log?.warn(`Invalid border-radius percentage: ${value}. Must be between 0 and 100.`); + const base_class_name = remaining[0]!; + + // Check if the base class is known + const base_class = ctx.class_definitions[base_class_name]; + if (!base_class) { return null; } - return `border-radius: ${value === 0 ? '0' : `${value}%`};`; - }, -}; -/** - * Interpreter for border radius corner percentage classes, - * handles all four corners: top-left, top-right, bottom-left, bottom-right. - * Examples: `border_top_left_radius_50`, `border_bottom_right_radius_100`. - */ -export const border_radius_corners_interpreter: CssClassDeclarationInterpreter = { - pattern: /^border_(top|bottom)_(left|right)_radius_(\d+)$/, - interpret: (matched, log) => { - const vertical = matched[1]!; - const horizontal = matched[2]!; - const value = parseInt(matched[3]!, 10); - if (value < 0 || value > 100) { - log?.warn( - `Invalid border-${vertical}-${horizontal}-radius percentage: ${value}. Must be between 0 and 100.`, - ); + // Must have at least one modifier (otherwise it's just the base class) + const has_modifiers = + modifiers.media || + modifiers.ancestor || + modifiers.states.length > 0 || + modifiers.pseudo_element; + if (!has_modifiers) { return null; } - return `border-${vertical}-${horizontal}-radius: ${value === 0 ? '0' : `${value}%`};`; + + const escaped_class_name = escape_css_selector(class_name); + + // Build state and pseudo-element CSS suffixes + let state_css = ''; + for (const state of modifiers.states) { + state_css += state.css; + } + const pseudo_element_css = modifiers.pseudo_element?.css ?? ''; + + // Handle composes-based or declaration-based definitions + if ('composes' in base_class || 'declaration' in base_class) { + const resolution_result = resolve_class_definition( + base_class, + base_class_name, + ctx.class_definitions, + ctx.css_properties, + ); + if (!resolution_result.ok) { + ctx.diagnostics.push(resolution_result.error); + return null; + } + // Add warnings if any + if (resolution_result.warnings) { + for (const warning of resolution_result.warnings) { + ctx.diagnostics.push(warning); + } + } + if (!resolution_result.declaration) { + return null; + } + + // Build the selector + let selector = `.${escaped_class_name}`; + selector += state_css; + selector += pseudo_element_css; + + // Create output compatible with generate_css_literal_simple + const output: CssLiteralOutput = { + declaration: resolution_result.declaration, + selector, + media_wrapper: modifiers.media?.css ?? null, + ancestor_wrapper: modifiers.ancestor?.css ?? null, + }; + + const css = generate_css_literal_simple(output); + return css.trimEnd(); + } + + // Handle ruleset-based classes + if ('ruleset' in base_class && base_class.ruleset) { + const result = generate_modified_ruleset( + base_class.ruleset, + base_class_name, + escaped_class_name, + state_css, + pseudo_element_css, + modifiers.media?.css ?? null, + modifiers.ancestor?.css ?? null, + ); + + // Emit warnings for skipped modifiers + if (result.skipped_modifiers) { + for (const skipped of result.skipped_modifiers) { + if (skipped.reason === 'pseudo_element_conflict') { + ctx.diagnostics.push({ + level: 'warning', + class_name, + message: `Rule "${skipped.selector}" already contains a pseudo-element; "${skipped.conflicting_modifier}" modifier was not applied`, + suggestion: `The rule is included with just the class renamed`, + }); + } else { + ctx.diagnostics.push({ + level: 'warning', + class_name, + message: `Rule "${skipped.selector}" already contains "${skipped.conflicting_modifier}"; modifier was not applied to avoid redundancy`, + suggestion: `The rule is included with just the class renamed`, + }); + } + } + } + + return result.css.trimEnd(); + } + + return null; }, }; /** - * Interpreter for z-index classes, - * `z_index_0` through `z_index_${Z_INDEX_MAX}` (max CSS z-index). + * Interpreter for CSS-literal classes (e.g., `display:flex`, `hover:opacity:80%`). + * Generates full CSS rulesets including any modifier wrappers. */ -export const z_index_interpreter: CssClassDeclarationInterpreter = { - pattern: /^z_index_(\d+)$/, - interpret: (matched, log) => { - const value = parseInt(matched[1]!, 10); - if (value < 0 || value > Z_INDEX_MAX) { - log?.warn(`Invalid z-index value: ${value}. Must be between 0 and ${Z_INDEX_MAX}.`); +export const css_literal_interpreter: CssClassDefinitionInterpreter = { + pattern: /^.+:.+$/, + interpret: (matched, ctx) => { + const class_name = matched[0]; + + if (!is_possible_css_literal(class_name)) { + return null; + } + + const escaped_class_name = escape_css_selector(class_name); + const result = interpret_css_literal(class_name, escaped_class_name, ctx.css_properties); + + if (!result.ok) { + ctx.diagnostics.push(result.error); return null; } - return `z-index: ${value};`; + + // Collect warnings for upstream handling + if (result.warnings) { + for (const warning of result.warnings) { + ctx.diagnostics.push(warning); + } + } + + // Generate the full CSS including any wrappers + const css = generate_css_literal_simple(result.output); + + // Return the CSS but strip trailing newline for consistency + return css.trimEnd(); }, }; /** * Collection of all builtin interpreters for dynamic CSS class generation. + * Order matters: modified_class_interpreter runs first to handle `hover:box` before + * css_literal_interpreter tries to interpret it as `hover:box` (property:value). */ -export const css_class_interpreters: Array = [ - opacity_interpreter, - font_weight_interpreter, - border_radius_interpreter, - border_radius_corners_interpreter, - z_index_interpreter, - // add new default interpreters here +export const css_class_interpreters: Array = [ + modified_class_interpreter, + css_literal_interpreter, ]; diff --git a/src/lib/css_class_resolution.ts b/src/lib/css_class_resolution.ts new file mode 100644 index 000000000..d5036601a --- /dev/null +++ b/src/lib/css_class_resolution.ts @@ -0,0 +1,333 @@ +/** + * CSS class resolution utilities for composing class definitions. + * + * Provides the `resolve_composes` helper to recursively resolve class names + * to their combined CSS declarations. + * + * @module + */ + +import {levenshtein_distance} from '@fuzdev/fuz_util/string.js'; + +import type {InterpreterDiagnostic} from './diagnostics.js'; +import type {CssClassDefinition, CssClassDefinitionStatic} from './css_class_generation.js'; +import { + try_resolve_literal, + extract_segments, + extract_and_validate_modifiers, + has_extracted_modifiers, + suggest_modifier, +} from './css_literal.js'; + +/** + * Result from resolving a `composes` array to combined declarations. + */ +export type ResolveComposesResult = + | {ok: true; declaration: string; warnings: Array | null} + | {ok: false; error: InterpreterDiagnostic}; + +/** + * Resolves a class definition's declaration, handling `composes` composition. + * + * If the definition has a `composes` property, resolves those classes recursively + * and combines with any explicit `declaration`. If no `composes`, returns the + * explicit `declaration` directly. + * + * @param def - The class definition to resolve + * @param class_name - The name of the class being resolved (for error messages) + * @param definitions - Record of all known class definitions + * @param css_properties - Set of valid CSS properties for literal validation, or null to skip + * @returns Combined declaration or an error + */ +export const resolve_class_definition = ( + def: CssClassDefinitionStatic, + class_name: string, + definitions: Record, + css_properties: Set | null = null, +): ResolveComposesResult => { + let warnings: Array | null = null; + + // Handle composes-based definitions + if ('composes' in def && def.composes) { + const result = resolve_composes( + def.composes, + definitions, + new Set([class_name]), + new Set(), + class_name, + css_properties, + ); + if (!result.ok) return result; + + // Combine resolved declarations with explicit declaration (if present) + let combined = result.declaration; + if ('declaration' in def) { + const trimmed = def.declaration?.trim(); + if (trimmed) { + combined = combined ? `${combined} ${trimmed}` : trimmed; + } else if (def.declaration !== undefined) { + // Warn about empty declaration + warnings = result.warnings ? [...result.warnings] : []; + warnings.push({ + level: 'warning', + class_name, + message: `Class "${class_name}" has an empty declaration`, + suggestion: 'Remove the empty declaration property or add CSS', + }); + } + } + if (!warnings && result.warnings) { + warnings = result.warnings; + } + return {ok: true, declaration: combined, warnings}; + } + + // Handle declaration-only definitions + if ('declaration' in def) { + const trimmed = def.declaration?.trim(); + if (!trimmed && def.declaration !== undefined) { + warnings = [ + { + level: 'warning', + class_name, + message: `Class "${class_name}" has an empty declaration`, + suggestion: 'Remove the empty declaration property or add CSS', + }, + ]; + } + return {ok: true, declaration: trimmed ?? '', warnings}; + } + + // Ruleset definitions don't have a single declaration + return {ok: true, declaration: '', warnings: null}; +}; + +/** + * Resolves an array of class names to their combined CSS declarations. + * + * Recursively resolves nested `composes` arrays and combines all declarations. + * Validates that referenced classes exist and are resolvable (not rulesets or interpreters). + * Supports unmodified CSS literals (e.g., `text-align:center`) in the composes array. + * + * Deduplication behavior: + * - Diamond dependencies (class reached via different composition branches) are silently skipped + * - Redundant listings (class already included by an earlier sibling in this array) emit a warning + * + * @param class_names - Array of class names to resolve + * @param definitions - Record of all known class definitions + * @param resolution_stack - Set of class names currently being resolved (for cycle detection) + * @param visited - Set of all class names already resolved (for deduplication) + * @param original_class_name - The class name being defined (for error messages) + * @param css_properties - Set of valid CSS properties for literal validation, or null to skip + * @returns Combined declarations or an error + * @mutates resolution_stack - Temporarily adds/removes names during recursion + * @mutates visited - Adds resolved class names for deduplication + */ +export const resolve_composes = ( + class_names: Array, + definitions: Record, + resolution_stack: Set, + visited: Set, + original_class_name: string, + css_properties: Set | null = null, +): ResolveComposesResult => { + const declarations: Array = []; + let warnings: Array | null = null; + + // Snapshot of visited at start - classes added during this array's processing are redundant + const visited_at_start: ReadonlySet = new Set(visited); + + for (const name of class_names) { + // Cycle detection (current path) + if (resolution_stack.has(name)) { + const cycle_path = [...resolution_stack, name].join(' → '); + return { + ok: false, + error: { + level: 'error', + class_name: original_class_name, + message: `Circular reference detected: ${cycle_path}`, + suggestion: 'Remove the circular dependency from composes arrays', + }, + }; + } + + // Deduplication - skip if already resolved + if (visited.has(name)) { + // Warn if redundant (added during this array's processing), not diamond (was already there) + if (!visited_at_start.has(name)) { + warnings ??= []; + warnings.push({ + level: 'warning', + class_name: original_class_name, + message: `Class "${name}" is redundant`, + suggestion: 'Already included by another class in this definition', + }); + } + continue; + } + + const def = definitions[name]; + if (!def) { + // Check if this looks like a modified class (hover:box, md:p_lg) + const segments = extract_segments(name); + if (segments.length >= 2) { + const mod_result = extract_and_validate_modifiers(segments, name); + if ( + mod_result.ok && + mod_result.remaining.length === 1 && + has_extracted_modifiers(mod_result.modifiers) + ) { + const base_name = mod_result.remaining[0]!; + const base_def = definitions[base_name]; + if (base_def) { + // Modified existing class - can't compose + return { + ok: false, + error: { + level: 'error', + class_name: original_class_name, + message: `Modified class "${name}" cannot be used in composes array`, + suggestion: 'Apply modified classes directly in markup, not in composes arrays', + }, + }; + } else { + // Looks like modifier:unknown - report the unknown base + return { + ok: false, + error: { + level: 'error', + class_name: original_class_name, + message: `Unknown class "${base_name}" in composes array`, + suggestion: `Check that "${base_name}" is defined in class_definitions`, + }, + }; + } + } + } + + // Try parsing as CSS literal + const literal_result = try_resolve_literal(name, css_properties, original_class_name); + if (literal_result.ok) { + visited.add(name); + declarations.push(literal_result.declaration); + if (literal_result.warnings) { + warnings ??= []; + warnings.push(...literal_result.warnings); + } + continue; + } + // If literal parsing returned an error, check for modifier typo pattern + if (literal_result.error) { + // Check for typo'd modifier where last segment is a known class + // Handles: hovr:box, md:hovr:box, etc. + const prop_match = /Unknown CSS property "([^"]+)"/.exec(literal_result.error.message); + if (prop_match) { + const failed_prop = prop_match[1]!; + const suggested = suggest_modifier(failed_prop); + // Only suggest if it's a close typo (edit distance 1) + if (suggested && levenshtein_distance(failed_prop, suggested) === 1) { + // Check if the last segment is a known class + const last_colon = name.lastIndexOf(':'); + const potential_class = name.slice(last_colon + 1); + if (definitions[potential_class]) { + // Reconstruct the corrected class name + const corrected = + name.slice(0, name.lastIndexOf(failed_prop)) + suggested + ':' + potential_class; + return { + ok: false, + error: { + level: 'error', + class_name: original_class_name, + message: `Unknown modifier "${failed_prop}" with class "${potential_class}"`, + suggestion: `Did you mean "${corrected}"? Note: modified classes cannot be used in composes`, + }, + }; + } + } + } + return {ok: false, error: literal_result.error}; + } + // Not a literal - fall through to original "unknown class" error + return { + ok: false, + error: { + level: 'error', + class_name: original_class_name, + message: `Unknown class "${name}" in composes array`, + suggestion: `Check that "${name}" is defined in class_definitions`, + }, + }; + } + + // Only static definitions allowed (not interpreters) + if ('pattern' in def) { + return { + ok: false, + error: { + level: 'error', + class_name: original_class_name, + message: `Cannot reference interpreter pattern "${name}" in composes array`, + suggestion: 'Only static class definitions can be referenced', + }, + }; + } + + // Ruleset not allowed in composes + if ('ruleset' in def && def.ruleset) { + return { + ok: false, + error: { + level: 'error', + class_name: original_class_name, + message: `Cannot reference ruleset class "${name}" in composes array`, + suggestion: 'Ruleset classes have multiple selectors and cannot be inlined', + }, + }; + } + + // Mark as visited before processing (for deduplication) + visited.add(name); + + // Recursive resolution for nested composes + if ('composes' in def && def.composes) { + resolution_stack.add(name); + const nested = resolve_composes( + def.composes, + definitions, + resolution_stack, + visited, + original_class_name, + css_properties, + ); + resolution_stack.delete(name); + if (!nested.ok) return nested; // Propagate error + if (nested.declaration) { + declarations.push(nested.declaration); + } + if (nested.warnings) { + warnings ??= []; + warnings.push(...nested.warnings); + } + } + + // Add the declaration if present (trimmed) + if ('declaration' in def) { + const trimmed = def.declaration?.trim(); + if (trimmed) { + declarations.push(trimmed); + } else if (def.declaration !== undefined) { + // Warn about empty declaration + warnings ??= []; + warnings.push({ + level: 'warning', + class_name: name, + message: `Class "${name}" has an empty declaration`, + suggestion: 'Remove the empty declaration property or add CSS', + }); + } + } + } + + return {ok: true, declaration: declarations.join(' '), warnings}; +}; diff --git a/src/lib/css_classes.ts b/src/lib/css_classes.ts index c9c7cae27..5afc78abc 100644 --- a/src/lib/css_classes.ts +++ b/src/lib/css_classes.ts @@ -1,407 +1,200 @@ -import type {CssClassDeclaration} from './css_class_helpers.js'; -import { - generate_classes, - to_kebab, - CSS_GLOBALS, - COLOR_INTENSITIES, - generate_property_classes, - generate_directional_classes, - generate_property_with_axes, - generate_border_radius_corners, - generate_shadow_classes, - format_spacing_value, - format_dimension_value, -} from './css_class_generators.js'; -import { - space_variants, - color_variants, - text_color_variants, - font_size_variants, - icon_size_variants, - line_height_variants, - border_radius_variants, - border_width_variants, - alignment_values, - justify_values, - overflow_values, - border_style_values, - display_values, - text_align_values, - vertical_align_values, - word_break_values, - position_values, - visibility_values, - float_values, - flex_wrap_values, - flex_direction_values, - overflow_wrap_values, - scrollbar_width_values, - scrollbar_gutter_values, - shadow_semantic_values, - shadow_alpha_variants, -} from './variable_data.js'; -import {css_class_composites} from './css_class_composites.js'; - -// TODO add animation support, either as a separate thing or rename `css_classes_by_name` to be more generic, like `css_by_name` - need to collect `animation: foo ...` names like we do classes - -// TODO think about variable support (much harder problem, need dependency graph) - -// TODO modifiers for :hover/:active/:focus (how? do we need to give up the compat with JS identifier names?) - /** - * @see `generate_classes_css` + * Collection management for extracted CSS classes. + * + * Tracks classes per-file for efficient incremental updates during watch mode. + * Uses `null` instead of empty collections to avoid allocation overhead. + * + * @module */ -export const css_classes_by_name: Record = { - // Composite classes go first, so they can be overridden by the more specific classes. - ...css_class_composites, - - ...generate_property_classes('position', position_values), - ...generate_property_classes('position', CSS_GLOBALS, to_kebab), - - ...generate_property_classes('display', display_values), - ...generate_property_classes('display', CSS_GLOBALS, to_kebab), - - ...generate_property_classes('visibility', visibility_values), - ...generate_property_classes('visibility', CSS_GLOBALS, to_kebab), - - ...generate_property_classes('float', float_values), - ...generate_property_classes('float', CSS_GLOBALS, to_kebab), - - ...generate_property_with_axes('overflow', overflow_values), - - ...generate_property_classes('overflow-wrap', overflow_wrap_values), - ...generate_property_classes('overflow-wrap', CSS_GLOBALS, to_kebab, 'overflow_wrap'), - - ...generate_property_classes('scrollbar-width', scrollbar_width_values), - ...generate_property_classes('scrollbar-width', CSS_GLOBALS, to_kebab, 'scrollbar_width'), - - ...generate_property_classes('scrollbar-gutter', scrollbar_gutter_values), - ...generate_property_classes('scrollbar-gutter', CSS_GLOBALS, to_kebab, 'scrollbar_gutter'), - - flex_1: {declaration: 'flex: 1;'}, - ...generate_property_classes('flex-wrap', flex_wrap_values), - ...generate_property_classes('flex-wrap', CSS_GLOBALS, to_kebab), - ...generate_property_classes('flex-direction', flex_direction_values), - ...generate_property_classes('flex-direction', CSS_GLOBALS, to_kebab), - flex_grow_1: {declaration: 'flex-grow: 1;'}, - flex_grow_0: {declaration: 'flex-grow: 0;'}, - flex_shrink_1: {declaration: 'flex-shrink: 1;'}, - flex_shrink_0: {declaration: 'flex-shrink: 0;'}, - - ...generate_property_classes('align-items', alignment_values), - ...generate_property_classes('align-content', [ - ...alignment_values, - 'space-between', - 'space-around', - 'space-evenly', - ]), - ...generate_property_classes('align-self', alignment_values), - ...generate_property_classes('justify-content', justify_values), - ...generate_property_classes('justify-items', [ - 'center', - 'start', - 'end', - 'left', - 'right', - 'baseline', - 'stretch', - ]), - ...generate_property_classes('justify-self', [ - 'center', - 'start', - 'end', - 'left', - 'right', - 'baseline', - 'stretch', - ]), - flip_x: {declaration: 'transform: scaleX(-1);'}, - flip_y: {declaration: 'transform: scaleY(-1);'}, - flip_xy: {declaration: 'transform: scaleX(-1) scaleY(-1);'}, - - font_family_sans: {declaration: 'font-family: var(--font_family_sans);'}, - font_family_serif: {declaration: 'font-family: var(--font_family_serif);'}, - font_family_mono: {declaration: 'font-family: var(--font_family_mono);'}, - - ...generate_property_classes('line-height', ['0', '1', ...line_height_variants], (v) => - v === '0' || v === '1' ? v : `var(--line_height_${v})`, - ), - ...generate_property_classes( - 'font-size', - font_size_variants, - (v) => `var(--font_size_${v}); --font_size: var(--font_size_${v})`, - ), - ...generate_property_classes( - 'font-size', - icon_size_variants, - (v) => `var(--icon_size_${v}); --font_size: var(--icon_size_${v})`, - 'icon_size', - ), - // TODO some of these need to be filled out and include CSS_GLOBALS (but maybe the API should be opt-out?) - ...generate_property_classes('text-align', text_align_values), - ...generate_property_classes('vertical-align', vertical_align_values), - ...generate_property_classes('word-break', word_break_values), - ...generate_property_classes('word-break', CSS_GLOBALS, to_kebab), - ...generate_property_classes('white-space', [ - 'normal', - 'nowrap', - 'pre', - 'pre-wrap', - 'pre-line', - 'break-spaces', - ]), - ...generate_property_classes('white-space-collapse', [ - 'collapse', - 'preserve', - 'preserve-breaks', - 'preserve-spaces', - 'break-spaces', - ]), - ...generate_property_classes('white-space-collapse', CSS_GLOBALS, to_kebab), - ...generate_property_classes('text-wrap', ['wrap', 'nowrap', 'balance', 'pretty', 'stable']), - ...generate_property_classes('user-select', ['none', 'auto', 'text', 'all']), - ...generate_property_classes('user-select', CSS_GLOBALS, to_kebab), - - /* - colors +import type {SourceLocation, ExtractionDiagnostic} from './diagnostics.js'; - */ - ...generate_property_classes( - 'color', - text_color_variants.map(String), - (v) => `var(--text_color_${v})`, - 'text_color', - ), - ...generate_property_classes( - 'background-color', - [1, 2, 3, 4, 5, 6, 7, 8, 9].map(String), - (v) => `var(--darken_${v})`, - 'darken', - ), - ...generate_property_classes( - 'background-color', - [1, 2, 3, 4, 5, 6, 7, 8, 9].map(String), - (v) => `var(--lighten_${v})`, - 'lighten', - ), - bg: {declaration: 'background-color: var(--bg);'}, - fg: {declaration: 'background-color: var(--fg);'}, - ...generate_property_classes( - 'background-color', - [1, 2, 3, 4, 5, 6, 7, 8, 9].map(String), - (v) => `var(--bg_${v})`, - 'bg', - ), - ...generate_property_classes( - 'background-color', - [1, 2, 3, 4, 5, 6, 7, 8, 9].map(String), - (v) => `var(--fg_${v})`, - 'fg', - ), - ...generate_property_classes( - 'color', - [1, 2, 3, 4, 5, 6, 7, 8, 9].map(String), - (v) => `var(--darken_${v})`, - 'color_darken', - ), - ...generate_property_classes( - 'color', - [1, 2, 3, 4, 5, 6, 7, 8, 9].map(String), - (v) => `var(--lighten_${v})`, - 'color_lighten', - ), - color_bg: {declaration: 'color: var(--bg);'}, - color_fg: {declaration: 'color: var(--fg);'}, - ...generate_property_classes( - 'color', - [1, 2, 3, 4, 5, 6, 7, 8, 9].map(String), - (v) => `var(--bg_${v})`, - 'color_bg', - ), - ...generate_property_classes( - 'color', - [1, 2, 3, 4, 5, 6, 7, 8, 9].map(String), - (v) => `var(--fg_${v})`, - 'color_fg', - ), - ...generate_classes( - (hue: string) => ({ - name: `hue_${hue}`, - css: `--hue: var(--hue_${hue});`, - }), - color_variants, - ), - ...generate_classes( - (hue: string, intensity: string) => ({ - name: `color_${hue}_${intensity}`, - css: `color: var(--color_${hue}_${intensity});`, - }), - color_variants, - COLOR_INTENSITIES, - ), - ...generate_classes( - (hue: string, intensity: string) => ({ - name: `bg_${hue}_${intensity}`, - css: `background-color: var(--color_${hue}_${intensity});`, - }), - color_variants, - COLOR_INTENSITIES, - ), - ...generate_property_classes( - 'border-color', - [1, 2, 3, 4, 5].map(String), - (v) => `var(--border_color_${v})`, - ), - ...generate_property_classes('border-color', color_variants, (v) => `var(--border_color_${v})`), - border_color_transparent: {declaration: 'border-color: transparent;'}, - ...generate_property_classes( - 'outline-color', - [1, 2, 3, 4, 5].map(String), - (v) => `var(--border_color_${v})`, - ), - ...generate_property_classes('outline-color', color_variants, (v) => `var(--border_color_${v})`), - outline_color_transparent: {declaration: 'outline-color: transparent;'}, - - ...generate_property_classes('border-width', ['0', ...border_width_variants.map(String)], (v) => - v === '0' ? '0' : `var(--border_width_${v})`, - ), - ...generate_property_classes('outline-width', ['0', ...border_width_variants.map(String)], (v) => - v === '0' ? '0' : `var(--border_width_${v})`, - ), - outline_width_focus: {declaration: 'outline-width: var(--outline_width_focus);'}, - outline_width_active: {declaration: 'outline-width: var(--outline_width_active);'}, - - ...generate_property_classes('border-style', border_style_values), - ...generate_property_classes('border-style', CSS_GLOBALS, to_kebab), - - ...generate_property_classes('outline-style', border_style_values), - ...generate_property_classes('outline-style', CSS_GLOBALS, to_kebab), - - ...generate_property_classes( - 'border-radius', - border_radius_variants, - (v) => `var(--border_radius_${v})`, - ), - ...generate_border_radius_corners(border_radius_variants, (v) => `var(--border_radius_${v})`), - - /* - - shadows - - */ - ...generate_shadow_classes(['xs', 'sm', 'md', 'lg', 'xl'], { - xs: '1', - sm: '2', - md: '3', - lg: '4', - xl: '5', - }), - ...generate_classes( - (value: string) => ({ - name: `shadow_color_${value}`, - css: `--shadow_color: var(--shadow_color_${value});`, - }), - shadow_semantic_values, - ), - ...generate_classes( - (hue: string) => ({ - name: `shadow_color_${hue}`, - css: `--shadow_color: var(--shadow_color_${hue});`, - }), - color_variants, - ), - ...generate_classes( - (alpha: number) => ({ - name: `shadow_alpha_${alpha}`, - css: `--shadow_alpha: var(--shadow_alpha_${alpha});`, - }), - shadow_alpha_variants, - ), - - /* higher specificity */ - /* TODO which order should these be in? */ - shadow_inherit: {declaration: 'box-shadow: inherit;'}, - shadow_none: {declaration: 'box-shadow: none;'}, - - /* - - layout - - */ - ...generate_property_classes( - 'width', - [ - '0', - '100', - '1px', - '2px', - '3px', - 'auto', - 'max-content', - 'min-content', - 'fit-content', - 'stretch', - ...space_variants, - ], - format_dimension_value, - ), - ...generate_property_classes( - 'height', - [ - '0', - '100', - '1px', - '2px', - '3px', - 'auto', - 'max-content', - 'min-content', - 'fit-content', - 'stretch', - ...space_variants, - ], - format_dimension_value, - ), - - ...generate_property_classes( - 'top', - ['0', '100', '1px', '2px', '3px', 'auto', ...space_variants], - format_spacing_value, - ), - ...generate_property_classes( - 'right', - ['0', '100', '1px', '2px', '3px', 'auto', ...space_variants], - format_spacing_value, - ), - ...generate_property_classes( - 'bottom', - ['0', '100', '1px', '2px', '3px', 'auto', ...space_variants], - format_spacing_value, - ), - ...generate_property_classes( - 'left', - ['0', '100', '1px', '2px', '3px', 'auto', ...space_variants], - format_spacing_value, - ), - - ...generate_property_classes( - 'inset', - ['0', '100', '1px', '2px', '3px', 'auto', ...space_variants], - format_spacing_value, - ), - - ...generate_directional_classes( - 'padding', - ['0', '100', '1px', '2px', '3px', ...space_variants], - format_spacing_value, - ), - ...generate_directional_classes( - 'margin', - ['0', '100', '1px', '2px', '3px', 'auto', ...space_variants], - format_spacing_value, - ), - ...generate_property_classes('gap', space_variants, format_spacing_value), - ...generate_property_classes('column-gap', space_variants, format_spacing_value), - ...generate_property_classes('row-gap', space_variants, format_spacing_value), -}; +/** + * Collection of CSS classes extracted from source files. + * Tracks classes per-file for efficient incremental updates. + * Handles include/exclude filtering and explicit class tracking. + * Uses `null` instead of empty collections to avoid allocation overhead. + */ +export class CssClasses { + #include_classes: Set | null; + #exclude_classes: Set | null; + + #all: Set = new Set(); + + #all_with_locations: Map> = new Map(); + + /** Combined map with include_classes (null locations) and extracted classes (actual locations) */ + #all_with_locations_including_includes: Map | null> = new Map(); + + /** Classes by file id (files with no classes are not stored) */ + #by_id: Map>> = new Map(); + + /** Explicit classes (from @fuz-classes) by file id */ + #explicit_by_id: Map> = new Map(); + + /** Aggregated explicit classes (from extraction + include_classes, minus exclude_classes) */ + #explicit: Set | null = null; + + /** Diagnostics stored per-file so they're replaced when a file is updated */ + #diagnostics_by_id: Map> = new Map(); + + #dirty = true; + + /** + * Creates a new CssClasses collection. + * + * @param include_classes - Classes to always include (also treated as explicit for warnings) + * @param exclude_classes - Classes to exclude from output (also suppresses warnings) + */ + constructor( + include_classes: Set | null = null, + exclude_classes: Set | null = null, + ) { + this.#include_classes = include_classes; + this.#exclude_classes = exclude_classes; + } + + /** + * Adds extraction results for a file. + * Replaces any previous classes and diagnostics for this file. + * + * @param id - File identifier + * @param classes - Map of class names to their source locations, or null if none + * @param explicit_classes - Classes from @fuz-classes comments, or null if none + * @param diagnostics - Extraction diagnostics from this file, or null if none + */ + add( + id: string, + classes: Map> | null, + explicit_classes?: Set | null, + diagnostics?: Array | null, + ): void { + this.#dirty = true; + if (classes) { + this.#by_id.set(id, classes); + } else { + this.#by_id.delete(id); + } + if (explicit_classes) { + this.#explicit_by_id.set(id, explicit_classes); + } else { + this.#explicit_by_id.delete(id); + } + if (diagnostics) { + this.#diagnostics_by_id.set(id, diagnostics); + } else { + // Clear any old diagnostics for this file + this.#diagnostics_by_id.delete(id); + } + } + + delete(id: string): void { + this.#dirty = true; + this.#by_id.delete(id); + this.#explicit_by_id.delete(id); + this.#diagnostics_by_id.delete(id); + } + + /** + * Gets all unique class names as a Set (with exclude filter applied). + */ + get(): Set { + if (this.#dirty) { + this.#dirty = false; + this.#recalculate(); + } + return this.#all; + } + + /** + * Gets all classes with their source locations (with exclude filter applied). + * Locations from include_classes are null. + */ + get_with_locations(): Map | null> { + if (this.#dirty) { + this.#dirty = false; + this.#recalculate(); + } + return this.#all_with_locations_including_includes; + } + + /** + * Gets all classes and their locations in a single call. + * More efficient than calling `get()` and `get_with_locations()` separately + * when both are needed (avoids potential double recalculation). + * + * Results have exclude filter applied and explicit_classes includes include_classes. + */ + get_all(): { + all_classes: Set; + all_classes_with_locations: Map | null>; + explicit_classes: Set | null; + } { + if (this.#dirty) { + this.#dirty = false; + this.#recalculate(); + } + return { + all_classes: this.#all, + all_classes_with_locations: this.#all_with_locations_including_includes, + explicit_classes: this.#explicit, + }; + } + + /** + * Gets all extraction diagnostics from all files. + */ + get_diagnostics(): Array { + const result: Array = []; + for (const diagnostics of this.#diagnostics_by_id.values()) { + result.push(...diagnostics); + } + return result; + } + + #recalculate(): void { + this.#all.clear(); + this.#all_with_locations.clear(); + this.#all_with_locations_including_includes.clear(); + this.#explicit = null; + + const exclude = this.#exclude_classes; + + // Add include_classes first (with null locations - no source) + if (this.#include_classes) { + for (const c of this.#include_classes) { + if (exclude?.has(c)) continue; + this.#all.add(c); + this.#all_with_locations_including_includes.set(c, null); + // include_classes are also explicit (user explicitly wants them) + (this.#explicit ??= new Set()).add(c); + } + } + + // Aggregate from all files + for (const classes of this.#by_id.values()) { + for (const [cls, locations] of classes) { + if (exclude?.has(cls)) continue; + this.#all.add(cls); + const existing = this.#all_with_locations.get(cls); + if (existing) { + existing.push(...locations); + } else { + this.#all_with_locations.set(cls, [...locations]); + } + // Add to combined map only if not already from include_classes + if (!this.#all_with_locations_including_includes.has(cls)) { + this.#all_with_locations_including_includes.set(cls, this.#all_with_locations.get(cls)!); + } + } + } + + // Aggregate explicit classes from all files (minus excludes) + for (const explicit of this.#explicit_by_id.values()) { + for (const cls of explicit) { + if (exclude?.has(cls)) continue; + (this.#explicit ??= new Set()).add(cls); + } + } + } +} diff --git a/src/lib/css_literal.ts b/src/lib/css_literal.ts new file mode 100644 index 000000000..4615c1812 --- /dev/null +++ b/src/lib/css_literal.ts @@ -0,0 +1,771 @@ +/** + * CSS-literal syntax parser, validator, and interpreter. + * + * Enables writing utility classes using actual CSS syntax: + * + * - `display:flex` → `.display\:flex { display: flex; }` + * - `hover:opacity:80%` → `.hover\:opacity\:80\%:hover { opacity: 80%; }` + * - `md:dark:hover:before:opacity:80%` → nested CSS with media query, ancestor, state, pseudo-element + * + * @see {@link https://github.com/fuzdev/fuz_css} for documentation + * @module + */ + +import {levenshtein_distance} from '@fuzdev/fuz_util/string.js'; + +import {type InterpreterDiagnostic} from './diagnostics.js'; +import {get_modifier, get_all_modifier_names, type ModifierDefinition} from './modifiers.js'; + +// +// Types +// + +/** + * Parsed CSS-literal class with all components extracted. + */ +export interface ParsedCssLiteral { + /** Original class name */ + class_name: string; + /** Media modifier (breakpoint or feature query) */ + media: ModifierDefinition | null; + /** Ancestor modifier (dark/light) */ + ancestor: ModifierDefinition | null; + /** State modifiers in alphabetical order (can have multiple) */ + states: Array; + /** Pseudo-element modifier (before, after, etc.) */ + pseudo_element: ModifierDefinition | null; + /** CSS property name */ + property: string; + /** CSS value (with ~ replaced by spaces) */ + value: string; +} + +/** + * Result of parsing a CSS-literal class name. + * + * Uses a discriminated union (Result type) because parsing a single class + * is binary: it either succeeds or fails entirely. This differs from + * {@link ExtractionResult} which uses embedded diagnostics because file + * extraction can partially succeed (some classes extracted, others have errors). + * + * Uses `| null` for diagnostics to avoid allocating empty arrays. + * Callers should use a guard pattern: `if (result.diagnostics) { ... }` + */ +export type CssLiteralParseResult = + | {ok: true; parsed: ParsedCssLiteral; diagnostics: Array | null} + | {ok: false; error: InterpreterDiagnostic}; + +/** + * Extracted modifiers from a class name. + * Used by both CSS-literal parsing and modified class interpretation. + */ +export interface ExtractedModifiers { + /** Media modifier (breakpoint or feature query) */ + media: ModifierDefinition | null; + /** Ancestor modifier (dark/light) */ + ancestor: ModifierDefinition | null; + /** State modifiers in alphabetical order (can have multiple) */ + states: Array; + /** Pseudo-element modifier (before, after, etc.) */ + pseudo_element: ModifierDefinition | null; +} + +/** + * Result of extracting modifiers from segments. + */ +export type ModifierExtractionResult = + | {ok: true; modifiers: ExtractedModifiers; remaining: Array} + | {ok: false; error: InterpreterDiagnostic}; + +/** + * Result of interpreting a CSS-literal class. + * + * Uses `| null` for warnings to avoid allocating empty arrays. + * Callers should use a guard pattern: `if (result.warnings) { ... }` + */ +export type InterpretCssLiteralResult = + | {ok: true; output: CssLiteralOutput; warnings: Array | null} + | {ok: false; error: InterpreterDiagnostic}; + +// +// CSS Property Validation +// + +/** + * Loads CSS properties from @webref/css. + * Returns a fresh Set each time - callers should cache the result if needed. + */ +export const load_css_properties = async (): Promise> => { + const webref = await import('@webref/css'); + const indexed = await webref.default.index(); + return new Set(Object.keys(indexed.properties)); +}; + +/** + * Checks if a property name is a valid CSS property. + * Custom properties (--*) always return true. + * + * @param property - The CSS property name to validate + * @param properties - Set of valid CSS properties from `load_css_properties()`. + * Pass `null` to skip validation. + * @returns True if valid CSS property or custom property + */ +export const is_valid_css_property = ( + property: string, + properties: Set | null, +): boolean => { + // Custom properties are always valid + if (property.startsWith('--')) return true; + + // If no properties provided, skip validation + if (!properties) return true; + + return properties.has(property); +}; + +/** + * Suggests a correct property name for a typo using Levenshtein distance. + * + * @param typo - The mistyped property name + * @param properties - Set of valid CSS properties from `load_css_properties()`. + * Pass `null` to skip suggestions. + * @returns The suggested property or null if no close match (Levenshtein distance > 2) + */ +export const suggest_css_property = ( + typo: string, + properties: Set | null, +): string | null => { + if (!properties) return null; + + let best_match: string | null = null; + let best_distance = 3; // Only suggest if distance <= 2 + + for (const prop of properties) { + const distance = levenshtein_distance(typo, prop); + if (distance < best_distance) { + best_distance = distance; + best_match = prop; + } + } + + return best_match; +}; + +/** + * Suggests a correct modifier name for a typo using Levenshtein distance. + * + * @param typo - The mistyped modifier name + * @returns The suggested modifier or null if no close match (Levenshtein distance > 2) + */ +export const suggest_modifier = (typo: string): string | null => { + const all_names = get_all_modifier_names(); + let best_match: string | null = null; + let best_distance = 3; + + for (const name of all_names) { + const distance = levenshtein_distance(typo, name); + if (distance < best_distance) { + best_distance = distance; + best_match = name; + } + } + + return best_match; +}; + +// +// Value Formatting +// + +/** + * Formats a CSS-literal value for CSS output. + * - Replaces `~` with space + * - Ensures space before `!important` + * + * @param value - Raw value from class name + * @returns Formatted CSS value + */ +export const format_css_value = (value: string): string => { + let result = value.replace(/~/g, ' '); + result = result.replace(/!important$/, ' !important'); + return result; +}; + +/** + * Pattern to detect calc expressions possibly missing spaces around + or -. + * CSS requires spaces around + and - in calc(). + */ +const CALC_MISSING_SPACE_PATTERN = /calc\([^)]*\d+[%a-z]*[+-]\d/i; + +/** + * Checks if a value contains a possibly invalid calc expression. + * + * @param value - The formatted CSS value + * @returns Warning message if suspicious, null otherwise + */ +export const check_calc_expression = (value: string): string | null => { + if (CALC_MISSING_SPACE_PATTERN.test(value)) { + return `Possible invalid calc expression. CSS requires spaces around + and - in calc(). Use ~ for spaces: "calc(100%~-~20px)"`; + } + return null; +}; + +// +// Parsing +// + +/** + * Checks if a class name could be a CSS-literal class. + * Quick check before attempting full parse. + * + * @param class_name - The class name to check + * @returns True if it could be CSS-literal syntax + */ +export const is_possible_css_literal = (class_name: string): boolean => { + // Must contain at least one colon + if (!class_name.includes(':')) return false; + + // Should not match existing class patterns (underscore-based) + if (/^[a-z_]+_[a-z0-9_]+$/.test(class_name)) return false; + + // Basic structure check: at minimum "property:value" + const parts = class_name.split(':'); + if (parts.length < 2) return false; + + // First part should be non-empty (property or first modifier) + if (parts[0] === '') return false; + + // Last part should be the value, shouldn't be empty + if (parts[parts.length - 1] === '') return false; + + return true; +}; + +/** + * Extracts colon-separated segments from a class name, handling parentheses. + * Parenthesized content (like function arguments) is kept intact. + * + * @example + * extract_segments('md:hover:display:flex') → ['md', 'hover', 'display', 'flex'] + * extract_segments('nth-child(2n+1):color:red') → ['nth-child(2n+1)', 'color', 'red'] + * extract_segments('width:calc(100%-20px)') → ['width', 'calc(100%-20px)'] + */ +export const extract_segments = (class_name: string): Array => { + const segments: Array = []; + let current = ''; + let paren_depth = 0; + + for (const char of class_name) { + if (char === '(') { + paren_depth++; + current += char; + } else if (char === ')') { + paren_depth--; + current += char; + } else if (char === ':' && paren_depth === 0) { + if (current) { + segments.push(current); + current = ''; + } + } else { + current += char; + } + } + + if (current) { + segments.push(current); + } + + return segments; +}; + +/** + * Extracts and validates modifiers from the beginning of a segments array. + * Modifiers are consumed from the front until a non-modifier segment is found. + * + * Used by both CSS-literal parsing and modified class interpretation. + * + * @param segments - Array of colon-separated segments + * @param class_name - Original class name for error messages + * @returns ModifierExtractionResult with modifiers and remaining segments, or error + */ +export const extract_and_validate_modifiers = ( + segments: Array, + class_name: string, +): ModifierExtractionResult => { + let media: ModifierDefinition | null = null; + let ancestor: ModifierDefinition | null = null; + const states: Array = []; + let pseudo_element: ModifierDefinition | null = null; + + let i = 0; + for (; i < segments.length; i++) { + const segment = segments[i]!; + const modifier = get_modifier(segment); + + // If not a modifier, stop - remaining segments are the base class/property:value + if (!modifier) { + break; + } + + // Validate order based on modifier type + switch (modifier.type) { + case 'media': { + if (media) { + return { + ok: false, + error: { + level: 'error', + message: `Multiple media modifiers not allowed`, + class_name, + suggestion: null, + }, + }; + } + if (ancestor) { + return { + ok: false, + error: { + level: 'error', + message: `Media modifier must come before ancestor modifier`, + class_name, + suggestion: `Move "${segment}" before "${ancestor.name}"`, + }, + }; + } + if (states.length > 0) { + return { + ok: false, + error: { + level: 'error', + message: `Media modifier must come before state modifiers`, + class_name, + suggestion: `Move "${segment}" before "${states[0]!.name}"`, + }, + }; + } + if (pseudo_element) { + return { + ok: false, + error: { + level: 'error', + message: `Media modifier must come before pseudo-element`, + class_name, + suggestion: `Move "${segment}" before "${pseudo_element.name}"`, + }, + }; + } + media = modifier; + break; + } + + case 'ancestor': { + if (ancestor) { + return { + ok: false, + error: { + level: 'error', + message: `Modifiers "${ancestor.name}" and "${segment}" are mutually exclusive`, + class_name, + suggestion: null, + }, + }; + } + if (states.length > 0) { + return { + ok: false, + error: { + level: 'error', + message: `Ancestor modifier must come before state modifiers`, + class_name, + suggestion: `Move "${segment}" before "${states[0]!.name}"`, + }, + }; + } + if (pseudo_element) { + return { + ok: false, + error: { + level: 'error', + message: `Ancestor modifier must come before pseudo-element`, + class_name, + suggestion: `Move "${segment}" before "${pseudo_element.name}"`, + }, + }; + } + ancestor = modifier; + break; + } + + case 'state': { + if (pseudo_element) { + return { + ok: false, + error: { + level: 'error', + message: `State modifiers must come before pseudo-element`, + class_name, + suggestion: `Move "${segment}" before "${pseudo_element.name}"`, + }, + }; + } + // Check alphabetical order (full string comparison) + if (states.length > 0) { + const prev = states[states.length - 1]!; + if (segment < prev.name) { + return { + ok: false, + error: { + level: 'error', + message: `State modifiers must be in alphabetical order: "${prev.name}:${segment}" should be "${segment}:${prev.name}"`, + class_name, + suggestion: `Reorder to: ...${segment}:${prev.name}:...`, + }, + }; + } + } + states.push(modifier); + break; + } + + case 'pseudo-element': { + if (pseudo_element) { + return { + ok: false, + error: { + level: 'error', + message: `Multiple pseudo-element modifiers not allowed`, + class_name, + suggestion: null, + }, + }; + } + pseudo_element = modifier; + break; + } + } + } + + return { + ok: true, + modifiers: {media, ancestor, states, pseudo_element}, + remaining: segments.slice(i), + }; +}; + +/** + * Parses a CSS-literal class name into its components. + * + * @param class_name - The class name to parse + * @param css_properties - Set of valid CSS properties from `load_css_properties()`. + * Pass `null` to skip property validation. + * @returns CssLiteralParseResult with parsed data or error + */ +export const parse_css_literal = ( + class_name: string, + css_properties: Set | null, +): CssLiteralParseResult => { + const segments = extract_segments(class_name); + + if (segments.length < 2) { + return { + ok: false, + error: { + level: 'error', + message: `Invalid CSS-literal syntax: expected "property:value" format`, + class_name, + suggestion: null, + }, + }; + } + + // Work backwards from end to find property:value + // Everything before that is modifiers + const value = segments[segments.length - 1]!; + const property = segments[segments.length - 2]!; + const modifier_segments = segments.slice(0, -2); + + let diagnostics: Array | null = null; + + // Validate modifiers using shared validation logic + const modifier_result = extract_and_validate_modifiers(modifier_segments, class_name); + + if (!modifier_result.ok) { + return {ok: false, error: modifier_result.error}; + } + + // All segments should have been consumed as modifiers + // If any remain, they're unknown modifiers (since we already separated property:value) + if (modifier_result.remaining.length > 0) { + const unknown = modifier_result.remaining[0]!; + const suggestion = suggest_modifier(unknown); + return { + ok: false, + error: { + level: 'error', + message: `Unknown modifier "${unknown}"`, + class_name, + suggestion: suggestion ? `Did you mean "${suggestion}"?` : null, + }, + }; + } + + const {media, ancestor, states, pseudo_element} = modifier_result.modifiers; + + // Validate property + if (!is_valid_css_property(property, css_properties)) { + const suggestion = suggest_css_property(property, css_properties); + return { + ok: false, + error: { + level: 'error', + message: `Unknown CSS property "${property}"`, + class_name, + suggestion: suggestion ? `Did you mean "${suggestion}"?` : null, + }, + }; + } + + // Format value + const formatted_value = format_css_value(value); + + // Check for calc warnings + const calc_warning = check_calc_expression(formatted_value); + if (calc_warning) { + (diagnostics ??= []).push({ + level: 'warning', + message: calc_warning, + class_name, + suggestion: `Use ~ for spaces in calc expressions`, + }); + } + + return { + ok: true, + parsed: { + class_name, + media, + ancestor, + states, + pseudo_element, + property, + value: formatted_value, + }, + diagnostics, + }; +}; + +// +// CSS Generation +// + +/** + * Generates the CSS selector for a parsed CSS-literal class. + * Includes state pseudo-classes and pseudo-element in the selector. + */ +export const generate_selector = (escaped_class_name: string, parsed: ParsedCssLiteral): string => { + let selector = `.${escaped_class_name}`; + + // Add state pseudo-classes + for (const state of parsed.states) { + selector += state.css; + } + + // Add pseudo-element (must come last) + if (parsed.pseudo_element) { + selector += parsed.pseudo_element.css; + } + + return selector; +}; + +/** + * Generates the CSS declaration for a parsed CSS-literal class. + */ +export const generate_declaration = (parsed: ParsedCssLiteral): string => { + return `${parsed.property}: ${parsed.value};`; +}; + +/** + * Information needed to generate CSS output for a CSS-literal class. + */ +export interface CssLiteralOutput { + /** CSS declaration (property: value;) */ + declaration: string; + /** Full CSS selector including pseudo-classes/elements */ + selector: string; + /** Media query wrapper if any */ + media_wrapper: string | null; + /** Ancestor wrapper if any */ + ancestor_wrapper: string | null; +} + +/** + * Interprets a CSS-literal class and returns CSS generation info. + * + * Callers should first check `is_possible_css_literal()` to filter non-CSS-literal classes. + * + * @param class_name - The class name to interpret + * @param escaped_class_name - The CSS-escaped version of the class name + * @param css_properties - Set of valid CSS properties from `load_css_properties()`. + * Pass `null` to skip property validation. + * @returns Result with output and warnings on success, or error on failure + */ +export const interpret_css_literal = ( + class_name: string, + escaped_class_name: string, + css_properties: Set | null, +): InterpretCssLiteralResult => { + const result = parse_css_literal(class_name, css_properties); + + if (!result.ok) { + return {ok: false, error: result.error}; + } + + const {parsed, diagnostics} = result; + + return { + ok: true, + output: { + declaration: generate_declaration(parsed), + selector: generate_selector(escaped_class_name, parsed), + media_wrapper: parsed.media?.css ?? null, + ancestor_wrapper: parsed.ancestor?.css ?? null, + }, + warnings: diagnostics, + }; +}; + +// +// Composition Support +// + +/** + * Result of attempting to resolve a CSS literal for composition. + */ +export type LiteralResolutionResult = + | {ok: true; declaration: string; warnings: Array | null} + | {ok: false; error: InterpreterDiagnostic | null}; + +/** + * Checks if a parsed CSS literal has any modifiers. + */ +export const has_modifiers = (parsed: ParsedCssLiteral): boolean => { + return !!(parsed.media || parsed.ancestor || parsed.states.length > 0 || parsed.pseudo_element); +}; + +/** + * Checks if extracted modifiers contain any modifier. + * Useful for tooling that needs to detect modified classes. + */ +export const has_extracted_modifiers = (modifiers: ExtractedModifiers): boolean => { + return !!( + modifiers.media || + modifiers.ancestor || + modifiers.states.length > 0 || + modifiers.pseudo_element + ); +}; + +/** + * Attempts to resolve a class name as an unmodified CSS literal for composition. + * + * Used by `resolve_composes` to support literals in `composes` arrays. + * Returns the declaration if successful, null if not a literal, or an error + * if it's a literal with issues (modifiers, invalid property, etc.). + * + * @param class_name - The class name to try resolving + * @param css_properties - Set of valid CSS properties, or null to skip validation + * @param context_class_name - The class being defined (for error messages) + * @returns Resolution result with declaration, or error, or null if not a literal + */ +export const try_resolve_literal = ( + class_name: string, + css_properties: Set | null, + context_class_name: string, +): LiteralResolutionResult => { + // Quick check - must look like a CSS literal + if (!is_possible_css_literal(class_name)) { + return {ok: false, error: null}; + } + + // Parse the literal + const result = parse_css_literal(class_name, css_properties); + + if (!result.ok) { + return {ok: false, error: contextualize_error(result.error, context_class_name)}; + } + + const {parsed, diagnostics} = result; + + // Check for modifiers - modified literals cannot be composed + if (has_modifiers(parsed)) { + return { + ok: false, + error: { + level: 'error', + class_name: context_class_name, + message: `Modified class "${class_name}" cannot be used in composes array`, + suggestion: 'Apply modified classes directly in markup, not in composes arrays', + }, + }; + } + + // Success - return the declaration + return { + ok: true, + declaration: generate_declaration(parsed), + warnings: diagnostics, + }; +}; + +/** + * Updates error to use the context class name (the class being defined). + */ +const contextualize_error = ( + error: InterpreterDiagnostic, + context_class_name: string, +): InterpreterDiagnostic => ({ + ...error, + class_name: context_class_name, +}); + +/** + * Generates simple CSS for a CSS-literal class (without grouping). + * Used by the interpreter for basic output. + * + * @param output - The CSS-literal output info + * @returns CSS string for this class + */ +export const generate_css_literal_simple = (output: CssLiteralOutput): string => { + let css = ''; + let indent = ''; + + // Open media wrapper if present + if (output.media_wrapper) { + css += `${output.media_wrapper} {\n`; + indent = '\t'; + } + + // Open ancestor wrapper if present + if (output.ancestor_wrapper) { + css += `${indent}${output.ancestor_wrapper} {\n`; + indent += '\t'; + } + + // Write the rule + css += `${indent}${output.selector} { ${output.declaration} }\n`; + + // Close ancestor wrapper + if (output.ancestor_wrapper) { + indent = indent.slice(0, -1); + css += `${indent}}\n`; + } + + // Close media wrapper + if (output.media_wrapper) { + css += '}\n'; + } + + return css; +}; diff --git a/src/lib/css_ruleset_parser.ts b/src/lib/css_ruleset_parser.ts new file mode 100644 index 000000000..a620176e2 --- /dev/null +++ b/src/lib/css_ruleset_parser.ts @@ -0,0 +1,638 @@ +/** + * CSS ruleset parser using Svelte's CSS parser. + * + * Parses CSS rulesets to extract selectors and declarations with position information. + * Used for: + * - Phase 0a: Detecting single-selector rulesets that could be converted to declaration format + * - Phase 2: Modifying selectors for modifier support on composite classes + * + * NOTE: We wrap CSS in a `'; + +/** + * Parses a CSS ruleset string using Svelte's CSS parser. + * + * @param css - Raw CSS string (e.g., ".box { display: flex; }") + * @returns ParsedRuleset with structured rule data and positions + */ +export const parse_ruleset = (css: string): ParsedRuleset => { + const wrapper_offset = STYLE_WRAPPER_PREFIX.length; + const wrapped = `${STYLE_WRAPPER_PREFIX}${css}${STYLE_WRAPPER_SUFFIX}`; + + const ast = parse(wrapped, {modern: true}); + const rules: Array = []; + + if (!ast.css) { + return {rules, wrapper_offset}; + } + + // Walk the CSS AST to find rules + walk_css_children(ast.css, css, wrapper_offset, rules); + + return {rules, wrapper_offset}; +}; + +/** + * Walks CSS AST children to extract rules. + * @mutates rules - Pushes extracted rules to the array + */ +const walk_css_children = ( + node: AST.CSS.StyleSheet | AST.CSS.Atrule, + original_css: string, + wrapper_offset: number, + rules: Array, +): void => { + const children = 'children' in node ? node.children : []; + + for (const child of children) { + if (child.type === 'Rule') { + extract_rule(child, original_css, wrapper_offset, rules); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if (child.type === 'Atrule' && child.block) { + // Recurse into at-rules (like @media) - rules are in block.children + walk_css_block(child.block, original_css, wrapper_offset, rules); + } + } +}; + +/** + * Walks a CSS block (from an at-rule) to extract rules. + * @mutates rules - Pushes extracted rules to the array + */ +const walk_css_block = ( + block: AST.CSS.Block, + original_css: string, + wrapper_offset: number, + rules: Array, +): void => { + for (const child of block.children) { + if (child.type === 'Rule') { + extract_rule(child, original_css, wrapper_offset, rules); + } else if (child.type === 'Atrule' && child.block) { + // Handle nested at-rules + walk_css_block(child.block, original_css, wrapper_offset, rules); + } + } +}; + +/** + * Extracts a single rule from the AST. + * @mutates rules - Pushes the extracted rule to the array + */ +const extract_rule = ( + rule: AST.CSS.Rule, + original_css: string, + wrapper_offset: number, + rules: Array, +): void => { + const prelude = rule.prelude; + + // Get the full selector text from the original CSS + const selector_start = prelude.start - wrapper_offset; + const selector_end = prelude.end - wrapper_offset; + const selector = original_css.slice(selector_start, selector_end); + + // Get declarations from the block + const block = rule.block; + const block_start = block.start - wrapper_offset; + const block_end = block.end - wrapper_offset; + + // Extract just the declarations (content between braces) + const block_content = original_css.slice(block_start, block_end); + const declarations = block_content.slice(1, -1).trim(); // Remove { } and trim + + rules.push({ + selector, + selector_start, + selector_end, + declarations, + rule_start: rule.start - wrapper_offset, + rule_end: rule.end - wrapper_offset, + }); +}; + +/** + * Checks if a ruleset has only a single simple selector (just the class name). + * Used to detect rulesets that could be converted to declaration format. + * + * @param rules - Parsed rules from the ruleset + * @param escaped_class_name - The CSS-escaped class name (e.g., "box" or "hover\\:card") + * @returns True if there's exactly one rule with selector ".class_name" + */ +export const is_single_selector_ruleset = ( + rules: Array, + escaped_class_name: string, +): boolean => { + if (rules.length !== 1) return false; + + const rule = rules[0]!; + const expected_selector = `.${escaped_class_name}`; + + // Normalize whitespace in selector for comparison + const normalized_selector = rule.selector.trim(); + + return normalized_selector === expected_selector; +}; + +/** + * Checks if any selector in the ruleset contains the expected class name. + * Used to validate that ruleset definitions match their key. + * + * @param rules - Parsed rules from the ruleset + * @param escaped_class_name - The CSS-escaped class name (e.g., "clickable" or "hover\\:card") + * @returns True if at least one selector contains ".class_name" + */ +export const ruleset_contains_class = ( + rules: Array, + escaped_class_name: string, +): boolean => { + // Match .class_name but not as part of another class (e.g., .class_name_foo) + const pattern = new RegExp(`\\.${escape_regexp(escaped_class_name)}(?![\\w-])`); + return rules.some((rule) => pattern.test(rule.selector)); +}; + +/** + * Extracts the CSS comment from a ruleset (if any). + * Looks for comments before the first rule. + * + * @param css - Raw CSS string + * @param rules - Parsed rules + * @returns Comment text without delimiters, or null if no comment + */ +export const extract_css_comment = (css: string, rules: Array): string | null => { + if (rules.length === 0) return null; + + const first_rule_start = rules[0]!.rule_start; + const before_rule = css.slice(0, first_rule_start).trim(); + + // Check for /* */ comment + const comment_pattern = /\/\*\s*([\s\S]*?)\s*\*\//; + const comment_match = comment_pattern.exec(before_rule); + if (comment_match) { + return comment_match[1]!.trim(); + } + + return null; +}; + +// +// Selector Modification +// + +/** + * Information about a modifier that was skipped for a selector during ruleset modification. + * The selector is still included in output, just without the conflicting modifier applied. + */ +export interface SkippedModifierInfo { + /** The specific selector where the modifier was skipped (not the full selector list) */ + selector: string; + /** Reason the modifier was skipped */ + reason: 'pseudo_element_conflict' | 'state_conflict'; + /** The conflicting modifier that was not applied (e.g., "::before" or ":hover") */ + conflicting_modifier: string; +} + +/** + * Result from modifying a selector group with conflict detection. + * + * Uses `| null` for skipped_modifiers to avoid allocating empty arrays. + * Callers should use a guard pattern: `if (result.skipped_modifiers) { ... }` + */ +export interface ModifiedSelectorGroupResult { + /** The modified selector list as a string */ + selector: string; + /** Information about modifiers skipped for specific selectors, or null if none */ + skipped_modifiers: Array | null; +} + +/** + * Result from generating a modified ruleset. + * + * Uses `| null` for skipped_modifiers to avoid allocating empty arrays. + * Callers should use a guard pattern: `if (result.skipped_modifiers) { ... }` + */ +export interface ModifiedRulesetResult { + /** The generated CSS */ + css: string; + /** Information about modifiers that were skipped for certain rules, or null if none */ + skipped_modifiers: Array | null; +} + +/** + * Skips an identifier (class name, pseudo-class name, etc.) starting at `start`. + * + * @returns New position after the identifier + */ +const skip_identifier = (selector: string, start: number): number => { + let pos = start; + while (pos < selector.length && /[\w-]/.test(selector[pos]!)) { + pos++; + } + return pos; +}; + +/** + * CSS2 pseudo-elements that use single-colon syntax. + * CSS3 uses double-colon (::before) but CSS2 syntax (:before) is still valid. + */ +const CSS2_PSEUDO_ELEMENTS = /:(before|after|first-letter|first-line)(?![\w-])/; + +/** + * Checks if a selector contains a pseudo-element (::before, ::after, etc.). + * Also detects CSS2 single-colon syntax (:before, :after, :first-letter, :first-line). + */ +const selector_has_pseudo_element = (selector: string): boolean => { + return selector.includes('::') || CSS2_PSEUDO_ELEMENTS.test(selector); +}; + +/** + * Checks if a selector already contains a specific state pseudo-class. + * Handles functional pseudo-classes like :not(:hover) by checking for the state + * both as a direct pseudo-class and within functional pseudo-classes. + * + * @param selector - The CSS selector to check + * @param state - The state to look for (e.g., ":hover", ":focus") + * @returns True if the selector already contains this state + */ +const selector_has_state = (selector: string, state: string): boolean => { + // Simple check: does the selector contain the state string? + // This catches both direct usage (.foo:hover) and within functional pseudo-classes (.foo:not(:hover)) + return selector.includes(state); +}; + +/** + * Splits a selector list by commas, respecting parentheses, brackets, and quoted strings. + * + * @example + * split_selector_list('.a, .b') → ['.a', '.b'] + * split_selector_list('.a:not(.b), .c') → ['.a:not(.b)', '.c'] + * split_selector_list('.a[data-x="a,b"], .c') → ['.a[data-x="a,b"]', '.c'] + */ +export const split_selector_list = (selector_group: string): Array => { + const selectors: Array = []; + let current = ''; + let paren_depth = 0; + let bracket_depth = 0; + let in_string: '"' | "'" | null = null; + + for (let i = 0; i < selector_group.length; i++) { + const char = selector_group[i]!; + const prev_char = i > 0 ? selector_group[i - 1] : ''; + + // Handle string boundaries (but not escaped quotes) + if ((char === '"' || char === "'") && prev_char !== '\\') { + if (in_string === null) { + in_string = char; + } else if (in_string === char) { + in_string = null; + } + current += char; + } else if (in_string !== null) { + // Inside a string - just accumulate + current += char; + } else if (char === '(') { + paren_depth++; + current += char; + } else if (char === ')') { + paren_depth--; + current += char; + } else if (char === '[') { + bracket_depth++; + current += char; + } else if (char === ']') { + bracket_depth--; + current += char; + } else if (char === ',' && paren_depth === 0 && bracket_depth === 0) { + selectors.push(current.trim()); + current = ''; + } else { + current += char; + } + } + + if (current.trim()) { + selectors.push(current.trim()); + } + + return selectors; +}; + +/** + * Finds the end position of the compound selector containing the class at class_pos. + * A compound selector is a sequence of simple selectors without combinators. + * + * @param selector - The CSS selector string + * @param class_pos - Position of the `.` in `.class_name` + * @returns Position where state modifiers should be inserted (before any pseudo-element) + */ +export const find_compound_end = (selector: string, class_pos: number): number => { + let pos = class_pos + 1; // Skip the dot + pos = skip_identifier(selector, pos); // Skip the class name + + // Continue scanning while we see parts of the same compound selector + while (pos < selector.length) { + const char = selector[pos]!; + + if (char === '.') { + // Another class - skip it + pos++; + pos = skip_identifier(selector, pos); + } else if (char === '#') { + // ID selector - skip it + pos++; + pos = skip_identifier(selector, pos); + } else if (char === '[') { + // Attribute selector - find matching ], respecting quoted strings + pos++; // Skip [ + let in_string: '"' | "'" | null = null; + while (pos < selector.length) { + const c = selector[pos]!; + const prev = pos > 0 ? selector[pos - 1] : ''; + if ((c === '"' || c === "'") && prev !== '\\') { + if (in_string === null) { + in_string = c; + } else if (in_string === c) { + in_string = null; + } + } else if (c === ']' && in_string === null) { + pos++; // Skip ] + break; + } + pos++; + } + } else if (char === ':') { + // Pseudo-class or pseudo-element + if (selector[pos + 1] === ':') { + // CSS3 pseudo-element (::before) - stop here (state comes before pseudo-element) + break; + } + // Check for CSS2 pseudo-elements (:before, :after, :first-letter, :first-line) + const rest = selector.slice(pos); + if (CSS2_PSEUDO_ELEMENTS.test(rest)) { + // CSS2 pseudo-element - stop here + break; + } + // Pseudo-class - skip it (including functional ones like :not()) + pos++; // Skip : + pos = skip_identifier(selector, pos); + if (pos < selector.length && selector[pos] === '(') { + // Functional pseudo-class - find matching ) + let paren_depth = 1; + pos++; + while (pos < selector.length && paren_depth > 0) { + if (selector[pos] === '(') paren_depth++; + else if (selector[pos] === ')') paren_depth--; + pos++; + } + } + } else { + // Hit whitespace, combinator, or something else - compound ends here + break; + } + } + + return pos; +}; + +/** + * Modifies a single CSS selector to add modifiers. + * + * @param selector - A single CSS selector (not a selector list) + * @param original_class - The base class name (e.g., "menu_item") + * @param new_class_escaped - The escaped new class name (e.g., "hover\\:menu_item") + * @param state_css - State modifier CSS to insert (e.g., ":hover") + * @param pseudo_element_css - Pseudo-element modifier CSS to insert (e.g., "::before") + * @returns Modified selector + * + * @example + * modify_single_selector('.menu_item', 'menu_item', 'hover\\:menu_item', ':hover', '') + * // → '.hover\\:menu_item:hover' + * + * modify_single_selector('.menu_item .icon', 'menu_item', 'hover\\:menu_item', ':hover', '') + * // → '.hover\\:menu_item:hover .icon' + * + * modify_single_selector('.menu_item.selected', 'menu_item', 'hover\\:menu_item', ':hover', '') + * // → '.hover\\:menu_item.selected:hover' + */ +export const modify_single_selector = ( + selector: string, + original_class: string, + new_class_escaped: string, + state_css: string, + pseudo_element_css: string, +): string => { + // Find the target class (must match the class name exactly, not as part of another class) + const class_pattern = new RegExp(`\\.${escape_regexp(original_class)}(?![\\w-])`); + const match = class_pattern.exec(selector); + if (!match) return selector; // Class not in this selector + + const class_pos = match.index; + + // Find where the compound selector ends (where to insert state) + const compound_end = find_compound_end(selector, class_pos); + + // Build the modified selector + // Insert state and pseudo-element at compound_end (before any existing pseudo-element) + const suffix = state_css + pseudo_element_css; + let result = selector.slice(0, compound_end) + suffix + selector.slice(compound_end); + + // Replace the class name with the new escaped name + result = result.replace(class_pattern, `.${new_class_escaped}`); + + return result; +}; + +/** + * Modifies a selector list (comma-separated selectors) to add modifiers. + * Handles conflicts per-selector: if one selector in a list has a conflict, + * only that selector skips the modifier; other selectors still get it. + * + * @param selector_group - CSS selector list (may contain commas) + * @param original_class - The base class name + * @param new_class_escaped - The escaped new class name + * @param states_to_add - Individual state modifiers (e.g., [":hover", ":focus"]) + * @param pseudo_element_css - Pseudo-element modifier CSS to insert (e.g., "::before") + * @returns Result with modified selector list and information about skipped modifiers + */ +export const modify_selector_group = ( + selector_group: string, + original_class: string, + new_class_escaped: string, + states_to_add: Array, + pseudo_element_css: string, +): ModifiedSelectorGroupResult => { + const selectors = split_selector_list(selector_group); + let skipped_modifiers: Array | null = null; + const adding_pseudo_element = pseudo_element_css !== ''; + + const modified_selectors = selectors.map((selector) => { + const trimmed = selector.trim(); + + // Check pseudo-element conflict for this specific selector + const has_pseudo_element_conflict = + adding_pseudo_element && selector_has_pseudo_element(trimmed); + + // Check state conflicts for this specific selector - filter to non-conflicting states + const non_conflicting_states: Array = []; + for (const state of states_to_add) { + if (selector_has_state(trimmed, state)) { + // Record the skip for this specific selector + (skipped_modifiers ??= []).push({ + selector: trimmed, + reason: 'state_conflict', + conflicting_modifier: state, + }); + } else { + non_conflicting_states.push(state); + } + } + + // Record pseudo-element skip for this specific selector + if (has_pseudo_element_conflict) { + (skipped_modifiers ??= []).push({ + selector: trimmed, + reason: 'pseudo_element_conflict', + conflicting_modifier: pseudo_element_css, + }); + } + + // Build effective modifiers for this selector + const effective_state_css = non_conflicting_states.join(''); + const effective_pseudo_element_css = has_pseudo_element_conflict ? '' : pseudo_element_css; + + return modify_single_selector( + trimmed, + original_class, + new_class_escaped, + effective_state_css, + effective_pseudo_element_css, + ); + }); + + return { + selector: modified_selectors.join(',\n'), + skipped_modifiers, + }; +}; + +/** + * Generates CSS for a modified ruleset with applied modifiers. + * + * Conflict handling is per-selector within selector lists: + * - For `.plain:hover, .plain:active` with `hover:` modifier, only `.plain:hover` skips + * the `:hover` addition; `.plain:active` still gets `:hover` appended. + * - For multiple states like `:hover:focus`, each is checked individually; conflicting + * states are skipped while non-conflicting ones are still applied. + * + * @param original_ruleset - The original CSS ruleset string + * @param original_class - The base class name + * @param new_class_escaped - The escaped new class name with modifiers + * @param state_css - State modifier CSS (e.g., ":hover" or ":hover:focus") + * @param pseudo_element_css - Pseudo-element modifier CSS (e.g., "::before") + * @param media_wrapper - Media query wrapper (e.g., "@media (width >= 48rem)") + * @param ancestor_wrapper - Ancestor wrapper (e.g., ":root.dark") + * @returns Result with generated CSS and information about skipped modifiers + */ +export const generate_modified_ruleset = ( + original_ruleset: string, + original_class: string, + new_class_escaped: string, + state_css: string, + pseudo_element_css: string, + media_wrapper: string | null, + ancestor_wrapper: string | null, +): ModifiedRulesetResult => { + const parsed = parse_ruleset(original_ruleset); + let skipped_modifiers: Array | null = null; + + // Extract individual states for per-selector conflict detection (e.g., ":hover:focus" → [":hover", ":focus"]) + const states_to_add = state_css.match(/:[a-z-]+/g) ?? []; + + let css = ''; + let indent = ''; + + // Open media wrapper if present + if (media_wrapper) { + css += `${media_wrapper} {\n`; + indent = '\t'; + } + + // Open ancestor wrapper if present + if (ancestor_wrapper) { + css += `${indent}${ancestor_wrapper} {\n`; + indent += '\t'; + } + + // Generate each rule with modified selector (conflict detection happens per-selector in modify_selector_group) + for (const rule of parsed.rules) { + const result = modify_selector_group( + rule.selector, + original_class, + new_class_escaped, + states_to_add, + pseudo_element_css, + ); + + // Collect skip info from per-selector conflict detection + if (result.skipped_modifiers) { + (skipped_modifiers ??= []).push(...result.skipped_modifiers); + } + + css += `${indent}${result.selector} { ${rule.declarations} }\n`; + } + + // Close ancestor wrapper + if (ancestor_wrapper) { + indent = indent.slice(0, -1); + css += `${indent}}\n`; + } + + // Close media wrapper + if (media_wrapper) { + css += '}\n'; + } + + return {css, skipped_modifiers}; +}; diff --git a/src/lib/diagnostics.ts b/src/lib/diagnostics.ts new file mode 100644 index 000000000..37ca9a780 --- /dev/null +++ b/src/lib/diagnostics.ts @@ -0,0 +1,139 @@ +/** + * Diagnostic types for CSS class extraction and generation. + * + * Provides a unified diagnostic system across all phases: + * - Extraction: Parsing source files to find class names + * - Generation: Producing CSS output from class definitions + * + * @module + */ + +// +// Source Location +// + +/** + * Source location for IDE/LSP integration. + */ +export interface SourceLocation { + file: string; + /** 1-based line number */ + line: number; + /** 1-based column number */ + column: number; +} + +// +// Diagnostic Types +// + +/** + * Base diagnostic with common fields. + */ +export interface BaseDiagnostic { + level: 'error' | 'warning'; + message: string; + suggestion: string | null; +} + +/** + * Diagnostic from the extraction phase. + */ +export interface ExtractionDiagnostic extends BaseDiagnostic { + phase: 'extraction'; + location: SourceLocation; +} + +/** + * Diagnostic from the generation phase. + */ +export interface GenerationDiagnostic { + phase: 'generation'; + level: 'error' | 'warning'; + message: string; + suggestion: string | null; + class_name: string; + /** Source locations where this class was used, or null if from include_classes */ + locations: Array | null; +} + +/** + * Union of all diagnostic types. + */ +export type Diagnostic = ExtractionDiagnostic | GenerationDiagnostic; + +/** + * Diagnostic from CSS class interpretation. + * Used internally by interpreters; converted to GenerationDiagnostic with locations. + */ +export interface InterpreterDiagnostic { + level: 'error' | 'warning'; + message: string; + class_name: string; + suggestion: string | null; +} + +// +// Diagnostic Utilities +// + +/** + * Converts a InterpreterDiagnostic to a GenerationDiagnostic with locations. + * + * @param diagnostic - Interpreter diagnostic to convert + * @param locations - Source locations where the class was used + */ +export const create_generation_diagnostic = ( + diagnostic: InterpreterDiagnostic, + locations: Array | null, +): GenerationDiagnostic => ({ + phase: 'generation', + level: diagnostic.level, + message: diagnostic.message, + class_name: diagnostic.class_name, + suggestion: diagnostic.suggestion ?? null, + locations, +}); + +/** + * Formats a diagnostic for display. + */ +export const format_diagnostic = (d: Diagnostic): string => { + const suggestion = d.suggestion ? ` (${d.suggestion})` : ''; + if (d.phase === 'extraction') { + return ` - ${d.location.file}:${d.location.line}:${d.location.column}: ${d.message}${suggestion}`; + } + const loc = d.locations?.[0]; + const location_str = loc ? `${loc.file}:${loc.line}:${loc.column}: ` : ''; + return ` - ${location_str}${d.class_name}: ${d.message}${suggestion}`; +}; + +/** + * Error thrown when CSS generation encounters errors or warnings + * (depending on `on_error` and `on_warning` settings). + * Contains the full diagnostics array for programmatic access. + */ +export class CssGenerationError extends Error { + diagnostics: Array; + + constructor(diagnostics: Array) { + const errors = diagnostics.filter((d) => d.level === 'error'); + const warnings = diagnostics.filter((d) => d.level === 'warning'); + + const parts: Array = []; + if (errors.length > 0) { + parts.push(`${errors.length} error${errors.length === 1 ? '' : 's'}`); + } + if (warnings.length > 0) { + parts.push(`${warnings.length} warning${warnings.length === 1 ? '' : 's'}`); + } + + const summary = parts.length > 0 ? parts.join(' and ') : '0 issues'; + const formatted = diagnostics.map(format_diagnostic).join('\n'); + const message = `CSS generation failed with ${summary}:\n${formatted}`; + + super(message); + this.name = 'CssGenerationError'; + this.diagnostics = diagnostics; + } +} diff --git a/src/lib/example_class_utilities.ts b/src/lib/example_class_utilities.ts new file mode 100644 index 000000000..e73dc258b --- /dev/null +++ b/src/lib/example_class_utilities.ts @@ -0,0 +1,93 @@ +/** + * Example CSS class exports demonstrating node_modules extraction. + * + * This module exists to verify that the Vite plugin extracts classes from + * dependencies in node_modules. The exports test specific extraction patterns + * that require being in a separate module. + * + * **Important:** Variable names must match `CLASS_NAME_PATTERN` in css_class_extractor.ts. + * Supported suffixes: `class`, `classes`, `className`, `classNames`, `classList`, `classLists` + * (also snake_case variants like `class_name`, `class_names`, `class_list`, `class_lists`). + * + * **Patterns demonstrated:** + * - Naming patterns: All CLASS_NAME_PATTERN suffix variants + * - Expression patterns: Ternary, logical AND, arrays + * - Comment hints: `@fuz-classes` directive + * + * Token, composite, and literal classes are demonstrated inline in the examples + * since they don't require special extraction testing. + * + * @module + */ + +/* eslint-disable no-constant-condition, @typescript-eslint/no-unnecessary-condition, no-constant-binary-expression */ + +// +// Naming patterns - all CLASS_NAME_PATTERN suffix variants (mb_* + ml_* for plurals) +// + +/** `*Class` suffix (camelCase) */ +export const demoClass = 'mb_xs5'; + +/** `*_class` suffix (snake_case) */ +export const demo_class = 'mb_xs4'; + +/** `SCREAMING_SNAKE_CASE` naming */ +export const DEMO_CLASS = 'mb_xs3'; + +/** `*Classes` suffix (camelCase) - plural, multiple classes */ +export const demoClasses = 'mb_xs2 ml_xs'; + +/** `*_classes` suffix (snake_case) - plural, multiple classes */ +export const demo_classes = 'mb_xs ml_sm'; + +/** `*ClassName` suffix (camelCase) */ +export const demoClassName = 'mb_sm'; + +/** `*class_name` suffix (snake_case) */ +export const demo_class_name = 'mb_md'; + +/** `*ClassNames` suffix (camelCase) - plural, multiple classes */ +export const demoClassNames = 'mb_lg ml_md'; + +/** `*class_names` suffix (snake_case) - plural, multiple classes */ +export const demo_class_names = 'mb_xl ml_lg'; + +/** `*ClassList` suffix (camelCase) */ +export const demoClassList = 'mb_xl2'; + +/** `*class_list` suffix (snake_case) */ +export const demo_class_list = 'mb_xl3'; + +/** `*ClassLists` suffix (camelCase) - plural, multiple classes */ +export const demoClassLists = 'mb_xl4 ml_xl'; + +/** `*class_lists` suffix (snake_case) - plural, multiple classes */ +export const demo_class_lists = 'mb_xl5 ml_xl2'; + +// +// Expression patterns - ternary, logical AND, array (mt_* incrementing) +// + +/** Ternary expression - both branches extracted */ +export const ternaryClass = true ? 'mt_xs' : 'mt_sm'; + +/** Logical AND - truthy value extracted */ +export const logicalClass = true && 'mt_md'; + +/** Array - all elements extracted */ +export const arrayClasses = ['mt_lg', 'mt_xl']; + +/** Object - keys extracted as class names */ +export const objectClasses = {mt_xl2: 'mt_xl2', mt_xl3: 'mt_xl3'}; + +// +// Comment hint examples - @fuz-classes directive +// + +// @fuz-classes shadow_lg +/** + * Comment hint - extracted via @fuz-classes, NOT via variable name pattern. + * Variable name `fromComment` doesn't end in class/classes/className. + */ +export const fromComment = 'shadow_lg'; diff --git a/src/lib/file_filter.ts b/src/lib/file_filter.ts new file mode 100644 index 000000000..1d6bfe07a --- /dev/null +++ b/src/lib/file_filter.ts @@ -0,0 +1,35 @@ +/** + * File filtering utilities for CSS class extraction. + * + * @module + */ + +/** + * Filter function to determine which files to process for CSS class extraction. + */ +export type FileFilter = (path: string) => boolean; + +/** + * Default file filter for CSS class extraction. + * Includes .svelte, .html, .ts, .js, .tsx, .jsx files. + * Excludes test files and generated files. + */ +export const filter_file_default: FileFilter = (path) => { + if ( + path.includes('.test.') || + path.includes('/test/') || + path.includes('/tests/') || + path.includes('.gen.') + ) { + return false; + } + const ext = path.slice(path.lastIndexOf('.')); + return ( + ext === '.svelte' || + ext === '.html' || + ext === '.ts' || + ext === '.js' || + ext === '.tsx' || + ext === '.jsx' + ); +}; diff --git a/src/lib/gen_fuz_css.ts b/src/lib/gen_fuz_css.ts index 5033e2dcb..189b13a32 100644 --- a/src/lib/gen_fuz_css.ts +++ b/src/lib/gen_fuz_css.ts @@ -1,57 +1,233 @@ +/** + * Gro generator for creating optimized utility CSS from extracted class names. + * Scans source files, extracts CSS classes with AST-based parsing, and generates + * only the CSS for classes actually used. Includes per-file caching with content + * hash validation for fast incremental rebuilds. + * + * @module + */ + +import {join} from 'node:path'; import type {Gen} from '@ryanatkn/gro/gen.js'; -import type {FileFilter} from '@fuzdev/fuz_util/path.js'; +import {map_concurrent, each_concurrent} from '@fuzdev/fuz_util/async.js'; +import {type FileFilter, filter_file_default} from './file_filter.js'; +import {extract_css_classes_with_locations, type AcornPlugin} from './css_class_extractor.js'; +import { + type SourceLocation, + type ExtractionDiagnostic, + type Diagnostic, + format_diagnostic, + CssGenerationError, +} from './diagnostics.js'; +import {CssClasses} from './css_classes.js'; import { - collect_css_classes, - CssClasses, generate_classes_css, - type CssClassDeclaration, - type CssClassDeclarationInterpreter, -} from './css_class_helpers.js'; -import {css_classes_by_name} from './css_classes.js'; + type CssClassDefinition, + type CssClassDefinitionInterpreter, +} from './css_class_generation.js'; +import {css_class_definitions} from './css_class_definitions.js'; import {css_class_interpreters} from './css_class_interpreters.js'; +import {load_css_properties} from './css_literal.js'; +import { + DEFAULT_CACHE_DIR, + get_cache_path, + load_cached_extraction, + save_cached_extraction, + delete_cached_extraction, + from_cached_extraction, + compute_hash, +} from './css_cache.js'; + +/** + * Skip cache on CI (no point writing cache that won't be reused). + * Handles CI=1, CI=true, and other truthy values. + */ +const is_ci = !!process.env.CI; + +/** + * Default concurrency for main loop: cache read + extract. + * This is NOT true CPU parallelism - Node.js JS is single-threaded. + * The value controls I/O interleaving (overlapping cache reads with parsing) + * and memory budget for in-flight operations. Higher values offer diminishing + * returns since AST parsing is synchronous on the main thread. + */ +const DEFAULT_CONCURRENCY = 8; + +/** + * Default concurrency for cache writes/deletes (I/O-bound). + * Safe to set high since Node's libuv thread pool (default 4 threads) + * limits actual parallel I/O operations. Memory pressure from buffered + * writes is the main constraint, but cache entries are small JSON files. + */ +const DEFAULT_CACHE_IO_CONCURRENCY = 50; + +/** + * Computes cache path for a file. + * Internal files use relative paths mirroring source tree. + * External files (outside project root) use hashed absolute paths in `_external/`. + */ +const get_file_cache_path = async ( + file_id: string, + cache_dir: string, + project_root: string, +): Promise => { + const is_internal = file_id.startsWith(project_root); + return is_internal + ? get_cache_path(file_id, cache_dir, project_root) + : join(cache_dir, '_external', (await compute_hash(file_id)).slice(0, 16) + '.json'); +}; + +/** + * Result from extracting CSS classes from a single file. + * Used internally during parallel extraction with caching. + * Uses `null` instead of empty collections to avoid allocation overhead. + */ +interface FileExtraction { + id: string; + /** Extracted classes, or null if none */ + classes: Map> | null; + /** Classes from @fuz-classes comments, or null if none */ + explicit_classes: Set | null; + /** Extraction diagnostics, or null if none */ + diagnostics: Array | null; + /** Cache path to write to, or null if no write needed (cache hit or CI) */ + cache_path: string | null; + content_hash: string; +} export interface GenFuzCssOptions { - filter_file?: FileFilter | null; + filter_file?: FileFilter; include_stats?: boolean; - classes_by_name?: Record; - class_interpreters?: Array; + /** + * Additional class definitions to merge with defaults. + * User definitions take precedence over defaults with the same name. + * Required when `include_default_definitions` is `false`. + */ + class_definitions?: Record; + /** + * Whether to include default class definitions (token and composite classes). + * When `false`, `class_definitions` is required. + * @default true + */ + include_default_definitions?: boolean; + /** + * Custom interpreters for dynamic class generation. + * Replaces the builtin interpreters entirely if provided. + */ + class_interpreters?: Array; + /** + * How to handle CSS-literal errors during generation. + * - 'log': Log errors, skip invalid classes, continue + * - 'throw': Throw on first error, fail the build + * @default 'throw' in CI, 'log' otherwise + */ + on_error?: 'log' | 'throw'; + /** + * How to handle warnings during generation. + * - 'log': Log warnings, continue + * - 'throw': Throw on first warning, fail the build + * - 'ignore': Suppress warnings entirely + * @default 'log' + */ + on_warning?: 'log' | 'throw' | 'ignore'; + /** + * Classes to always include in the output, regardless of whether they're detected in source files. + * Useful for dynamically generated class names that can't be statically extracted. + */ + include_classes?: Iterable; + /** + * Classes to exclude from the output, even if they're detected in source files. + * Useful for filtering out false positives from extraction. + */ + exclude_classes?: Iterable; + /** + * Cache directory relative to project_root. + * @default DEFAULT_CACHE_DIR + */ + cache_dir?: string; + /** + * Project root directory. Source paths must be under this directory. + * @default process.cwd() + */ + project_root?: string; + /** + * Max concurrent file processing (cache read + extract). + * Bottlenecked by CPU-bound AST parsing. + * @default 8 + */ + concurrency?: number; + /** + * Max concurrent cache writes and deletes (I/O-bound). + * @default 50 + */ + cache_io_concurrency?: number; + /** + * Additional acorn plugins to use when parsing TS/JS files. + * Useful for adding JSX support via `acorn-jsx` for React projects. + * + * @example + * ```ts + * import jsx from 'acorn-jsx'; + * export const gen = gen_fuz_css({ + * acorn_plugins: [jsx()], + * }); + * ``` + */ + acorn_plugins?: Array; } -const filter_file_default: FileFilter = (path) => { - if (path.includes('.test.') || path.includes('/test/') || path.includes('.gen.')) { - return false; - } - const ext = path.slice(path.lastIndexOf('.')); - return ext === '.svelte' || ext === '.ts' || ext === '.js'; -}; - export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => { const { filter_file = filter_file_default, include_stats = false, - classes_by_name = css_classes_by_name, + class_definitions: user_class_definitions, + include_default_definitions = true, class_interpreters = css_class_interpreters, + on_error = is_ci ? 'throw' : 'log', + on_warning = 'log', + include_classes, + exclude_classes, + cache_dir = DEFAULT_CACHE_DIR, + project_root: project_root_option, + concurrency = DEFAULT_CONCURRENCY, + cache_io_concurrency = DEFAULT_CACHE_IO_CONCURRENCY, + acorn_plugins, } = options; + // Convert to Sets for efficient lookup + const include_set = include_classes ? new Set(include_classes) : null; + const exclude_set = exclude_classes ? new Set(exclude_classes) : null; + + // Instance-level state for watch mode cleanup + let previous_paths: Set | null = null; + return { - dependencies: 'all', - // TODO optimize, do we need to handle deleted files or removed classes though? - // This isn't as much a problem in watch mode but isn't clean. - // dependencies: ({changed_file_id, filer}) => { - // if (!changed_file_id) return 'all'; - // const disknode = filer.get_by_id(changed_file_id); - // if (disknode?.contents && collect_css_classes(disknode.contents).size) { - // return 'all'; - // } - // return null; - // }, + // Filter dependencies to skip non-extractable files. + // Returns 'all' when an extractable file changes, null otherwise. + dependencies: ({changed_file_id}) => { + if (!changed_file_id) return 'all'; + if (filter_file(changed_file_id)) return 'all'; + return null; // Ignore .json, .md, etc. + }, + generate: async ({filer, log, origin_path}) => { - log.info('generating Fuz CSS classes...'); + log.info('generating fuz_css classes...'); + + // Load CSS properties for validation before generation + const css_properties = await load_css_properties(); await filer.init(); - const css_classes = new CssClasses(); + // Normalize project root - ensure it ends with / + const raw_project_root = project_root_option ?? process.cwd(); + const project_root = raw_project_root.endsWith('/') + ? raw_project_root + : raw_project_root + '/'; + const resolved_cache_dir = join(project_root, cache_dir); + + const css_classes = new CssClasses(include_set, exclude_set); + const current_paths: Set = new Set(); const stats = { total_files: filer.files.size, @@ -60,8 +236,17 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => { processed_files: 0, files_with_content: 0, files_with_classes: 0, + cache_hits: 0, + cache_misses: 0, }; + // Collect nodes to process + const nodes: Array<{ + id: string; + contents: string; + content_hash: string; + }> = []; + for (const disknode of filer.files.values()) { if (disknode.external) { stats.external_files++; @@ -69,28 +254,116 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => { stats.internal_files++; } - if (filter_file && !filter_file(disknode.id)) { + if (!filter_file(disknode.id)) { continue; } stats.processed_files++; - if (disknode.contents !== null) { + if (disknode.contents !== null && disknode.content_hash !== null) { stats.files_with_content++; - const classes = collect_css_classes(disknode.contents); - if (classes.size > 0) { - css_classes.add(disknode.id, classes); + nodes.push({ + id: disknode.id, + contents: disknode.contents, + content_hash: disknode.content_hash, + }); + } + } + + // Parallel extraction with cache check + const extractions: Array = await map_concurrent( + nodes, + async (node): Promise => { + current_paths.add(node.id); + const cache_path = await get_file_cache_path(node.id, resolved_cache_dir, project_root); + + // Try cache (skip on CI) + if (!is_ci) { + const cached = await load_cached_extraction(cache_path); + if (cached && cached.content_hash === node.content_hash) { + // Cache hit + stats.cache_hits++; + return { + id: node.id, + ...from_cached_extraction(cached), + cache_path: null, + content_hash: node.content_hash, + }; + } + } + + // Cache miss - extract + stats.cache_misses++; + const result = extract_css_classes_with_locations(node.contents, { + filename: node.id, + acorn_plugins, + }); + + return { + id: node.id, + classes: result.classes, + explicit_classes: result.explicit_classes, + diagnostics: result.diagnostics, + cache_path: is_ci ? null : cache_path, + content_hash: node.content_hash, + }; + }, + concurrency, + ); + + // Add to CssClasses (null = empty, so use truthiness check) + for (const {id, classes, explicit_classes, diagnostics} of extractions) { + if (classes || explicit_classes || diagnostics) { + css_classes.add(id, classes, explicit_classes, diagnostics); + if (classes) { stats.files_with_classes++; } } } - const unique_classes = new Set(); - for (const file_classes of css_classes.get().values()) { - for (const class_name of file_classes) { - unique_classes.add(class_name); + // Collect cache writes (entries that need writing) + const cache_writes = extractions.filter( + (e): e is FileExtraction & {cache_path: string} => e.cache_path !== null, + ); + + // Parallel cache writes (await completion) + if (cache_writes.length > 0) { + await each_concurrent( + cache_writes, + async ({cache_path, content_hash, classes, explicit_classes, diagnostics}) => { + await save_cached_extraction( + cache_path, + content_hash, + classes, + explicit_classes, + diagnostics, + ); + }, + cache_io_concurrency, + ).catch((err) => log.warn('Cache write error:', err)); + } + + // Watch mode cleanup: delete cache files for removed source files + // Note: Empty directories are intentionally left behind (rare case, not worth the cost) + if (!is_ci && previous_paths) { + const paths_to_delete = [...previous_paths].filter((p) => !current_paths.has(p)); + if (paths_to_delete.length > 0) { + await each_concurrent( + paths_to_delete, + async (path) => { + const cache_path = await get_file_cache_path(path, resolved_cache_dir, project_root); + await delete_cached_extraction(cache_path); + }, + cache_io_concurrency, + ).catch(() => { + // Ignore deletion errors + }); } } + previous_paths = current_paths; + + // Get all classes with locations (already filtered by exclude_classes) + const {all_classes, all_classes_with_locations, explicit_classes} = css_classes.get_all(); if (include_stats) { log.info('File statistics:'); @@ -100,29 +373,82 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => { log.info(` Files processed (passed filter): ${stats.processed_files}`); log.info(` With content: ${stats.files_with_content}`); log.info(` With CSS classes: ${stats.files_with_classes}`); - log.info(` Unique CSS classes found: ${unique_classes.size}`); + log.info(` Cache: ${stats.cache_hits} hits, ${stats.cache_misses} misses`); + log.info(` Unique CSS classes found: ${all_classes.size}`); + } + + // Merge class definitions (user definitions take precedence) + if (!include_default_definitions && !user_class_definitions) { + throw new Error('class_definitions is required when include_default_definitions is false'); } + const all_class_definitions = include_default_definitions + ? user_class_definitions + ? {...css_class_definitions, ...user_class_definitions} + : css_class_definitions + : user_class_definitions!; - const css = generate_classes_css(css_classes.get(), classes_by_name, class_interpreters, log); + const result = generate_classes_css({ + class_names: all_classes, + class_definitions: all_class_definitions, + interpreters: class_interpreters, + css_properties, + log, + class_locations: all_classes_with_locations, + explicit_classes, + }); + + // Collect all diagnostics: extraction + generation + const all_diagnostics: Array = [ + ...css_classes.get_diagnostics(), + ...result.diagnostics, + ]; + + // Separate errors and warnings + const errors = all_diagnostics.filter((d) => d.level === 'error'); + const warnings = all_diagnostics.filter((d) => d.level === 'warning'); + + // Handle warnings based on on_warning setting + if (warnings.length > 0) { + if (on_warning === 'throw') { + throw new CssGenerationError(warnings); + } else if (on_warning === 'log') { + for (const warning of warnings) { + log.warn(format_diagnostic(warning)); + } + } + // 'ignore' - do nothing + } + + // Handle errors based on on_error setting + if (errors.length > 0) { + if (on_error === 'throw') { + throw new CssGenerationError(errors); + } + // 'log' mode - log each error with details + for (const error of errors) { + log.error(format_diagnostic(error)); + } + } const banner = `generated by ${origin_path}`; const content_parts = [`/* ${banner} */`]; if (include_stats) { - const performance_note = `/* * + const performance_note = `/* * * File statistics: * - Total files in filer: ${stats.total_files} * - External dependencies: ${stats.external_files} * - Internal project files: ${stats.internal_files} * - Files processed (passed filter): ${stats.processed_files} * - Files with CSS classes: ${stats.files_with_classes} - * - Unique classes found: ${unique_classes.size} + * - Cache: ${stats.cache_hits} hits, ${stats.cache_misses} misses + * - Unique classes found: ${all_classes.size} */`; content_parts.push(performance_note); } - content_parts.push(css); + content_parts.push(result.css); content_parts.push(`/* ${banner} */`); return content_parts.join('\n\n'); diff --git a/src/lib/modifiers.ts b/src/lib/modifiers.ts new file mode 100644 index 000000000..9a95fce6a --- /dev/null +++ b/src/lib/modifiers.ts @@ -0,0 +1,305 @@ +/** + * Declarative modifier definitions for CSS-literal syntax. + * + * Modifiers enable responsive, state-based, and contextual styling: + * - Media modifiers: `md:`, `print:`, `motion-safe:` + * - Ancestor modifiers: `dark:`, `light:` + * - State modifiers: `hover:`, `focus:`, `disabled:` + * - Pseudo-element modifiers: `before:`, `after:` + * + * @see {@link https://github.com/fuzdev/fuz_css} for documentation + * @module + */ + +/** + * Type of modifier determining its position in the class name and CSS output. + * + * Order in class names: `[media:][ancestor:][state...:][pseudo-element:]property:value` + */ +export type ModifierType = 'media' | 'ancestor' | 'state' | 'pseudo-element'; + +/** + * Definition for a single modifier. + */ +export interface ModifierDefinition { + /** The prefix used in class names (e.g., 'hover', 'md', 'dark') */ + name: string; + /** Type determines position in modifier order and CSS output behavior */ + type: ModifierType; + /** The CSS output - wrapper for media/ancestor, suffix for state/pseudo-element */ + css: string; + /** Optional ordering within type (for breakpoints, sorted by this value) */ + order?: number; +} + +/** + * All modifier definitions in a single declarative structure. + * Adding a new modifier requires only adding to this array. + */ +export const MODIFIERS: Array = [ + // Media modifiers - viewport breakpoints (mobile-first) + {name: 'sm', type: 'media', css: '@media (width >= 40rem)', order: 1}, + {name: 'md', type: 'media', css: '@media (width >= 48rem)', order: 2}, + {name: 'lg', type: 'media', css: '@media (width >= 64rem)', order: 3}, + {name: 'xl', type: 'media', css: '@media (width >= 80rem)', order: 4}, + {name: '2xl', type: 'media', css: '@media (width >= 96rem)', order: 5}, + + // Max-width variants (for targeting below a breakpoint) + {name: 'max-sm', type: 'media', css: '@media (width < 40rem)', order: 11}, + {name: 'max-md', type: 'media', css: '@media (width < 48rem)', order: 12}, + {name: 'max-lg', type: 'media', css: '@media (width < 64rem)', order: 13}, + {name: 'max-xl', type: 'media', css: '@media (width < 80rem)', order: 14}, + {name: 'max-2xl', type: 'media', css: '@media (width < 96rem)', order: 15}, + + // Media modifiers - feature queries + {name: 'print', type: 'media', css: '@media print'}, + {name: 'motion-safe', type: 'media', css: '@media (prefers-reduced-motion: no-preference)'}, + {name: 'motion-reduce', type: 'media', css: '@media (prefers-reduced-motion: reduce)'}, + {name: 'contrast-more', type: 'media', css: '@media (prefers-contrast: more)'}, + {name: 'contrast-less', type: 'media', css: '@media (prefers-contrast: less)'}, + {name: 'portrait', type: 'media', css: '@media (orientation: portrait)'}, + {name: 'landscape', type: 'media', css: '@media (orientation: landscape)'}, + {name: 'forced-colors', type: 'media', css: '@media (forced-colors: active)'}, + + // Ancestor modifiers - color scheme + {name: 'dark', type: 'ancestor', css: ':root.dark'}, + {name: 'light', type: 'ancestor', css: ':root.light'}, + + // State modifiers - interaction (ordered for proper cascade: LVFHA) + {name: 'any-link', type: 'state', css: ':any-link'}, + {name: 'link', type: 'state', css: ':link'}, + {name: 'visited', type: 'state', css: ':visited', order: 1}, + {name: 'focus-within', type: 'state', css: ':focus-within', order: 2}, + {name: 'focus', type: 'state', css: ':focus', order: 3}, + {name: 'focus-visible', type: 'state', css: ':focus-visible', order: 4}, + {name: 'hover', type: 'state', css: ':hover', order: 5}, + {name: 'active', type: 'state', css: ':active', order: 6}, + {name: 'target', type: 'state', css: ':target', order: 7}, + + // State modifiers - form states + {name: 'autofill', type: 'state', css: ':autofill'}, + {name: 'blank', type: 'state', css: ':blank'}, + {name: 'disabled', type: 'state', css: ':disabled'}, + {name: 'enabled', type: 'state', css: ':enabled'}, + {name: 'checked', type: 'state', css: ':checked'}, + {name: 'indeterminate', type: 'state', css: ':indeterminate'}, + {name: 'default', type: 'state', css: ':default'}, + {name: 'required', type: 'state', css: ':required'}, + {name: 'optional', type: 'state', css: ':optional'}, + {name: 'valid', type: 'state', css: ':valid'}, + {name: 'invalid', type: 'state', css: ':invalid'}, + {name: 'user-valid', type: 'state', css: ':user-valid'}, + {name: 'user-invalid', type: 'state', css: ':user-invalid'}, + {name: 'in-range', type: 'state', css: ':in-range'}, + {name: 'out-of-range', type: 'state', css: ':out-of-range'}, + {name: 'placeholder-shown', type: 'state', css: ':placeholder-shown'}, + {name: 'read-only', type: 'state', css: ':read-only'}, + {name: 'read-write', type: 'state', css: ':read-write'}, + + // State modifiers - structural + {name: 'first', type: 'state', css: ':first-child'}, + {name: 'last', type: 'state', css: ':last-child'}, + {name: 'only', type: 'state', css: ':only-child'}, + {name: 'first-of-type', type: 'state', css: ':first-of-type'}, + {name: 'last-of-type', type: 'state', css: ':last-of-type'}, + {name: 'only-of-type', type: 'state', css: ':only-of-type'}, + {name: 'odd', type: 'state', css: ':nth-child(odd)'}, + {name: 'even', type: 'state', css: ':nth-child(even)'}, + {name: 'empty', type: 'state', css: ':empty'}, + // Note: nth-child(N), nth-last-child(N), nth-of-type(N), nth-last-of-type(N) are handled dynamically + + // State modifiers - UI states + {name: 'fullscreen', type: 'state', css: ':fullscreen'}, + {name: 'modal', type: 'state', css: ':modal'}, + {name: 'open', type: 'state', css: ':open'}, + {name: 'popover-open', type: 'state', css: ':popover-open'}, + + // State modifiers - media states + {name: 'paused', type: 'state', css: ':paused'}, + {name: 'playing', type: 'state', css: ':playing'}, + + // Pseudo-element modifiers + {name: 'before', type: 'pseudo-element', css: '::before'}, + {name: 'after', type: 'pseudo-element', css: '::after'}, + {name: 'cue', type: 'pseudo-element', css: '::cue'}, + {name: 'first-letter', type: 'pseudo-element', css: '::first-letter'}, + {name: 'first-line', type: 'pseudo-element', css: '::first-line'}, + {name: 'placeholder', type: 'pseudo-element', css: '::placeholder'}, + {name: 'selection', type: 'pseudo-element', css: '::selection'}, + {name: 'marker', type: 'pseudo-element', css: '::marker'}, + {name: 'file', type: 'pseudo-element', css: '::file-selector-button'}, + {name: 'backdrop', type: 'pseudo-element', css: '::backdrop'}, +]; + +// Generated lookup maps for efficient access + +/** Map of media modifier names to their CSS output */ +export const MEDIA_MODIFIERS: Map = new Map( + MODIFIERS.filter((m) => m.type === 'media').map((m) => [m.name, m]), +); + +/** Map of ancestor modifier names to their CSS output */ +export const ANCESTOR_MODIFIERS: Map = new Map( + MODIFIERS.filter((m) => m.type === 'ancestor').map((m) => [m.name, m]), +); + +/** Map of state modifier names to their CSS output */ +export const STATE_MODIFIERS: Map = new Map( + MODIFIERS.filter((m) => m.type === 'state').map((m) => [m.name, m]), +); + +/** Map of pseudo-element modifier names to their CSS output */ +export const PSEUDO_ELEMENT_MODIFIERS: Map = new Map( + MODIFIERS.filter((m) => m.type === 'pseudo-element').map((m) => [m.name, m]), +); + +/** All modifier names for quick lookup */ +export const ALL_MODIFIER_NAMES: Set = new Set(MODIFIERS.map((m) => m.name)); + +/** + * Pattern for arbitrary min-width breakpoints: `min-width(800px):` + */ +export const ARBITRARY_MIN_WIDTH_PATTERN = /^min-width\(([^)]+)\)$/; + +/** + * Pattern for arbitrary max-width breakpoints: `max-width(600px):` + */ +export const ARBITRARY_MAX_WIDTH_PATTERN = /^max-width\(([^)]+)\)$/; + +/** + * Pattern for parameterized nth-child: `nth-child(2n+1):` + */ +export const NTH_CHILD_PATTERN = /^nth-child\(([^)]+)\)$/; + +/** + * Pattern for parameterized nth-last-child: `nth-last-child(2n+1):` + */ +export const NTH_LAST_CHILD_PATTERN = /^nth-last-child\(([^)]+)\)$/; + +/** + * Pattern for parameterized nth-of-type: `nth-of-type(2n):` + */ +export const NTH_OF_TYPE_PATTERN = /^nth-of-type\(([^)]+)\)$/; + +/** + * Pattern for parameterized nth-last-of-type: `nth-last-of-type(2n):` + */ +export const NTH_LAST_OF_TYPE_PATTERN = /^nth-last-of-type\(([^)]+)\)$/; + +/** + * Parses an arbitrary breakpoint modifier. + * + * @returns The CSS media query or null if not an arbitrary breakpoint + */ +export const parse_arbitrary_breakpoint = (segment: string): string | null => { + const min_match = ARBITRARY_MIN_WIDTH_PATTERN.exec(segment); + if (min_match) { + return `@media (width >= ${min_match[1]})`; + } + + const max_match = ARBITRARY_MAX_WIDTH_PATTERN.exec(segment); + if (max_match) { + return `@media (width < ${max_match[1]})`; + } + + return null; +}; + +/** + * Parses a parameterized state modifier (nth-child, nth-last-child, nth-of-type, nth-last-of-type). + * + * @returns Object with name (including parameter) and CSS, or null if not parameterized + */ +export const parse_parameterized_state = ( + segment: string, +): {name: string; css: string; type: 'state'} | null => { + const nth_child_match = NTH_CHILD_PATTERN.exec(segment); + if (nth_child_match) { + return { + name: segment, + css: `:nth-child(${nth_child_match[1]})`, + type: 'state', + }; + } + + const nth_last_child_match = NTH_LAST_CHILD_PATTERN.exec(segment); + if (nth_last_child_match) { + return { + name: segment, + css: `:nth-last-child(${nth_last_child_match[1]})`, + type: 'state', + }; + } + + const nth_of_type_match = NTH_OF_TYPE_PATTERN.exec(segment); + if (nth_of_type_match) { + return { + name: segment, + css: `:nth-of-type(${nth_of_type_match[1]})`, + type: 'state', + }; + } + + const nth_last_of_type_match = NTH_LAST_OF_TYPE_PATTERN.exec(segment); + if (nth_last_of_type_match) { + return { + name: segment, + css: `:nth-last-of-type(${nth_last_of_type_match[1]})`, + type: 'state', + }; + } + + return null; +}; + +/** + * Gets the modifier definition for a segment. + * Handles both static modifiers and dynamic patterns (arbitrary breakpoints, parameterized states). + * + * @returns The modifier definition or null if not a known modifier + */ +export const get_modifier = ( + segment: string, +): (ModifierDefinition & {is_arbitrary?: boolean}) | null => { + // Check static modifiers first + const media = MEDIA_MODIFIERS.get(segment); + if (media) return media; + + const ancestor = ANCESTOR_MODIFIERS.get(segment); + if (ancestor) return ancestor; + + const state = STATE_MODIFIERS.get(segment); + if (state) return state; + + const pseudo = PSEUDO_ELEMENT_MODIFIERS.get(segment); + if (pseudo) return pseudo; + + // Check arbitrary breakpoints + const arbitrary_css = parse_arbitrary_breakpoint(segment); + if (arbitrary_css) { + return { + name: segment, + type: 'media', + css: arbitrary_css, + is_arbitrary: true, + }; + } + + // Check parameterized state modifiers + const parameterized = parse_parameterized_state(segment); + if (parameterized) { + return { + ...parameterized, + is_arbitrary: true, + }; + } + + return null; +}; + +/** + * Gets all modifier names for error message suggestions. + */ +export const get_all_modifier_names = (): Array => { + return Array.from(ALL_MODIFIER_NAMES).sort(); +}; diff --git a/src/lib/style.css b/src/lib/style.css index 343237556..aa990eb8e 100644 --- a/src/lib/style.css +++ b/src/lib/style.css @@ -1,6 +1,6 @@ /* -A CSS Fuz CSS theme file like `@fuzdev/fuz_css/theme.css` +A CSS fuz_css theme file like `@fuzdev/fuz_css/theme.css` is expected to be imported alongside this one containing the base variable declarations. By default that's the `'base'` theme's variables, @@ -11,7 +11,7 @@ Some notes: - acts as a CSS reset and also sets minimal defaults for elements with themable variables - uses `:where` on all selectors with more specificity than a single tag/class to lower specificity so utility classes override them - - this also means the Fuz CSS stylesheet is is less likely to + this also means the fuz_css stylesheet is is less likely to interfere with the page's styles regardless of where it's imported - `.unstyled` class opts out of opinionated styling (colors, borders, decorative properties) while keeping normalizations (font inheritance, border-collapse) - @@ -136,8 +136,7 @@ Respects `hidden="until-found"` for find-in-page support. :where(:is(h1, h2, h3, h4, h5, h6, .heading):not(.unstyled)) { font-family: var(--font_family_serif); - font-size: var(--font_size, var(--font_size_md)); - font-weight: var(--font_weight); + font-size: var(--font_size, inherit); line-height: var(--line_height_sm); text-wrap: balance; /* @see https://developer.mozilla.org/en-US/docs/Web/CSS/text-wrap#balance */ /* TODO use this pattern elsewhere? provides API to components like `MdnLogo` */ @@ -146,38 +145,38 @@ Respects `hidden="until-found"` for find-in-page support. :where(h1:not(.unstyled)) { --font_size: var(--font_size_xl3); - --font_weight: 300; + font-weight: 300; margin-bottom: var(--space_xl5); /* TODO strange to omit only this one, but seems to be generally my desired behavior */ /* margin-top: var(--space_xl7); */ } :where(h2:not(.unstyled)) { --font_size: var(--font_size_xl2); - --font_weight: 400; + font-weight: 400; margin-bottom: var(--space_xl4); margin-top: var(--space_xl6); } :where(h3:not(.unstyled)) { --font_size: var(--font_size_xl); - --font_weight: 500; + font-weight: 500; margin-bottom: var(--space_xl3); margin-top: var(--space_xl5); } :where(h4:not(.unstyled)) { --font_size: var(--font_size_lg); - --font_weight: 700; + font-weight: 700; margin-bottom: var(--space_xl2); margin-top: var(--space_xl4); } :where(h5:not(.unstyled)) { --font_size: var(--font_size_md); - --font_weight: 900; + font-weight: 900; margin-bottom: var(--space_xl); margin-top: var(--space_xl3); } :where(h6:not(.unstyled)) { --font_size: var(--font_size_sm); - --font_weight: 600; + font-weight: 600; margin-bottom: var(--space_lg); margin-top: var(--space_xl2); text-transform: uppercase; @@ -490,7 +489,6 @@ for disabled colors without needing a wrapper .disabled class */ } :where(label.row:not(.unstyled)) { justify-content: flex-start; - align-items: center; } :where(label.row:not(.unstyled) :is(input[type='checkbox'], input[type='radio']):not(.unstyled)) { margin-right: var(--space_md); diff --git a/src/lib/variable_data.ts b/src/lib/variable_data.ts index bf3886445..a498d2ff4 100644 --- a/src/lib/variable_data.ts +++ b/src/lib/variable_data.ts @@ -76,8 +76,8 @@ export const shadow_variant_prefixes = [ 'shadow_inset_bottom_', ] as const; -export type ShadowSizeVariant = ArrayElement; -export const shadow_font_size_variants = ['xs', 'sm', 'md', 'lg', 'xl'] as const; +export type ShadowSizeVariant = ArrayElement; +export const shadow_size_variants = ['xs', 'sm', 'md', 'lg', 'xl'] as const; export type ShadowAlphaVariant = ArrayElement; export const shadow_alpha_variants = [1, 2, 3, 4, 5] as const; @@ -102,117 +102,19 @@ export const icon_sizes = { export type ColorVariant = ArrayElement; export const color_variants = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] as const; +// TODO maybe add 0 and 10? +export type IntensityVariant = ArrayElement; +export const intensity_variants = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const; + +export type BorderColorIntensityVariant = ArrayElement; +export const border_color_intensity_variants = [1, 2, 3, 4, 5] as const; + export type BorderWidthVariant = ArrayElement; export const border_width_variants = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const; export type OutlineWidthVariant = ArrayElement; export const outline_width_variants = ['focus', 'active'] as const; -export type AlignmentValue = ArrayElement; -export const alignment_values = ['center', 'start', 'end', 'baseline', 'stretch'] as const; - -export type JustifyValue = ArrayElement; -export const justify_values = [ - 'center', - 'start', - 'end', - 'left', - 'right', - 'space-between', - 'space-around', - 'space-evenly', - 'stretch', -] as const; - -export type OverflowValue = ArrayElement; -export const overflow_values = ['auto', 'hidden', 'scroll', 'clip', 'visible'] as const; - -export type BorderStyleValue = ArrayElement; -export const border_style_values = [ - 'none', - 'hidden', - 'dotted', - 'dashed', - 'solid', - 'double', - 'groove', - 'ridge', - 'inset', - 'outset', -] as const; - -export type DisplayValue = ArrayElement; -export const display_values = [ - 'none', - 'contents', - 'block', - 'flow-root', - 'inline', - 'inline-block', - 'run-in', - 'list-item', - 'inline list-item', - 'flex', - 'inline-flex', - 'grid', - 'inline-grid', - 'ruby', - 'block ruby', - 'table', - 'inline-table', -] as const; - -export type TextAlignValue = ArrayElement; -export const text_align_values = [ - 'start', - 'end', - 'left', - 'right', - 'center', - 'justify', - 'justify-all', - 'match-parent', -] as const; - -export type VerticalAlignValue = ArrayElement; -export const vertical_align_values = [ - 'baseline', - 'sub', - 'super', - 'text-top', - 'text-bottom', - 'middle', - 'top', - 'bottom', -] as const; - -export type WordBreakValue = ArrayElement; -export const word_break_values = ['normal', 'break-all', 'keep-all'] as const; - -export type PositionValue = ArrayElement; -export const position_values = ['static', 'relative', 'absolute', 'fixed', 'sticky'] as const; - -export type VisibilityValue = ArrayElement; -export const visibility_values = ['visible', 'hidden', 'collapse'] as const; - -export type FloatValue = ArrayElement; -export const float_values = ['none', 'left', 'right', 'inline-start'] as const; - -export type FlexWrapValue = ArrayElement; -export const flex_wrap_values = ['nowrap', 'wrap', 'wrap-reverse'] as const; - -export type FlexDirectionValue = ArrayElement; -export const flex_direction_values = ['row', 'row-reverse', 'column', 'column-reverse'] as const; - -export type OverflowWrapValue = ArrayElement; -export const overflow_wrap_values = ['normal', 'anywhere', 'break-word'] as const; - -export type ScrollbarWidthValue = ArrayElement; -export const scrollbar_width_values = ['auto', 'thin', 'none'] as const; - -export type ScrollbarGutterValue = ArrayElement; -export const scrollbar_gutter_values = ['auto', 'stable', 'stable both-edges'] as const; - /** * Maximum value for CSS z-index property (32-bit signed integer max). */ diff --git a/src/lib/vite_plugin_fuz_css.ts b/src/lib/vite_plugin_fuz_css.ts new file mode 100644 index 000000000..306f5e7c5 --- /dev/null +++ b/src/lib/vite_plugin_fuz_css.ts @@ -0,0 +1,508 @@ +/** + * Vite plugin for fuz_css utility class generation. + * + * Uses Vite's transform hook to extract CSS classes from source files + * as they're processed, including node_modules dependencies. + * Generates CSS on-demand via virtual module with HMR support. + * + * @example + * ```ts + * // vite.config.ts + * import {defineConfig} from 'vite'; + * import jsx from 'acorn-jsx'; + * import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.js'; + * + * export default defineConfig({ + * plugins: [ + * vite_plugin_fuz_css({ + * acorn_plugins: [jsx()], + * }), + * ], + * }); + * ``` + * + * @module + */ + +import type {Plugin, ViteDevServer} from 'vite'; +import {join} from 'node:path'; + +import {extract_css_classes_with_locations, type AcornPlugin} from './css_class_extractor.js'; +import {type Diagnostic, CssGenerationError} from './diagnostics.js'; +import { + generate_classes_css, + type CssClassDefinition, + type CssClassDefinitionInterpreter, +} from './css_class_generation.js'; +import {css_class_definitions} from './css_class_definitions.js'; +import {css_class_interpreters} from './css_class_interpreters.js'; +import {load_css_properties} from './css_literal.js'; +import { + DEFAULT_CACHE_DIR, + get_cache_path, + load_cached_extraction, + save_cached_extraction, + delete_cached_extraction, + from_cached_extraction, + compute_hash, +} from './css_cache.js'; +import {type FileFilter, filter_file_default} from './file_filter.js'; +import {CssClasses} from './css_classes.js'; + +/* eslint-disable no-console */ + +/** Marker comment used to delimit generated CSS in the output. */ +export const FUZ_CSS_MARKER = '/* generated by vite_plugin_fuz_css */'; + +const VIRTUAL_ID = 'virtual:fuz.css'; +// In dev mode, resolve to .js so Vite treats it as JS (for HMR handling) +// In build mode, resolve to .css for proper CSS bundling +const RESOLVED_VIRTUAL_ID_JS = '\0virtual:fuz.css.js'; +const RESOLVED_VIRTUAL_ID_CSS = '\0virtual:fuz.css'; + +// TODO investigate: Dev mode uses a JS wrapper that injects CSS and self-accepts HMR. +// We couldn't get plain CSS with `css-update` to work for virtual modules - there's a +// mismatch between `data-vite-dev-id` (set to `\0virtual:fuz.css`) and the importable +// URL (`/@id/__x00__virtual:fuz.css`). UnoCSS uses `js-update` with `mod.url` for plain +// CSS but that broke all HMR when we tried it. Areas to investigate: +// - How does Vite's CSS HMR actually resolve virtual module URLs? +// - Why does UnoCSS's approach work for them but not here? +// - Is there a way to control what `data-vite-dev-id` gets set to? +// The current JS wrapper approach works reliably, but plain CSS would be cleaner. + +/** + * Skip cache on CI (no point writing cache that won't be reused). + */ +const is_ci = !!process.env.CI; + +/** + * Options for the fuz_css Vite plugin. + */ +export interface VitePluginFuzCssOptions { + /** + * Filter function to determine which files to extract classes from. + * By default, extracts from .svelte, .html, .ts, .js, .tsx, .jsx files, + * excluding test files and .gen files. + */ + filter_file?: FileFilter; + /** + * Additional class definitions to merge with defaults. + * User definitions take precedence over defaults with the same name. + * Required when `include_default_definitions` is `false`. + */ + class_definitions?: Record; + /** + * Whether to include default class definitions (token and composite classes). + * When `false`, `class_definitions` is required. + * @default true + */ + include_default_definitions?: boolean; + /** + * Custom interpreters for dynamic class generation. + * Replaces the builtin interpreters entirely if provided. + */ + class_interpreters?: Array; + /** + * Classes to always include in the output, regardless of detection. + */ + include_classes?: Iterable; + /** + * Classes to exclude from the output, even if detected. + */ + exclude_classes?: Iterable; + /** + * Additional acorn plugins for parsing. + * Use `acorn-jsx` for React/Preact/Solid projects. + */ + acorn_plugins?: Array; + /** + * How to handle CSS-literal errors during generation. + * - 'log': Log errors, skip invalid classes, continue + * - 'throw': Throw on first error, fail the build + * @default 'throw' in CI, 'log' otherwise + */ + on_error?: 'log' | 'throw'; + /** + * How to handle warnings during generation. + * - 'log': Log warnings, continue + * - 'throw': Throw on first warning, fail the build + * - 'ignore': Suppress warnings entirely + * @default 'log' + */ + on_warning?: 'log' | 'throw' | 'ignore'; + /** + * Cache directory relative to project root. + * @default DEFAULT_CACHE_DIR + */ + cache_dir?: string; +} + +/** + * Creates the fuz_css Vite plugin. + * + * Extracts CSS classes from source files during Vite's transform phase + * and generates optimized CSS via the `virtual:fuz.css` virtual module. + */ +export const vite_plugin_fuz_css = (options: VitePluginFuzCssOptions = {}): Plugin => { + const { + filter_file = filter_file_default, + class_definitions: user_class_definitions, + include_default_definitions = true, + class_interpreters = css_class_interpreters, + include_classes, + exclude_classes, + acorn_plugins, + on_error = is_ci ? 'throw' : 'log', + on_warning = 'log', + cache_dir = DEFAULT_CACHE_DIR, + } = options; + + // Merge class definitions (user definitions take precedence) + if (!include_default_definitions && !user_class_definitions) { + throw new Error('class_definitions is required when include_default_definitions is false'); + } + const all_class_definitions = include_default_definitions + ? user_class_definitions + ? {...css_class_definitions, ...user_class_definitions} + : css_class_definitions + : user_class_definitions!; + + // Convert to Sets for efficient lookup + const include_set = include_classes ? new Set(include_classes) : null; + const exclude_set = exclude_classes ? new Set(exclude_classes) : null; + + // Plugin state + const css_classes = new CssClasses(include_set, exclude_set); + const hashes: Map = new Map(); + let virtual_module_loaded = false; + let server: ViteDevServer | null = null; + let css_properties: Set | null = null; + let resolved_cache_dir: string | null = null; + let project_root: string | null = null; + let hmr_timeout: ReturnType | null = null; + let last_generated_css: string | null = null; + + /** Logs a warning message (works in both dev and build) */ + const log_warn = (msg: string): void => { + if (server) { + server.config.logger.warn(msg); + } else { + console.warn(msg); + } + }; + + /** Logs an error message (works in both dev and build) */ + const log_error = (msg: string): void => { + if (server) { + server.config.logger.error(msg); + } else { + console.error(msg); + } + }; + + /** + * Computes cache path for a file. + * Internal files use relative paths, external files use hashed absolute paths. + */ + const get_file_cache_path = async (file_id: string): Promise => { + const is_internal = file_id.startsWith(project_root!); + return is_internal + ? get_cache_path(file_id, resolved_cache_dir!, project_root!) + : join( + resolved_cache_dir!, + '_external', + (await compute_hash(file_id)).slice(0, 16) + '.json', + ); + }; + + /** + * Generates CSS from the current classes. + */ + const generate_css = (): string => { + const { + all_classes: classes, + all_classes_with_locations: locations, + explicit_classes, + } = css_classes.get_all(); + + const result = generate_classes_css({ + class_names: classes, + class_definitions: all_class_definitions, + interpreters: class_interpreters, + css_properties, + class_locations: locations, + explicit_classes, + }); + + // Collect all diagnostics: extraction + generation + const all_diagnostics: Array = [ + ...css_classes.get_diagnostics(), + ...result.diagnostics, + ]; + + // Separate errors and warnings + const errors = all_diagnostics.filter((d) => d.level === 'error'); + const warnings = all_diagnostics.filter((d) => d.level === 'warning'); + + // Handle warnings based on on_warning setting + if (warnings.length > 0) { + if (on_warning === 'throw') { + throw new CssGenerationError(warnings); + } else if (on_warning === 'log') { + for (const d of result.diagnostics.filter((d) => d.level === 'warning')) { + log_warn(`[fuz_css] ${d.class_name}: ${d.message}`); + } + } + // 'ignore' - do nothing + } + + // Handle errors based on on_error setting + if (errors.length > 0) { + if (on_error === 'throw') { + throw new CssGenerationError(errors); + } + // Log errors (extraction diagnostics already logged in transform) + for (const d of result.diagnostics.filter((d) => d.level === 'error')) { + log_error(`[fuz_css] ${d.class_name}: ${d.message}`); + } + } + + return `${FUZ_CSS_MARKER}\n\n${result.css}\n\n${FUZ_CSS_MARKER}`; + }; + + /** + * Invalidates the virtual module and triggers HMR. + * Debounced to avoid spamming updates when multiple files change rapidly. + * Only triggers if the generated CSS actually changed. + */ + const invalidate_virtual_module = (): void => { + if (!server) return; + + // Debounce: wait 10ms for more changes before triggering HMR + if (hmr_timeout) { + clearTimeout(hmr_timeout); + } + hmr_timeout = setTimeout(() => { + hmr_timeout = null; + + // Check if CSS actually changed + const new_css = generate_css(); + if (new_css === last_generated_css) { + return; // No change, skip HMR + } + last_generated_css = new_css; + + const mod = server!.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID_JS); + if (mod) { + server!.moduleGraph.invalidateModule(mod); + // TODO investigate: This hardcoded path matches Vite's URL encoding for virtual + // modules. Using `mod.url` doesn't work (it's `\0virtual:fuz.css.js`). Could break + // if Vite changes their encoding scheme. Is there a proper API for this? + const hmr_path = '/@id/__x00__virtual:fuz.css.js'; + const hot = server?.hot ?? server!.ws; + hot.send({ + type: 'update', + updates: [ + { + type: 'js-update', + path: hmr_path, + acceptedPath: hmr_path, + timestamp: Date.now(), + }, + ], + }); + } + }, 10); + }; + + return { + name: 'vite-plugin-fuz-css', + // Run before other plugins (like Svelte) to see original source files + enforce: 'pre', + + configResolved(resolved_config) { + const root = resolved_config.root; + project_root = root.endsWith('/') ? root : root + '/'; + resolved_cache_dir = join(root, cache_dir); + }, + + configureServer(dev_server) { + server = dev_server; + + // Handle file deletion - watcher 'unlink' event + dev_server.watcher.on('unlink', (file) => { + if (hashes.has(file)) { + css_classes.delete(file); + hashes.delete(file); + + // Delete cache file (fire and forget) + if (!is_ci && resolved_cache_dir && project_root) { + get_file_cache_path(file) + .then((cache_path) => delete_cached_extraction(cache_path)) + .catch(() => { + // Ignore cache deletion errors + }); + } + + if (virtual_module_loaded) { + invalidate_virtual_module(); + } + } + }); + }, + + async buildStart() { + // Load CSS properties for validation + css_properties = await load_css_properties(); + }, + + resolveId(id) { + if (id === VIRTUAL_ID) { + // In dev mode, resolve to .js for HMR support + // In build mode, resolve to .css for proper bundling + return server ? RESOLVED_VIRTUAL_ID_JS : RESOLVED_VIRTUAL_ID_CSS; + } + return undefined; + }, + + load(id) { + // Dev mode: JS module that injects CSS and handles HMR + if (id === RESOLVED_VIRTUAL_ID_JS) { + virtual_module_loaded = true; + const css = generate_css(); + last_generated_css = css; // Track for HMR diffing + const escaped_css = JSON.stringify(css); + return ` +const css = ${escaped_css}; + +// Find existing style tag or create new one +let style = document.querySelector('style[data-fuz-css]'); +if (!style) { + style = document.createElement('style'); + style.setAttribute('data-fuz-css', ''); + document.head.appendChild(style); +} +style.textContent = css; + +if (import.meta.hot) { + import.meta.hot.accept(); +} + +export {}; +`; + } + // Build mode: plain CSS + if (id === RESOLVED_VIRTUAL_ID_CSS) { + virtual_module_loaded = true; + // Return empty CSS for build - generateBundle will append the complete CSS + return '/* fuz_css placeholder */'; + } + return undefined; + }, + + generateBundle(_options, bundle) { + // Regenerate CSS with all extracted classes and append to the CSS asset + // This runs after all transforms are complete, so all classes are available + const generated_css = generate_css(); + + // Find the main CSS asset and append our generated CSS + // Vite combines all CSS into one or more asset files + for (const chunk of Object.values(bundle)) { + if ( + chunk.type === 'asset' && + typeof chunk.source === 'string' && + chunk.fileName.endsWith('.css') + ) { + // Append the complete generated CSS to the end + chunk.source = chunk.source + '\n' + generated_css; + break; // Only append to first CSS asset + } + } + }, + + async transform(code, id) { + // Skip non-matching files + if (!filter_file(id)) { + return null; + } + + // Compute content hash + const hash = await compute_hash(code); + const existing_hash = hashes.get(id); + + // Check if unchanged + if (existing_hash === hash) { + return null; + } + + // Try cache (if not CI and we have cache dir) + if (!is_ci && resolved_cache_dir && project_root) { + const cache_path = await get_file_cache_path(id); + const cached = await load_cached_extraction(cache_path); + + if (cached?.content_hash === hash) { + // Cache hit + const {classes, explicit_classes, diagnostics} = from_cached_extraction(cached); + css_classes.add(id, classes, explicit_classes, diagnostics); + hashes.set(id, hash); + + if (virtual_module_loaded) { + invalidate_virtual_module(); + } + return null; + } + } + + // Extract classes + const result = extract_css_classes_with_locations(code, { + filename: id, + acorn_plugins, + }); + + // Log extraction diagnostics + if (result.diagnostics) { + for (const d of result.diagnostics) { + const loc = `${d.location.file}:${d.location.line}:${d.location.column}`; + const msg = `[fuz_css] ${loc}: ${d.message}`; + if (d.level === 'error') { + log_error(msg); + } else if (on_warning === 'log') { + log_warn(msg); + } + // 'ignore' and 'throw' (handled at generation time) - don't log here + } + } + + // Update CssClasses + css_classes.add(id, result.classes, result.explicit_classes, result.diagnostics); + hashes.set(id, hash); + + // Save to cache (fire and forget - don't block transform) + if (!is_ci && resolved_cache_dir && project_root) { + get_file_cache_path(id) + .then((cache_path) => + save_cached_extraction( + cache_path, + hash, + result.classes, + result.explicit_classes, + result.diagnostics, + ), + ) + .catch(() => { + // Ignore cache errors + }); + } + + // Trigger HMR if virtual module already loaded + if (virtual_module_loaded) { + invalidate_virtual_module(); + } + + return null; + }, + + // Note: handleHotUpdate not needed - transform hook handles file changes, + // and configureServer's watcher.on('unlink') handles file deletion + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b3d649d45..ce3a35856 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -28,7 +28,7 @@ - Fuz CSS - magical organic stylesheets + fuz_css - CSS with more utility @@ -40,7 +40,7 @@ -{#if typeof icon === 'string'}{icon}{:else}{@render icon()}{/if} {#if children}{@render children()}{:else}{final_path}{/if} diff --git a/src/routes/FontSizeControl.svelte b/src/routes/FontSizeControl.svelte index 4cf86f737..a525c9092 100644 --- a/src/routes/FontSizeControl.svelte +++ b/src/routes/FontSizeControl.svelte @@ -23,14 +23,14 @@