diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a886620 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,99 @@ +# Архитектурные правила + +## Изоляция modes + +Каждый полный mode key является независимым adapter со своим generated-контрактом: + +- `react@vite`; +- `react@webpack`; +- `next@app/turbopack`; +- `next@app/webpack`; +- `next@pages/turbopack`; +- `next@pages/webpack`; +- будущие `vue@*`, `standalone` и другие modes. + +Для каждого exact mode используется отдельный каталог `src/modes//`. Adapter самостоятельно определяет: + +- compile options; +- список и имена generated-файлов; +- runtime JavaScript; +- TypeScript declarations; +- manifest source; +- CSS; +- способ получения asset URL; +- mode-specific result metadata. + +Изменение output одного mode не должно менять output другого mode. Дублирование component, manifest, declaration и CSS codegen между adapters является сознательной ценой изоляции. + +Запрещено: + +- импортировать один `src/modes//` из другого mode; +- создавать общий framework/component/manifest/CSS codegen для нескольких modes; +- создавать общий runtime asset URL generator для нескольких modes; +- ветвиться по generic target внутри adapter; +- писать generated-файлы непосредственно из adapter через `fs`; +- импортировать exact-mode adapters из core. + +Единственное место, которое импортирует все adapters, — `src/mode-registry.ts`. + +## Общий core + +Общими могут быть только mode-neutral данные и инфраструктура: + +- загрузка, merge и валидация config; +- scanner исходных SVG; +- shape IDs и проверка конфликтов; +- низкоуровневый SVG compiler и transformations; +- подготовка нейтрального compiled artifact; +- protocol `ModeAdapter`/`OutputPlan`; +- проверка output paths; +- atomic writer, symlink protection и ownership state; +- logger и базовые result types. + +Core не генерирует JavaScript, declarations, manifest source, CSS или framework-specific exports. Изменение общего compiler может ожидаемо изменить SVG всех modes; изменение generated source должно быть локально одному adapter. + +## Generated-контракт + +Один config разрешается ровно в один mode и один output. Множественные modes не генерируются в один root; orchestration выполняется независимыми config/API/CLI вызовами. + +Runtime генерируется как ESM JavaScript. Типизация добавляется отдельными `.d.ts`; TypeScript/TSX не используется как runtime output. + +Core writer владеет корневым `.gitignore` и `.svg-sprite/state.json`. State не является manifest спрайта: он хранит owner mode, contract version и список управляемых файлов. + +Sprite-level asset, icon data, manifest и facade лежат непосредственно в `.svg-sprite/`. Framework runtime группируется отдельно: React adapters используют `.svg-sprite/react/`, будущие framework adapters получают собственный framework-каталог. + +Каждый adapter имеет собственный `contractVersion`. При изменении его generated-контракта повышается только версия этого adapter. + +Adapter возвращает файлы в памяти. Только core writer проверяет paths и markers, обновляет output и записывает `state.json` последним. + +## Зависимости + +Допустимое направление импортов: + +```text +public API / CLI + -> generate + -> mode-registry + -> один exact-mode adapter + -> core protocols and services +``` + +Обратные и горизонтальные зависимости запрещены: + +```text +core -X-> modes +mode A -X-> mode B +mode A -X-> shared output codegen +``` + +`src/react/` с `SpriteViewer` является отдельным runtime пакета, а не mode adapter. + +## Изменение mode + +При работе с adapter: + +1. Изменяйте только его каталог и mode-neutral protocol, если это действительно необходимо. +2. Не переносите output-логику в core ради устранения дублирования. +3. Не меняйте generated-контракты других adapters автоматически. +4. Повышайте `contractVersion` только изменённого adapter. +5. Проверяйте отсутствие cross-mode imports. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index b630bee..708d9bd 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,6 +1,6 @@ # Third-Party Notices -The distributed React entry and preview template include code from the projects listed below. +The distributed React entry includes code from the projects listed below. ## React, React DOM and Scheduler @@ -49,27 +49,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -## clsx - -Copyright (c) Luke Edwards (lukeed.com) - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/package.json b/package.json index 8dbac83..c9b0dae 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "dist/types-*.d.ts", "dist/chunk-*.js", "dist/chunk-*.js.map", - "dist/preview-template.html", "README_RU.md", "docs/en/*.md", "docs/ru/*.md", @@ -39,14 +38,13 @@ "THIRD_PARTY_NOTICES.md" ], "scripts": { - "build": "tsup && npm run build:preview", - "build:preview": "npm ci --prefix preview && npm run build --prefix preview", + "build": "npm run build:package", + "build:package": "tsup", "build:skill": "node skills/svg-sprites/build.mjs", "check:skill": "node skills/svg-sprites/build.mjs --check", "dev": "tsup --watch", "test": "tsup && node --test test/*.test.mjs", "typecheck": "tsc --noEmit", - "sprite": "node dist/cli.js --mode legacy .", "verify": "npm run check:skill && npm run typecheck && npm test", "prepack": "npm run verify && npm run build" }, diff --git a/preview/.gitignore b/preview/.gitignore deleted file mode 100644 index fb1b64a..0000000 --- a/preview/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local -public/dev-data.js - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/preview/eslint.config.js b/preview/eslint.config.js deleted file mode 100644 index 5e6b472..0000000 --- a/preview/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/preview/index.html b/preview/index.html deleted file mode 100644 index ef0ad56..0000000 --- a/preview/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - SVG Sprites Preview - - - -
- - - diff --git a/preview/package-lock.json b/preview/package-lock.json deleted file mode 100644 index e8f21dd..0000000 --- a/preview/package-lock.json +++ /dev/null @@ -1,3116 +0,0 @@ -{ - "name": "preview", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "preview", - "version": "0.0.0", - "dependencies": { - "clsx": "^2.1.1", - "react": "^19.2.5", - "react-colorful": "^5.6.1", - "react-dom": "^19.2.5" - }, - "devDependencies": { - "@eslint/js": "^9.39.4", - "@types/node": "^24.12.2", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.5.0", - "typescript": "~6.0.2", - "typescript-eslint": "^8.58.2", - "vite": "^8.0.9", - "vite-plugin-singlefile": "^2.3.3" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@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.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "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/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "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/@typescript-eslint/eslint-plugin": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", - "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/type-utils": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", - "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", - "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.0", - "@typescript-eslint/types": "^8.59.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", - "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", - "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", - "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", - "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", - "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.0", - "@typescript-eslint/tsconfig-utils": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", - "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", - "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "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/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.42", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", - "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", - "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", - "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.10.42", - "caniuse-lite": "^1.0.30001800", - "electron-to-chromium": "^1.5.387", - "node-releases": "^2.0.50", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001803", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", - "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", - "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/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "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/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "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/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "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/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.389", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", - "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", - "dev": true, - "license": "ISC" - }, - "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/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", - "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "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/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "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/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "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/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "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/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "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/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "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/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "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/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", - "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.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-colorful": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", - "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.5" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "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/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", - "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.0", - "@typescript-eslint/parser": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "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/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", - "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.16", - "rolldown": "~1.1.4", - "tinyglobby": "^0.2.17" - }, - "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", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-plugin-singlefile": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", - "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">18.0.0" - }, - "peerDependencies": { - "rollup": "^4.59.0", - "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} diff --git a/preview/package.json b/preview/package.json deleted file mode 100644 index c2c9834..0000000 --- a/preview/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "preview", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev:prepare": "node scripts/generate-dev-data.js", - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "clsx": "^2.1.1", - "react": "^19.2.5", - "react-colorful": "^5.6.1", - "react-dom": "^19.2.5" - }, - "devDependencies": { - "@eslint/js": "^9.39.4", - "@types/node": "^24.12.2", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.5.0", - "typescript": "~6.0.2", - "typescript-eslint": "^8.58.2", - "vite": "^8.0.9", - "vite-plugin-singlefile": "^2.3.3" - } -} diff --git a/preview/scripts/generate-dev-data.js b/preview/scripts/generate-dev-data.js deleted file mode 100644 index c6fea13..0000000 --- a/preview/scripts/generate-dev-data.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Генерирует dev-данные для preview из реальных спрайтов основного пакета. - * - * Запуск: node scripts/generate-dev-data.js - * - * Результат: - * public/dev-data.js — window.__SPRITES_DATA__ с метаданными иконок - * public/dev-sprites.svg — инлайновые из всех спрайтов - */ -import fs from 'node:fs' -import path from 'node:path' - -const ROOT = path.resolve(import.meta.dirname, '../..') -const SPRITES_OUTPUT = path.join(ROOT, 'preview/public') -const PREVIEW_PUBLIC = path.join(import.meta.dirname, '../public') - -/** Извлекает id иконок из SVG-спрайта. */ -function extractIconIds(spritePath) { - const content = fs.readFileSync(spritePath, 'utf-8') - const ids = [] - const regex = /<(?:svg|symbol)\b[^>]*\bid="([^"]+)"/g - let match - while ((match = regex.exec(content)) !== null) { - ids.push(match[1]) - } - return ids.sort() -} - -/** Извлекает viewBox из SVG-фрагмента иконки. */ -function extractViewBox(svgFragment) { - const match = svgFragment.match(/viewBox="([^"]+)"/) - if (!match) return null - const parts = match[1].split(/\s+/).map(Number) - if (parts.length !== 4) return null - return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] } -} - -/** Извлекает CSS-переменные из SVG-фрагмента иконки. */ -function extractIconVars(svgFragment) { - const vars = new Map() - const regex = /var\((--icon-color-\d+),\s*([^)]+)\)/g - let match - while ((match = regex.exec(svgFragment)) !== null) { - if (!vars.has(match[1])) { - vars.set(match[1], match[2].trim()) - } - } - return [...vars.entries()].map(([varName, fallback]) => ({ - varName, - fallback, - hex: colorToHex(fallback), - isCurrentColor: fallback.toLowerCase() === 'currentcolor', - })) -} - -/** Извлекает фрагменты иконок из спрайта. */ -function extractIconFragments(spritePath) { - const content = fs.readFileSync(spritePath, 'utf-8') - const fragments = new Map() - const regex = /<(?:svg|symbol)\b[^>]*\bid="([^"]+)"[^>]*>[\s\S]*?<\/(?:svg|symbol)>/g - let match - while ((match = regex.exec(content)) !== null) { - fragments.set(match[1], match[0]) - } - return fragments -} - -/** Конвертирует CSS-цвет в hex. */ -function colorToHex(color) { - const named = { - red: '#ff0000', blue: '#0000ff', green: '#008000', white: '#ffffff', - black: '#000000', yellow: '#ffff00', cyan: '#00ffff', magenta: '#ff00ff', - orange: '#ffa500', purple: '#800080', pink: '#ffc0cb', gray: '#808080', - grey: '#808080', currentcolor: '#000000', - } - const lower = color.toLowerCase().trim() - if (lower.startsWith('#')) { - if (lower.length === 4) return `#${lower[1]}${lower[1]}${lower[2]}${lower[2]}${lower[3]}${lower[3]}` - return lower - } - return named[lower] || '#000000' -} - -/** Подготавливает спрайт для инлайна — вложенные . */ -function prepareInlineSprite(spritePath) { - let content = fs.readFileSync(spritePath, 'utf-8') - content = content.replace(/<\?xml[^?]*\?>\s*/g, '') - content = content.replace(/' : '' - } - depth++ - if (depth > 1) { - const cleanAttrs = attrs.replace(/\s*xmlns="[^"]*"/g, '') - return `` - } - return `` - }) - return content -} - -// --- Main --- -const spriteFiles = fs.readdirSync(SPRITES_OUTPUT).filter((entry) => { - return entry.endsWith('.sprite.svg') -}) - -const groups = [] -const inlineSprites = [] - -for (const fileName of spriteFiles) { - const spritePath = path.join(SPRITES_OUTPUT, fileName) - const name = fileName.replace('.sprite.svg', '') - - const fragments = extractIconFragments(spritePath) - const ids = extractIconIds(spritePath) - - const icons = ids.map((id) => { - const fragment = fragments.get(id) || '' - return { - id, - group: name, - mode: 'stack', - spriteFile: fileName, - viewBox: extractViewBox(fragment), - vars: extractIconVars(fragment), - } - }) - - groups.push({ name, mode: 'stack', spriteFile: fileName, icons }) - inlineSprites.push(prepareInlineSprite(spritePath)) -} - -// Write dev-data.js — данные + инлайн-спрайты через DOM injection -fs.mkdirSync(PREVIEW_PUBLIC, { recursive: true }) - -const svgContent = inlineSprites.join('\n').replace(/`/g, '\\`').replace(/\$/g, '\\$') - -const dataJs = [ - `window.__SPRITES_DATA__ = ${JSON.stringify({ groups }, null, 2)};`, - '', - '// Inject inline SVG sprites into DOM via DOMParser for correct SVG namespace', - '(function() {', - ` var svg = \`${svgContent}\`;`, - ' var parser = new DOMParser();', - ' var doc = parser.parseFromString("
" + svg + "
", "text/html");', - ' var nodes = doc.body.firstChild.childNodes;', - ' while (nodes.length > 0) {', - ' document.body.insertBefore(nodes[0], document.body.firstChild);', - ' }', - '})();', -].join('\n') - -fs.writeFileSync(path.join(PREVIEW_PUBLIC, 'dev-data.js'), dataJs) - -// Cleanup old separate file if exists -const oldSvg = path.join(PREVIEW_PUBLIC, 'dev-sprites.svg') -if (fs.existsSync(oldSvg)) fs.unlinkSync(oldSvg) - -console.log(`Generated dev data: ${groups.length} groups, ${groups.reduce((s, g) => s + g.icons.length, 0)} icons`) diff --git a/preview/src/App.tsx b/preview/src/App.tsx deleted file mode 100644 index 4b47c09..0000000 --- a/preview/src/App.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useState, useMemo } from 'react' -import { useTheme } from './infrastructure/theme' -import { Banner } from './ui/banner' -import { SearchInput } from './ui/search-input' -import { IconGrid } from './ui/icon-grid' -import { IconModal } from './ui/icon-modal' -import type { SpritesData, IconData } from './shared/types' -import styles from './app/styles/app.module.css' - -declare global { - interface Window { - __SPRITES_DATA__?: SpritesData - } -} - -const MOCK_DATA: SpritesData = { - groups: [], -} - -/** - * Корневой компонент превью SVG-спрайтов. - * - * Используется для: - * - отображения всех иконок из сгенерированных спрайтов - * - просмотра деталей иконки и кода использования - */ -export const App = () => { - const { toggle } = useTheme() - const data = window.__SPRITES_DATA__ ?? MOCK_DATA - const [searchQuery, setSearchQuery] = useState('') - const [selectedIconId, setSelectedIconId] = useState(null) - - const isFileProtocol = location.protocol === 'file:' - - const totalIcons = useMemo( - () => data.groups.reduce((sum, g) => sum + g.icons.length, 0), - [data.groups], - ) - - const selectedIcon = useMemo((): IconData | null => { - if (!selectedIconId) { - return null - } - - for (const group of data.groups) { - const found = group.icons.find((i) => i.id === selectedIconId) - if (found) { - return found - } - } - - return null - }, [selectedIconId, data.groups]) - - const handleCloseModal = (): void => { - setSelectedIconId(null) - } - - return ( - <> - {isFileProtocol && ( - - file:// — Preview opened from local file. External SVG - references won't work in code snippets. Use a local server for full - functionality. - - )} - -
-

SVG Sprites

- {totalIcons} icons -
- - -
-
- - - - - - - - ) -} diff --git a/preview/src/app/styles/app.module.css b/preview/src/app/styles/app.module.css deleted file mode 100644 index 70c2570..0000000 --- a/preview/src/app/styles/app.module.css +++ /dev/null @@ -1,64 +0,0 @@ -.header { - display: flex; - align-items: center; - gap: var(--space-4); - flex-wrap: wrap; - margin-bottom: var(--space-6); -} - -.title { - font-size: 1.5rem; - font-weight: 700; -} - -.count { - font-size: 13px; - color: var(--color-muted); - font-weight: 400; -} - -.toolbar { - display: flex; - gap: var(--space-3); - margin-left: auto; - align-items: center; -} - -.themeButton { - padding: var(--space-2) var(--space-3); - border: 1px solid var(--color-border); - border-radius: var(--radius-2); - background: var(--color-card-bg); - color: var(--color-fg); - cursor: pointer; - font-size: 14px; -} - -.themeButton:hover { - background: var(--color-card-hover); -} - -.footer { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-3); - margin-top: auto; - padding-top: var(--space-4); - border-top: 1px solid var(--color-border); - color: var(--color-muted); - font-size: 13px; -} - -.footerText { - font-weight: 500; -} - -.footerLink { - color: var(--color-accent); - text-decoration: none; -} - -.footerLink:hover { - text-decoration: underline; -} diff --git a/preview/src/app/styles/globals.css b/preview/src/app/styles/globals.css deleted file mode 100644 index dac9728..0000000 --- a/preview/src/app/styles/globals.css +++ /dev/null @@ -1,26 +0,0 @@ -@import './variables.css'; - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: var(--color-bg); - color: var(--color-fg); - padding: var(--space-6); - max-width: 1400px; - margin: 0 auto; - min-height: 100dvh; - display: flex; - flex-direction: column; -} - -#root { - flex: 1; - display: flex; - flex-direction: column; -} diff --git a/preview/src/app/styles/variables.css b/preview/src/app/styles/variables.css deleted file mode 100644 index d9adaa7..0000000 --- a/preview/src/app/styles/variables.css +++ /dev/null @@ -1,60 +0,0 @@ -:root { - --color-bg: #f0f0f3; - --color-fg: #1a1a1a; - --color-card-bg: #ffffff; - --color-card-hover: #eaeaed; - --color-border: #d8d8d8; - --color-accent: #3b82f6; - --color-muted: #888888; - --color-code-bg: #f5f5f5; - - --radius-1: 4px; - --radius-2: 8px; - --radius-3: 12px; - - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; - --space-8: 32px; - --space-10: 40px; - - --icon-size: 128px; - - --checkerboard: conic-gradient( - var(--color-checker-a) 25%, var(--color-checker-b) 0 50%, - var(--color-checker-a) 0 75%, var(--color-checker-b) 0 - ); - --checkerboard-size: 8px 8px; - --color-checker-a: #e9e9e9; - --color-checker-b: #ffffff; - - --font-mono: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace; -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme='light']) { - --color-bg: #1a1a1a; - --color-fg: #e5e5e5; - --color-card-bg: #2a2a2a; - --color-card-hover: #333333; - --color-border: #404040; - --color-code-bg: #2a2a2a; - --color-checker-a: #333333; - --color-checker-b: #2a2a2a; - } -} - -:root[data-theme='dark'] { - --color-bg: #1a1a1a; - --color-fg: #e5e5e5; - --color-card-bg: #2a2a2a; - --color-card-hover: #333333; - --color-border: #404040; - --color-code-bg: #2a2a2a; - --color-checker-a: #333333; - --color-checker-b: #2a2a2a; -} - diff --git a/preview/src/infrastructure/theme/hooks/use-theme.hook.ts b/preview/src/infrastructure/theme/hooks/use-theme.hook.ts deleted file mode 100644 index f4e95c7..0000000 --- a/preview/src/infrastructure/theme/hooks/use-theme.hook.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useState, useCallback, useEffect } from 'react' -import type { Theme } from '../types/theme.type' - -const getSystemTheme = (): Theme => { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' -} - -/** - * Управляет темой приложения: авто-определение по системе + ручное переключение. - */ -export const useTheme = () => { - const [theme, setTheme] = useState(getSystemTheme) - - const toggle = useCallback(() => { - setTheme((prev) => { - const next: Theme = prev === 'dark' ? 'light' : 'dark' - document.documentElement.dataset.theme = next - return next - }) - }, []) - - useEffect(() => { - // Устанавливаем data-theme при инициализации, чтобы CSS-селекторы - // (включая подсветку кода) работали сразу, а не только после ручного переключения. - document.documentElement.dataset.theme = getSystemTheme() - - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') - - const handleChange = (): void => { - const next = getSystemTheme() - document.documentElement.dataset.theme = next - setTheme(next) - } - - mediaQuery.addEventListener('change', handleChange) - return () => mediaQuery.removeEventListener('change', handleChange) - }, []) - - return { theme, toggle } -} diff --git a/preview/src/infrastructure/theme/index.ts b/preview/src/infrastructure/theme/index.ts deleted file mode 100644 index 0f8c14f..0000000 --- a/preview/src/infrastructure/theme/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useTheme } from './hooks/use-theme.hook' -export type { Theme } from './types/theme.type' diff --git a/preview/src/infrastructure/theme/styles/theme.module.css b/preview/src/infrastructure/theme/styles/theme.module.css deleted file mode 100644 index c3a2af6..0000000 --- a/preview/src/infrastructure/theme/styles/theme.module.css +++ /dev/null @@ -1,2 +0,0 @@ -.root { -} diff --git a/preview/src/infrastructure/theme/theme.infra.tsx b/preview/src/infrastructure/theme/theme.infra.tsx deleted file mode 100644 index c98a234..0000000 --- a/preview/src/infrastructure/theme/theme.infra.tsx +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Theme infrastructure module — реэкспортирует хук. - * Компонент не требуется, тема управляется через data-theme на documentElement. - */ diff --git a/preview/src/infrastructure/theme/types/theme.type.ts b/preview/src/infrastructure/theme/types/theme.type.ts deleted file mode 100644 index 7afba61..0000000 --- a/preview/src/infrastructure/theme/types/theme.type.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Тема приложения. - */ -export type Theme = 'light' | 'dark' - -/** - * Параметры Theme. - */ -export type ThemeParams = {} - -export type ThemeProps = ThemeParams diff --git a/preview/src/main.tsx b/preview/src/main.tsx deleted file mode 100644 index 17af05b..0000000 --- a/preview/src/main.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './app/styles/globals.css' -import { App } from './App' - -function loadDevScript(): Promise { - return new Promise((resolve, reject) => { - const script = document.createElement('script') - script.src = '/dev-data.js' - script.onload = () => resolve() - script.onerror = reject - document.head.appendChild(script) - }) -} - -async function loadDevData(): Promise { - if (import.meta.env.DEV && !window.__SPRITES_DATA__) { - await loadDevScript() - } -} - -loadDevData().then(() => { - createRoot(document.getElementById('root')!).render( - - - , - ) -}) diff --git a/preview/src/shared/lib/rgb-to-hex.util.ts b/preview/src/shared/lib/rgb-to-hex.util.ts deleted file mode 100644 index 3204eb7..0000000 --- a/preview/src/shared/lib/rgb-to-hex.util.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Конвертирует CSS-цвет (rgb или hex) в hex-формат. - */ -export const rgbToHex = (color: string): string => { - if (color.startsWith('#')) { - return color - } - - const match = color.match(/\d+/g) - - if (match && match.length >= 3) { - return '#' + match.slice(0, 3).map((c) => parseInt(c).toString(16).padStart(2, '0')).join('') - } - - return '#000000' -} diff --git a/preview/src/shared/types/index.ts b/preview/src/shared/types/index.ts deleted file mode 100644 index da40077..0000000 --- a/preview/src/shared/types/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type { - IconVar, - IconData, - SpriteGroup, - SpritesData, -} from './sprites-data.type' diff --git a/preview/src/shared/types/sprites-data.type.ts b/preview/src/shared/types/sprites-data.type.ts deleted file mode 100644 index 3147dbe..0000000 --- a/preview/src/shared/types/sprites-data.type.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * CSS-переменная иконки. - */ -export type IconVar = { - /** Имя CSS-переменной. */ - varName: string - /** Fallback-значение. */ - fallback: string - /** HEX-значение для color picker. */ - hex: string - /** Является ли fallback значением currentColor. */ - isCurrentColor: boolean -} - -/** - * Размеры viewBox иконки. - */ -export type IconViewBox = { - x: number - y: number - width: number - height: number -} - -/** - * Данные одной иконки. - */ -export type IconData = { - /** Идентификатор иконки. */ - id: string - /** Имя группы (папки спрайта). */ - group: string - /** Режим спрайта. */ - mode: 'stack' | 'symbol' - /** Относительный путь к файлу спрайта. */ - spriteFile: string - /** Размеры viewBox иконки. */ - viewBox: IconViewBox | null - /** CSS-переменные иконки. */ - vars: IconVar[] -} - -/** - * Группа спрайтов. - */ -export type SpriteGroup = { - /** Имя группы. */ - name: string - /** Режим спрайта. */ - mode: 'stack' | 'symbol' - /** Относительный путь к файлу спрайта. */ - spriteFile: string - /** Иконки в группе. */ - icons: IconData[] -} - -/** - * Данные для рендера превью. - */ -export type SpritesData = { - /** Группы спрайтов. */ - groups: SpriteGroup[] -} diff --git a/preview/src/ui/banner/banner.ui.tsx b/preview/src/ui/banner/banner.ui.tsx deleted file mode 100644 index b8b4634..0000000 --- a/preview/src/ui/banner/banner.ui.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import cl from 'clsx' -import type { BannerProps } from './types/banner.type' -import styles from './styles/banner.module.css' - -/** - * Баннер-уведомление. - * - * Используется для: - * - предупреждений о file:// протоколе - * - информационных сообщений - */ -export const Banner = (props: BannerProps) => { - const { children, className, variant = 'warn', ...htmlAttr } = props - - return ( -
- {children} -
- ) -} diff --git a/preview/src/ui/banner/index.ts b/preview/src/ui/banner/index.ts deleted file mode 100644 index 4500a84..0000000 --- a/preview/src/ui/banner/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Banner } from './banner.ui' diff --git a/preview/src/ui/banner/styles/banner.module.css b/preview/src/ui/banner/styles/banner.module.css deleted file mode 100644 index 3e99433..0000000 --- a/preview/src/ui/banner/styles/banner.module.css +++ /dev/null @@ -1,29 +0,0 @@ -.root { - display: flex; - align-items: center; - gap: var(--space-2); - padding: 10px var(--space-4); - border-radius: var(--radius-2); - font-size: 13px; - margin-bottom: var(--space-5); -} - -._warn { - background: #fef3c7; - color: #92400e; - border: 1px solid #fde68a; -} - -:root[data-theme='dark'] ._warn { - background: #451a03; - color: #fde68a; - border-color: #92400e; -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme='light']) ._warn { - background: #451a03; - color: #fde68a; - border-color: #92400e; - } -} diff --git a/preview/src/ui/banner/types/banner.type.ts b/preview/src/ui/banner/types/banner.type.ts deleted file mode 100644 index 0e86e7a..0000000 --- a/preview/src/ui/banner/types/banner.type.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { HTMLAttributes } from 'react' - -/** - * Параметры Banner. - */ -export type BannerParams = { - /** Вариант отображения. */ - variant?: 'warn' | 'info' -} - -/** HTML-атрибуты корневого элемента. */ -type RootAttrs = HTMLAttributes - -export type BannerProps = RootAttrs & BannerParams diff --git a/preview/src/ui/code-block/code-block.ui.tsx b/preview/src/ui/code-block/code-block.ui.tsx deleted file mode 100644 index 27bb0e5..0000000 --- a/preview/src/ui/code-block/code-block.ui.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useState, useMemo } from 'react' -import cl from 'clsx' -import { highlight } from './lib/highlight.lib' -import type { CodeBlockProps } from './types/code-block.type' -import styles from './styles/code-block.module.css' - -export const CodeBlock = (props: CodeBlockProps) => { - const { code, language = 'html', copyable = true, className, ...htmlAttr } = props - - const [isCopied, setIsCopied] = useState(false) - - const highlightedHtml = useMemo(() => highlight(code, language), [code, language]) - - const handleCopy = (): void => { - navigator.clipboard.writeText(code).then(() => { - setIsCopied(true) - setTimeout(() => setIsCopied(false), 1500) - }) - } - - return ( -
-
-        
-      
- {copyable && ( - - )} -
- ) -} diff --git a/preview/src/ui/code-block/index.ts b/preview/src/ui/code-block/index.ts deleted file mode 100644 index 91aec98..0000000 --- a/preview/src/ui/code-block/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CodeBlock } from './code-block.ui' diff --git a/preview/src/ui/code-block/lib/highlight.lib.ts b/preview/src/ui/code-block/lib/highlight.lib.ts deleted file mode 100644 index 73c2031..0000000 --- a/preview/src/ui/code-block/lib/highlight.lib.ts +++ /dev/null @@ -1,68 +0,0 @@ -const ESCAPE_RE = /[&<>"]/g -const ESCAPE_MAP: Record = { '&': '&', '<': '<', '>': '>', '"': '"' } - -const escape = (str: string): string => str.replace(ESCAPE_RE, (c) => ESCAPE_MAP[c]) - -type Token = { kind: string; match: string } -type Rule = [RegExp, string] - -const HTML_RULES: Rule[] = [ - [//g, 'comment'], - [/<\/?[\w-]+/g, 'tag'], - [/<\/?/g, 'punctuation'], - [/\/?>/g, 'punctuation'], - [/[\w-]+(?=\s*=)/g, 'attr'], - [/=/g, 'punctuation'], - [/"[^"]*"|'[^']*'/g, 'string'], -] - -const CSS_RULES: Rule[] = [ - [/\/\*[\s\S]*?\*\//g, 'comment'], - [/\.[\w-]+/g, 'selector'], - [/[\w-]+(?=\s*:)/g, 'property'], - [/[:;]/g, 'punctuation'], - [/\{|\}/g, 'punctuation'], - [/'[^']*'|"[^"]*"/g, 'string'], - [/#[\da-fA-F]{3,8}\b/g, 'color'], - [/\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|deg|s|ms)?/g, 'number'], - [/url\([^)]*\)/g, 'function'], -] - -const RULES: Record = { html: HTML_RULES, css: CSS_RULES, xml: HTML_RULES } - -const tokenize = (code: string, lang: string): Token[] => { - const rules = RULES[lang] ?? [] - const tokens: Token[] = [] - let pos = 0 - - while (pos < code.length) { - let matched = false - for (const [re, kind] of rules) { - re.lastIndex = pos - const m = re.exec(code) - if (m && m.index === pos) { - tokens.push({ kind, match: m[0] }) - pos += m[0].length - matched = true - break - } - } - if (!matched) { - const last = tokens[tokens.length - 1] - const ch = code[pos] - if (last && last.kind === 'plain') { - last.match += ch - } else { - tokens.push({ kind: 'plain', match: ch }) - } - pos++ - } - } - - return tokens -} - -export const highlight = (code: string, lang: string): string => - tokenize(code, lang) - .map((t) => (t.kind === 'plain' ? escape(t.match) : `${escape(t.match)}`)) - .join('') diff --git a/preview/src/ui/code-block/styles/code-block.module.css b/preview/src/ui/code-block/styles/code-block.module.css deleted file mode 100644 index 51b5f56..0000000 --- a/preview/src/ui/code-block/styles/code-block.module.css +++ /dev/null @@ -1,85 +0,0 @@ -.root { - position: relative; - border-radius: var(--radius-2); - overflow: hidden; - background: var(--color-code-bg); -} - -.pre { - margin: 0; - padding: var(--space-4); - overflow-x: auto; - font-size: 12px; - font-family: var(--font-mono); - line-height: 1.6; -} - -.code { - font-family: var(--font-mono); -} - -:global(:root[data-theme='light']) .code { - --hl-tag: #116329; - --hl-attr: #0550ae; - --hl-string: #0a3069; - --hl-comment: #8b949e; - --hl-punctuation: #6639ba; - --hl-selector: #6639ba; - --hl-property: #0550ae; - --hl-color: #0a3069; - --hl-number: #0550ae; - --hl-function: #0550ae; -} - -:global(:root[data-theme='dark']) .code { - --hl-tag: #7ee787; - --hl-attr: #79c0ff; - --hl-string: #a5d6ff; - --hl-comment: #8b949e; - --hl-punctuation: #d2a8ff; - --hl-selector: #d2a8ff; - --hl-property: #79c0ff; - --hl-color: #a5d6ff; - --hl-number: #79c0ff; - --hl-function: #d2a8ff; -} - -.code :global(.hl-tag), -.code :global(.hl-attr), -.code :global(.hl-string), -.code :global(.hl-comment), -.code :global(.hl-punctuation), -.code :global(.hl-selector), -.code :global(.hl-property), -.code :global(.hl-color), -.code :global(.hl-number), -.code :global(.hl-function) { - color: var(--hl-tag); -} - -.code :global(.hl-attr) { color: var(--hl-attr); } -.code :global(.hl-string) { color: var(--hl-string); } -.code :global(.hl-comment) { color: var(--hl-comment); } -.code :global(.hl-punctuation) { color: var(--hl-punctuation); } -.code :global(.hl-selector) { color: var(--hl-selector); } -.code :global(.hl-property) { color: var(--hl-property); } -.code :global(.hl-color) { color: var(--hl-color); } -.code :global(.hl-number) { color: var(--hl-number); } -.code :global(.hl-function) { color: var(--hl-function); } - -.copyButton { - position: absolute; - top: var(--space-2); - right: var(--space-2); - padding: var(--space-1) var(--space-2); - font-size: 11px; - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: var(--radius-1); - color: var(--color-muted); - cursor: pointer; -} - -.copyButton:hover { - color: var(--color-fg); -} diff --git a/preview/src/ui/code-block/types/code-block.type.ts b/preview/src/ui/code-block/types/code-block.type.ts deleted file mode 100644 index cbfcf41..0000000 --- a/preview/src/ui/code-block/types/code-block.type.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { HTMLAttributes } from 'react' - -/** - * Параметры CodeBlock. - */ -export type CodeBlockParams = { - /** Код для отображения. */ - code: string - /** Язык подсветки синтаксиса. */ - language?: 'html' | 'css' | 'xml' - /** Показывать кнопку копирования. */ - copyable?: boolean -} - -/** HTML-атрибуты корневого элемента. */ -type RootAttrs = HTMLAttributes - -export type CodeBlockProps = RootAttrs & CodeBlockParams diff --git a/preview/src/ui/color-picker/color-picker.ui.tsx b/preview/src/ui/color-picker/color-picker.ui.tsx deleted file mode 100644 index 7c6f411..0000000 --- a/preview/src/ui/color-picker/color-picker.ui.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useState, useRef, useEffect, useCallback } from 'react' -import { HexColorPicker } from 'react-colorful' -import cl from 'clsx' -import type { ColorPickerProps } from './types/color-picker.type' -import styles from './styles/color-picker.module.css' - -/** - * Выбор цвета с попапом react-colorful и HEX-инпутом. - * - * Используется для: - * - настройки CSS-переменных цвета иконок - * - визуального подбора цвета в модалке - */ -export const ColorPicker = (props: ColorPickerProps) => { - const { value, onValueChange, label, className, ...htmlAttr } = props - - const [isOpen, setIsOpen] = useState(false) - const popoverRef = useRef(null) - const swatchRef = useRef(null) - - const handleToggle = useCallback(() => { - setIsOpen((prev) => !prev) - }, []) - - useEffect(() => { - if (!isOpen) return - - const handleClickOutside = (e: MouseEvent): void => { - const target = e.target as Node - - if ( - popoverRef.current && !popoverRef.current.contains(target) && - swatchRef.current && !swatchRef.current.contains(target) - ) { - setIsOpen(false) - } - } - - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, [isOpen]) - - const handleHexInput = (e: React.ChangeEvent): void => { - const hex = e.target.value - - if (/^#[0-9a-fA-F]{6}$/.test(hex)) { - onValueChange(hex) - } - } - - return ( -
-
- ) -} diff --git a/preview/src/ui/color-picker/index.ts b/preview/src/ui/color-picker/index.ts deleted file mode 100644 index ff2b65f..0000000 --- a/preview/src/ui/color-picker/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ColorPicker } from './color-picker.ui' diff --git a/preview/src/ui/color-picker/styles/color-picker.module.css b/preview/src/ui/color-picker/styles/color-picker.module.css deleted file mode 100644 index 6d9682b..0000000 --- a/preview/src/ui/color-picker/styles/color-picker.module.css +++ /dev/null @@ -1,53 +0,0 @@ -.root { - position: relative; - display: inline-flex; - align-items: center; - gap: var(--space-2); -} - -.swatch { - width: 24px; - height: 24px; - border-radius: var(--radius-1); - border: 1px solid var(--color-border); - cursor: pointer; - padding: 0; - background: none; -} - -.label { - font-size: 12px; - color: var(--color-muted); - font-family: var(--font-mono); -} - -.popover { - position: absolute; - top: calc(100% + var(--space-2)); - left: 0; - z-index: 200; - padding: var(--space-3); - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: var(--radius-2); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); -} - -.hexInput { - display: block; - width: 100%; - margin-top: var(--space-2); - padding: var(--space-1) var(--space-2); - font-size: 12px; - font-family: var(--font-mono); - color: var(--color-fg); - background: var(--color-card-bg); - border: 1px solid var(--color-border); - border-radius: var(--radius-1); - outline: none; - text-align: center; -} - -.hexInput:focus { - border-color: var(--color-accent); -} diff --git a/preview/src/ui/color-picker/types/color-picker.type.ts b/preview/src/ui/color-picker/types/color-picker.type.ts deleted file mode 100644 index e65f1e6..0000000 --- a/preview/src/ui/color-picker/types/color-picker.type.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { HTMLAttributes } from 'react' - -/** - * Параметры ColorPicker. - */ -export type ColorPickerParams = { - /** Текущий цвет в HEX. */ - value: string - /** Обработчик изменения цвета. */ - onValueChange: (color: string) => void - /** Подпись под picker-ом. */ - label?: string -} - -/** HTML-атрибуты корневого элемента. */ -type RootAttrs = Omit, 'onChange'> - -export type ColorPickerProps = RootAttrs & ColorPickerParams diff --git a/preview/src/ui/icon-card/icon-card.ui.tsx b/preview/src/ui/icon-card/icon-card.ui.tsx deleted file mode 100644 index 01c86d0..0000000 --- a/preview/src/ui/icon-card/icon-card.ui.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import cl from 'clsx' -import type { IconCardProps } from './types/icon-card.type' -import styles from './styles/icon-card.module.css' - -/** - * Карточка иконки в сетке превью. - * - * Используется для: - * - отображения иконки из спрайта с именем - * - открытия модалки деталей по клику - */ -export const IconCard = (props: IconCardProps) => { - const { iconId, onSelect, className, ...htmlAttr } = props - - const handleClick = (): void => { - onSelect?.(iconId) - } - - return ( -
-
- - - -
- {iconId} -
- ) -} diff --git a/preview/src/ui/icon-card/index.ts b/preview/src/ui/icon-card/index.ts deleted file mode 100644 index d0dce9d..0000000 --- a/preview/src/ui/icon-card/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { IconCard } from './icon-card.ui' diff --git a/preview/src/ui/icon-card/styles/icon-card.module.css b/preview/src/ui/icon-card/styles/icon-card.module.css deleted file mode 100644 index f3625c3..0000000 --- a/preview/src/ui/icon-card/styles/icon-card.module.css +++ /dev/null @@ -1,37 +0,0 @@ -.root { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-2); - padding: var(--space-4) var(--space-2); - border-radius: var(--radius-2); - background: var(--color-card-bg); - cursor: pointer; - transition: background 0.15s; -} - -.root:hover { - background: var(--color-card-hover); -} - -.iconWrap { - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-1); - background: var(--checkerboard); - background-size: var(--checkerboard-size); -} - -.icon { - width: var(--icon-size); - height: var(--icon-size); - color: var(--color-fg); -} - -.name { - font-size: 13px; - color: var(--color-muted); - text-align: center; - word-break: break-all; -} diff --git a/preview/src/ui/icon-card/types/icon-card.type.ts b/preview/src/ui/icon-card/types/icon-card.type.ts deleted file mode 100644 index f4148e7..0000000 --- a/preview/src/ui/icon-card/types/icon-card.type.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { HTMLAttributes } from 'react' - -/** - * Параметры IconCard. - */ -export type IconCardParams = { - /** Идентификатор иконки. */ - iconId: string - /** Обработчик клика по карточке. */ - onSelect?: (iconId: string) => void -} - -/** HTML-атрибуты корневого элемента. */ -type RootAttrs = Omit, 'onSelect'> - -export type IconCardProps = RootAttrs & IconCardParams diff --git a/preview/src/ui/icon-grid/icon-grid.ui.tsx b/preview/src/ui/icon-grid/icon-grid.ui.tsx deleted file mode 100644 index 0bf99d2..0000000 --- a/preview/src/ui/icon-grid/icon-grid.ui.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import cl from 'clsx' -import { IconCard } from '../icon-card' -import type { IconGridProps } from './types/icon-grid.type' -import styles from './styles/icon-grid.module.css' - -/** - * Сетка иконок, сгруппированная по спрайтам. - * - * Используется для: - * - отображения всех иконок из всех спрайтов - * - фильтрации иконок по поисковому запросу - */ -export const IconGrid = (props: IconGridProps) => { - const { groups, searchQuery = '', onIconSelect, className, ...htmlAttr } = props - - const query = searchQuery.toLowerCase() - - return ( -
- {groups.map((group) => { - const filteredIcons = query - ? group.icons.filter((icon) => icon.id.includes(query)) - : group.icons - - const isGroupHidden = filteredIcons.length === 0 - - return ( -
-

- {group.name} - {group.mode} - {group.icons.length} -

-
- {filteredIcons.map((icon) => ( - - ))} -
-
- ) - })} -
- ) -} diff --git a/preview/src/ui/icon-grid/index.ts b/preview/src/ui/icon-grid/index.ts deleted file mode 100644 index 4222794..0000000 --- a/preview/src/ui/icon-grid/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { IconGrid } from './icon-grid.ui' diff --git a/preview/src/ui/icon-grid/styles/icon-grid.module.css b/preview/src/ui/icon-grid/styles/icon-grid.module.css deleted file mode 100644 index 97b8d8d..0000000 --- a/preview/src/ui/icon-grid/styles/icon-grid.module.css +++ /dev/null @@ -1,42 +0,0 @@ -.root { -} - -.group { - margin-bottom: var(--space-10); -} - -.groupHeader { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: 1.1rem; - font-weight: 600; - margin-bottom: var(--space-4); -} - -.badge { - font-size: 11px; - font-weight: 500; - padding: 2px var(--space-2); - border-radius: 10px; - background: var(--color-accent); - color: #ffffff; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.count { - font-size: 13px; - color: var(--color-muted); - font-weight: 400; -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: var(--space-3); -} - -._hidden { - display: none; -} diff --git a/preview/src/ui/icon-grid/types/icon-grid.type.ts b/preview/src/ui/icon-grid/types/icon-grid.type.ts deleted file mode 100644 index 78038df..0000000 --- a/preview/src/ui/icon-grid/types/icon-grid.type.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { HTMLAttributes } from 'react' -import type { SpriteGroup } from '../../../shared/types' - -/** - * Параметры IconGrid. - */ -export type IconGridParams = { - /** Группы спрайтов для отображения. */ - groups: SpriteGroup[] - /** Строка поиска для фильтрации. */ - searchQuery?: string - /** Обработчик выбора иконки. */ - onIconSelect?: (iconId: string) => void -} - -/** HTML-атрибуты корневого элемента. */ -type RootAttrs = HTMLAttributes - -export type IconGridProps = RootAttrs & IconGridParams diff --git a/preview/src/ui/icon-modal/icon-modal.ui.tsx b/preview/src/ui/icon-modal/icon-modal.ui.tsx deleted file mode 100644 index b085ae1..0000000 --- a/preview/src/ui/icon-modal/icon-modal.ui.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react' -import cl from 'clsx' -import { CodeBlock } from '../code-block' -import { ColorPicker } from '../color-picker' -import { rgbToHex } from '../../shared/lib/rgb-to-hex.util' -import type { IconModalProps } from './types/icon-modal.type' -import styles from './styles/icon-modal.module.css' - -type TabId = 'react' | 'svg' | 'img' | 'css' - -const TABS: { id: TabId; label: string }[] = [ - { id: 'react', label: 'React' }, - { id: 'svg', label: 'SVG' }, - { id: 'img', label: 'IMG' }, - { id: 'css', label: 'CSS' }, -] - -/** - * Модалка деталей иконки с превью, color pickers и табами кода. - * - * Превью иконки рендерится тем же способом, что выбран в табе: - * - React / SVG — через - * - IMG — через - * - CSS — через mask-image на
- */ -export const IconModal = (props: IconModalProps) => { - const { icon, onClose, defaultSprite, className, ...htmlAttr } = props - - const [activeTab, setActiveTab] = useState('react') - const [colors, setColors] = useState>({}) - const [cssColor, setCssColor] = useState('#000000') - const iconWrapRef = useRef(null) - - const handleOverlayClick = useCallback((e: React.MouseEvent) => { - if (e.target === e.currentTarget) { - onClose() - } - }, [onClose]) - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent): void => { - if (e.key === 'Escape') { - onClose() - } - } - - if (icon) { - document.addEventListener('keydown', handleKeyDown) - } - - return () => document.removeEventListener('keydown', handleKeyDown) - }, [icon, onClose]) - - useEffect(() => { - if (!icon) return - - const fg = getComputedStyle(document.documentElement).getPropertyValue('--color-fg').trim() - const fgHex = rgbToHex(fg) - - const initialColors: Record = {} - icon.vars.forEach((v) => { - initialColors[v.varName] = v.isCurrentColor ? fgHex : v.hex - }) - - setColors(initialColors) - setCssColor(fgHex) - setActiveTab('react') - - if (iconWrapRef.current) { - iconWrapRef.current.removeAttribute('style') - } - }, [icon]) - - if (!icon) { - return null - } - - const spriteRef = `${icon.spriteFile}#${icon.id}` - - const isDefaultSprite = icon.group === defaultSprite - const codeReact = isDefaultSprite - ? `` - : `` - const codeSvg = `\n \n` - const codeImg = `${icon.id}` - const codeCss = `.${icon.id} {\n width: 24px;\n height: 24px;\n mask: url('${spriteRef}') no-repeat center / contain;\n -webkit-mask: url('${spriteRef}') no-repeat center / contain;\n background-color: ${cssColor};\n}` - - const codeByTab: Record = { - react: { code: codeReact, language: 'html' }, - svg: { code: codeSvg, language: 'html' }, - img: { code: codeImg, language: 'html' }, - css: { code: codeCss, language: 'css' }, - } - - const handleColorChange = (varName: string, value: string): void => { - setColors((prev) => ({ ...prev, [varName]: value })) - iconWrapRef.current?.style.setProperty(varName, value) - } - - const renderPreview = () => { - switch (activeTab) { - case 'react': - case 'svg': - return ( - - - - ) - - case 'img': - return ( - {icon.id} - ) - - case 'css': - return ( -
- ) - } - } - - const supportsColorChange = activeTab === 'react' || activeTab === 'svg' - const isMono = icon.vars.length > 0 && icon.vars.every((v) => v.isCurrentColor) - - return ( -
-
- - -
-
- {renderPreview()} -
-
- -
-
{icon.id}
- {icon.viewBox && ( - - {icon.viewBox.width} × {icon.viewBox.height} - - )} -
- -
- {icon.vars.length > 0 && supportsColorChange ? ( - <> -
- {isMono ? ( - - Иконка наследует цвет текста или задаётся точечно через CSS-переменную. - - ) : ( - - Многоцветная иконка — каждый цвет задаётся отдельной CSS-переменной. - - )} -
-
CSS Variables
- {icon.vars.map((v) => ( - handleColorChange(v.varName, color)} - label={`${v.varName}: ${v.fallback}`} - className={styles.varRow} - /> - ))} - - ) : activeTab === 'img' ? ( -
- Управление цветом в режиме IMG невозможно. <img> изолирует SVG — CSS-переменные - и currentColor не проникают внутрь. Подходит для многоцветных изображений с фиксированными цветами. -
- ) : activeTab === 'css' ? ( - <> -
- В режиме CSS mask иконка монохромная — цвет задаётся через background-color. - CSS-переменные спрайта не поддерживаются. -
- - - ) : ( - No color variables - )} -
- -
- {TABS.map((tab) => ( - - ))} -
- - {TABS.map((tab) => ( -
- -
- ))} -
-
- ) -} diff --git a/preview/src/ui/icon-modal/index.ts b/preview/src/ui/icon-modal/index.ts deleted file mode 100644 index 377c6e9..0000000 --- a/preview/src/ui/icon-modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { IconModal } from './icon-modal.ui' diff --git a/preview/src/ui/icon-modal/styles/icon-modal.module.css b/preview/src/ui/icon-modal/styles/icon-modal.module.css deleted file mode 100644 index dfeaa85..0000000 --- a/preview/src/ui/icon-modal/styles/icon-modal.module.css +++ /dev/null @@ -1,211 +0,0 @@ -.overlay { - position: fixed; - inset: 0; - z-index: 100; - display: flex; - align-items: flex-start; - justify-content: center; - padding: var(--space-10) 0; - overflow-y: auto; - background: rgba(0, 0, 0, 0.5); -} - -.root { - position: relative; - width: 90%; - max-width: 560px; - padding: var(--space-6); - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: var(--radius-3); -} - -.closeButton { - position: absolute; - top: var(--space-3); - right: var(--space-3); - padding: var(--space-1) var(--space-2); - background: none; - border: none; - color: var(--color-fg); - font-size: 20px; - cursor: pointer; - line-height: 1; -} - -.iconWrap { - display: flex; - align-items: center; - justify-content: center; - padding: var(--space-6); - margin-bottom: var(--space-4); - background: var(--color-card-bg); - border-radius: var(--radius-2); -} - -.iconViewBox { - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-1); - background: var(--checkerboard); - background-size: var(--checkerboard-size); -} - -.icon, -.iconImg, -.iconCss { - width: 256px; - height: 256px; -} - -.icon { - color: var(--color-fg); -} - -.iconImg { - object-fit: contain; -} - -.iconCss { - background-color: var(--color-fg); -} - -.nameRow { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-2); - margin-bottom: var(--space-4); -} - -.name { - font-size: 1rem; - font-weight: 600; -} - -.viewBoxBadge { - font-size: 11px; - font-weight: 500; - padding: 2px var(--space-2); - border-radius: 10px; - background: var(--color-card-bg); - border: 1px solid var(--color-border); - color: var(--color-muted); - white-space: nowrap; -} - -.varsSection { - margin-bottom: var(--space-5); -} - -.varsTitle { - font-size: 12px; - font-weight: 600; - color: var(--color-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: var(--space-2); -} - -.varRow { - display: flex; - align-items: center; - gap: var(--space-2); - margin-bottom: var(--space-2); -} - - - -.noVars { - font-size: 12px; - color: var(--color-muted); - font-style: italic; -} - -.hint { - font-size: 12px; - line-height: 1.5; - color: var(--color-muted); - padding: var(--space-2) var(--space-3); - background: var(--color-card-bg); - border-radius: var(--radius-1); - border-left: 3px solid var(--color-accent); -} - -.colorHint { - margin-bottom: var(--space-3); - padding: var(--space-2) var(--space-3); - font-size: 12px; - line-height: 1.5; - color: var(--color-muted); - background: var(--color-card-bg); - border-radius: var(--radius-1); - border-left: 3px solid var(--color-accent); -} - -.colorHintTitle { - font-size: 12px; - font-weight: 600; - color: var(--color-muted); - margin-bottom: var(--space-1); -} - -.colorHintRow { - display: flex; - align-items: baseline; - gap: var(--space-2); - margin-top: var(--space-1); -} - -.colorHintCode { - font-family: var(--font-mono); - font-size: 11px; - padding: 2px var(--space-1); - background: var(--color-bg); - border-radius: 3px; - white-space: nowrap; -} - -.colorHintDesc { - font-size: 11px; - color: var(--color-muted); -} - -.cssColorRow { - display: flex; - align-items: center; - gap: var(--space-2); - margin-top: var(--space-3); -} - -.tabs { - display: flex; - gap: 0; - border-bottom: 1px solid var(--color-border); - margin-bottom: var(--space-3); -} - -.tab { - padding: var(--space-2) var(--space-4); - font-size: 12px; - font-weight: 600; - cursor: pointer; - border: none; - border-bottom: 2px solid transparent; - color: var(--color-muted); - background: none; -} - -.tab._active { - color: var(--color-accent); - border-bottom-color: var(--color-accent); -} - -.tabContent { - display: none; -} - -.tabContent._active { - display: block; -} diff --git a/preview/src/ui/icon-modal/types/icon-modal.type.ts b/preview/src/ui/icon-modal/types/icon-modal.type.ts deleted file mode 100644 index e325d78..0000000 --- a/preview/src/ui/icon-modal/types/icon-modal.type.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { HTMLAttributes } from 'react' -import type { IconData } from '../../../shared/types' - -/** - * Параметры IconModal. - */ -export type IconModalParams = { - /** Данные иконки для отображения. null — модалка закрыта. */ - icon: IconData | null - /** Имя спрайта по умолчанию (первый из конфига). */ - defaultSprite?: string - /** Обработчик закрытия модалки. */ - onClose: () => void -} - -/** HTML-атрибуты корневого элемента. */ -type RootAttrs = HTMLAttributes - -export type IconModalProps = RootAttrs & IconModalParams diff --git a/preview/src/ui/search-input/index.ts b/preview/src/ui/search-input/index.ts deleted file mode 100644 index c60a639..0000000 --- a/preview/src/ui/search-input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SearchInput } from './search-input.ui' diff --git a/preview/src/ui/search-input/search-input.ui.tsx b/preview/src/ui/search-input/search-input.ui.tsx deleted file mode 100644 index be42a0e..0000000 --- a/preview/src/ui/search-input/search-input.ui.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import cl from 'clsx' -import type { SearchInputProps } from './types/search-input.type' -import styles from './styles/search-input.module.css' - -/** - * Поле поиска. - * - * Используется для: - * - фильтрации иконок по имени - */ -export const SearchInput = (props: SearchInputProps) => { - const { className, onValueChange, ...htmlAttr } = props - - const handleChange = (e: React.ChangeEvent): void => { - onValueChange?.(e.target.value) - } - - return ( - - ) -} diff --git a/preview/src/ui/search-input/styles/search-input.module.css b/preview/src/ui/search-input/styles/search-input.module.css deleted file mode 100644 index c978e94..0000000 --- a/preview/src/ui/search-input/styles/search-input.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.root { - padding: var(--space-2) var(--space-3); - border: 1px solid var(--color-border); - border-radius: var(--radius-2); - background: var(--color-card-bg); - color: var(--color-fg); - font-size: 14px; - width: 200px; - outline: none; -} - -.root:focus { - border-color: var(--color-accent); -} diff --git a/preview/src/ui/search-input/types/search-input.type.ts b/preview/src/ui/search-input/types/search-input.type.ts deleted file mode 100644 index c674f4f..0000000 --- a/preview/src/ui/search-input/types/search-input.type.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { InputHTMLAttributes } from 'react' - -/** - * Параметры SearchInput. - */ -export type SearchInputParams = { - /** Обработчик изменения значения поиска. */ - onValueChange?: (value: string) => void -} - -/** HTML-атрибуты корневого элемента. */ -type RootAttrs = Omit, 'onChange'> - -export type SearchInputProps = RootAttrs & SearchInputParams diff --git a/preview/src/vite-env.d.ts b/preview/src/vite-env.d.ts deleted file mode 100644 index feb7ba8..0000000 --- a/preview/src/vite-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// - -declare module '*.module.css' { - const classes: { readonly [key: string]: string } - export default classes -} diff --git a/preview/tsconfig.app.json b/preview/tsconfig.app.json deleted file mode 100644 index 7f42e5f..0000000 --- a/preview/tsconfig.app.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "es2023", - "lib": ["ES2023", "DOM"], - "module": "esnext", - "types": ["vite/client"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] -} diff --git a/preview/tsconfig.json b/preview/tsconfig.json deleted file mode 100644 index 1ffef60..0000000 --- a/preview/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/preview/tsconfig.node.json b/preview/tsconfig.node.json deleted file mode 100644 index d3c52ea..0000000 --- a/preview/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "es2023", - "lib": ["ES2023"], - "module": "esnext", - "types": ["node"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["vite.config.ts"] -} diff --git a/preview/vite.config.ts b/preview/vite.config.ts deleted file mode 100644 index 891521d..0000000 --- a/preview/vite.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { resolve } from 'node:path' -import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs' -import { defineConfig } from 'vite' -import type { Plugin } from 'vite' -import react from '@vitejs/plugin-react' -import { viteSingleFile } from 'vite-plugin-singlefile' - -/** Переименовывает index.html → preview-template.html после сборки. */ -function renameOutput(): Plugin { - return { - name: 'rename-preview-template', - closeBundle() { - const outDir = resolve(__dirname, '../dist') - const src = resolve(outDir, 'index.html') - const dest = resolve(outDir, 'preview-template.html') - - if (existsSync(src)) { - const content = readFileSync(src, 'utf-8') - writeFileSync(dest, content) - unlinkSync(src) - } - }, - } -} - -export default defineConfig({ - plugins: [react(), viteSingleFile(), renameOutput()], - build: { - outDir: resolve(__dirname, '../dist'), - emptyOutDir: false, - }, -}) diff --git a/src/api/next.ts b/src/api/next.ts new file mode 100644 index 0000000..58b1925 --- /dev/null +++ b/src/api/next.ts @@ -0,0 +1,43 @@ +import type { SpriteGenerationBaseResult } from '../core/result.js' +import { generateSprite } from '../generate.js' +import type { + NextAssetTarget, + NextBundler, + NextRouter, +} from '../targets/types.js' +import type { SpriteConfig } from '../types.js' + +/** @deprecated Используйте единый SpriteConfig. */ +export type NextSpriteConfig = Omit & { + mode?: NextAssetTarget +} + +export type NextSpriteGenerationOptions = { + router: NextRouter + bundler: NextBundler +} + +export type NextSpriteGenerationResult = SpriteGenerationBaseResult< + NextAssetTarget, + NextAssetTarget +> & { + router: NextRouter + bundler: NextBundler +} + +/** Генерирует Next.js sprite-модуль для явно выбранных router и bundler. */ +export async function generateNextSprite( + source: string, + options: NextSpriteGenerationOptions, +): Promise { + if (!options || (options.router !== 'app' && options.router !== 'pages')) { + throw new Error(`Unsupported Next.js router: ${String(options?.router)}`) + } + if (options.bundler !== 'turbopack' && options.bundler !== 'webpack') { + throw new Error(`Unsupported Next.js bundler: ${String(options.bundler)}`) + } + + return generateSprite(source, { + mode: `next@${options.router}/${options.bundler}`, + }) as Promise +} diff --git a/src/api/react.ts b/src/api/react.ts new file mode 100644 index 0000000..8df2bf7 --- /dev/null +++ b/src/api/react.ts @@ -0,0 +1,30 @@ +import { generateSprite } from '../generate.js' +import type { ReactAssetTarget, ReactSpriteMode } from '../targets/types.js' +import type { ResolvedSpriteConfig, SpriteConfig } from '../types.js' +import type { SpriteGenerationBaseResult } from '../core/result.js' + +/** @deprecated Используйте единый SpriteConfig. */ +export type ReactSpriteConfig = Omit & { + mode?: ReactSpriteMode +} + +/** @deprecated Используйте единый ResolvedSpriteConfig. */ +export type ResolvedReactSpriteConfig = Omit & { + mode: ReactSpriteMode +} + +export type ReactSpriteGenerationResult = SpriteGenerationBaseResult< + ReactSpriteMode, + ReactAssetTarget +> + +/** Генерирует React sprite-модуль для явно выбранного asset target. */ +export function generateReactSprite( + source: string, + target: ReactAssetTarget, +): Promise { + if (target !== 'vite' && target !== 'webpack') { + throw new Error(`Unsupported React asset target: ${String(target)}`) + } + return generateSprite(source, { mode: `react@${target}` }) as Promise +} diff --git a/src/cli.ts b/src/cli.ts index 2925ed6..6ec6240 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,17 +1,7 @@ #!/usr/bin/env node -import path from 'node:path' import { CLI_USAGE, parseCliArgs } from './cli/parse-args.js' +import { generateSprite } from './generate.js' import { log } from './logger.js' -import { generateNextSprite } from './modes/next/generate.js' -import { generateReactSprite } from './modes/react/generate.js' -import { loadLegacyConfig } from './modes/legacy/config.js' -import { generateLegacy } from './modes/legacy/generate.js' - -async function runLegacy(spritePath: string): Promise { - const rootDir = path.resolve(spritePath) - const config = await loadLegacyConfig(rootDir) - await generateLegacy(config) -} async function main() { try { @@ -22,23 +12,7 @@ async function main() { return } - switch (args.mode) { - case 'legacy': - await runLegacy(args.path) - return - case 'react': - await generateReactSprite(args.path, args.target) - return - case 'next': { - const [, routerAndBundler] = args.target.split('@') - const [router, bundler] = routerAndBundler.split('/') - await generateNextSprite(args.path, { - router: router as 'app' | 'pages', - bundler: bundler as 'turbopack' | 'webpack', - }) - return - } - } + await generateSprite(args.path, args.overrides) } catch (error) { log.error(error instanceof Error ? error.message : String(error)) process.exit(1) diff --git a/src/cli/parse-args.ts b/src/cli/parse-args.ts index d88a4e0..c7cbab5 100644 --- a/src/cli/parse-args.ts +++ b/src/cli/parse-args.ts @@ -1,119 +1,143 @@ -import type { NextAssetTarget, ReactAssetTarget } from '../targets/types.js' +import { isSpriteMode } from '../config.js' +import type { SpriteConfig, TransformOptions } from '../types.js' import type { CliArgs } from './types.js' -const REACT_TARGETS = new Set(['vite', 'webpack']) -const NEXT_TARGETS = new Set([ - 'next@app/turbopack', - 'next@app/webpack', - 'next@pages/turbopack', - 'next@pages/webpack', -]) - export const CLI_USAGE = [ 'Usage:', - ' svg-sprites --mode ', + ' svg-sprites [options] ', + '', + 'Config files:', + ' Any explicitly provided .js, .json or .ts file', + ' A directory enables config-less generation from CLI options', '', 'Modes:', - ' legacy Generate sprites through the legacy pipeline', - ' react@vite Generate a React module for Vite', - ' react@webpack Generate a React module for Webpack 5', - ' next@app/turbopack Generate an App Router module for Turbopack', - ' next@app/webpack Generate an App Router module for Webpack 5', - ' next@pages/turbopack Generate a Pages Router module for Turbopack', - ' next@pages/webpack Generate a Pages Router module for Webpack 5', + ' react@vite', + ' react@webpack', + ' next@app/turbopack', + ' next@app/webpack', + ' next@pages/turbopack', + ' next@pages/webpack', + '', + 'Options:', + ' --mode ', + ' --name ', + ' --description ', + ' --input-folder ', + ' --input-file Repeat for multiple files', + ' --[no-]remove-size', + ' --[no-]replace-colors', + ' --[no-]add-transition', + ' --[no-]generated-notice', ].join('\n') -export function parseCliArgs(argv: string[]): CliArgs | { help: true } { - if (argv.includes('--help') || argv.includes('-h')) { - return { help: true } +function optionValue(argv: string[], index: number, option: string): [string, number] { + const argument = argv[index] + const inlinePrefix = `${option}=` + if (argument.startsWith(inlinePrefix)) { + const value = argument.slice(inlinePrefix.length) + if (!value) throw new Error(`Missing value for ${option}.`) + return [value, index] } - let modeValue: string | undefined + const value = argv[index + 1] + if (!value || value.startsWith('-')) throw new Error(`Missing value for ${option}.`) + return [value, index + 1] +} + +function setTransform( + overrides: SpriteConfig, + option: keyof TransformOptions, + value: boolean, +): void { + overrides.transform = { + ...overrides.transform, + [option]: value, + } +} + +export function parseCliArgs(argv: string[]): CliArgs | { help: true } { + if (argv.includes('--help') || argv.includes('-h')) return { help: true } + const positional: string[] = [] + const overrides: SpriteConfig = {} for (let index = 0; index < argv.length; index++) { const argument = argv[index] - if (argument === '--mode') { - modeValue = argv[index + 1] - index++ + if (argument === '--mode' || argument.startsWith('--mode=')) { + const [value, nextIndex] = optionValue(argv, index, '--mode') + if (!isSpriteMode(value)) throw new Error(`Unsupported sprite mode: ${value}.\n\n${CLI_USAGE}`) + overrides.mode = value + index = nextIndex + continue + } + if (argument === '--name' || argument.startsWith('--name=')) { + const [value, nextIndex] = optionValue(argv, index, '--name') + overrides.name = value + index = nextIndex + continue + } + if (argument === '--description' || argument.startsWith('--description=')) { + const [value, nextIndex] = optionValue(argv, index, '--description') + overrides.description = value + index = nextIndex + continue + } + if (argument === '--input-folder' || argument.startsWith('--input-folder=')) { + const [value, nextIndex] = optionValue(argv, index, '--input-folder') + overrides.inputFolder = value + index = nextIndex + continue + } + if (argument === '--input-file' || argument.startsWith('--input-file=')) { + const [value, nextIndex] = optionValue(argv, index, '--input-file') + overrides.inputFiles = [...(overrides.inputFiles ?? []), value] + index = nextIndex continue } - if (argument.startsWith('--mode=')) { - modeValue = argument.slice('--mode='.length) - continue + switch (argument) { + case '--remove-size': + setTransform(overrides, 'removeSize', true) + continue + case '--no-remove-size': + setTransform(overrides, 'removeSize', false) + continue + case '--replace-colors': + setTransform(overrides, 'replaceColors', true) + continue + case '--no-replace-colors': + setTransform(overrides, 'replaceColors', false) + continue + case '--add-transition': + setTransform(overrides, 'addTransition', true) + continue + case '--no-add-transition': + setTransform(overrides, 'addTransition', false) + continue + case '--generated-notice': + overrides.generatedNotice = true + continue + case '--no-generated-notice': + overrides.generatedNotice = false + continue } if (argument.startsWith('-')) { throw new Error(`Unknown argument: ${argument}\n\n${CLI_USAGE}`) } - positional.push(argument) } - if (!modeValue) { - throw new Error(`Missing required argument: --mode\n\n${CLI_USAGE}`) - } - if (positional.length === 0) { - throw new Error(`Missing sprite path.\n\n${CLI_USAGE}`) + throw new Error(`Missing sprite config file or module directory.\n\n${CLI_USAGE}`) } - if (positional.length > 1) { - throw new Error(`Expected one sprite path, received: ${positional.join(', ')}`) + throw new Error(`Expected one config file or module directory, received: ${positional.join(', ')}`) } - if (modeValue === 'legacy') { - return { - mode: 'legacy', - path: positional[0], - } + return { + path: positional[0], + overrides, } - - if (modeValue === 'react') { - throw new Error( - 'React mode requires a target. Supported: react@vite, react@webpack.', - ) - } - - if (modeValue.startsWith('react@')) { - const target = modeValue.slice('react@'.length) - - if (!REACT_TARGETS.has(target as ReactAssetTarget)) { - throw new Error( - `Unsupported React target: ${target}. Supported: ${[...REACT_TARGETS].join(', ')}.`, - ) - } - - return { - mode: 'react', - path: positional[0], - target: target as ReactAssetTarget, - } - } - - if (modeValue === 'next') { - throw new Error( - `Next.js mode requires a router and bundler. Supported: ${[...NEXT_TARGETS].join(', ')}.`, - ) - } - - if (modeValue.startsWith('next@')) { - if (!NEXT_TARGETS.has(modeValue as NextAssetTarget)) { - throw new Error( - `Unsupported Next.js target: ${modeValue}. Supported: ${[...NEXT_TARGETS].join(', ')}.`, - ) - } - - return { - mode: 'next', - path: positional[0], - target: modeValue as NextAssetTarget, - } - } - - throw new Error( - `Unknown mode: ${modeValue}\nSupported modes: legacy, react@vite, react@webpack, ${[...NEXT_TARGETS].join(', ')}`, - ) } diff --git a/src/cli/types.ts b/src/cli/types.ts index 0b29623..59c1e0a 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -1,26 +1,6 @@ -import type { NextAssetTarget, ReactAssetTarget } from '../targets/types.js' +import type { SpriteConfig } from '../types.js' -/** Корневой режим генерации, определяющий структуру создаваемых файлов. */ -export type GenerationMode = 'legacy' | 'next' | 'react' - -/** Аргументы legacy pipeline. */ -export type LegacyCliArgs = { - mode: 'legacy' +export type CliArgs = { path: string + overrides: SpriteConfig } - -/** Аргументы React pipeline с обязательной средой обработки SVG asset. */ -export type ReactCliArgs = { - mode: 'react' - path: string - target: ReactAssetTarget -} - -/** Аргументы Next.js pipeline с явно выбранными роутером и сборщиком. */ -export type NextCliArgs = { - mode: 'next' - path: string - target: NextAssetTarget -} - -export type CliArgs = LegacyCliArgs | NextCliArgs | ReactCliArgs diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..0da06a2 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,232 @@ +import fs from 'node:fs' +import path from 'node:path' +import { createJiti } from 'jiti' +import { toKebabCase, validateSpriteName } from './core/naming.js' +import type { SpriteMode } from './targets/types.js' +import type { ResolvedSpriteConfig, SpriteConfig, TransformOptions } from './types.js' + +const CONFIG_EXTENSIONS = new Set(['.js', '.json', '.ts']) +const CONFIG_FIELDS = new Set([ + 'mode', + 'name', + 'description', + 'inputFolder', + 'inputFiles', + 'transform', + 'generatedNotice', +]) +const TRANSFORM_FIELDS = new Set(['removeSize', 'replaceColors', 'addTransition']) +const MODES = new Set([ + 'react@vite', + 'react@webpack', + 'next@app/turbopack', + 'next@app/webpack', + 'next@pages/turbopack', + 'next@pages/webpack', +]) + +export type SpriteConfigSource = { + rootDir: string + configPath: string | null + config: SpriteConfig +} + +export function isSpriteMode(value: unknown): value is SpriteMode { + return typeof value === 'string' && MODES.has(value as SpriteMode) +} + +function getDefaultName(rootDir: string): string { + const rootName = path.basename(rootDir) + const source = rootName === 'svg-sprite' || rootName === 'svg-sprites' + ? path.basename(path.dirname(rootDir)) + : rootName + const name = toKebabCase(source) + + if (!name) throw new Error(`Cannot infer sprite name from directory: ${rootDir}`) + return name +} + +function configError(message: string): Error { + return new Error(`Sprite config: ${message}`) +} + +/** Проверяет единый конфиг независимо от формата исходного файла. */ +export function validateSpriteConfig(value: unknown): asserts value is SpriteConfig { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw configError('expected an object.') + } + + const config = value as Record + + if ('output' in config || 'sprites' in config || 'preview' in config) { + throw configError('legacy config fields are no longer supported.') + } + for (const field of Object.keys(config)) { + if (!CONFIG_FIELDS.has(field)) throw configError(`unknown field "${field}".`) + } + if (config.mode !== undefined && !isSpriteMode(config.mode)) { + throw configError(`unsupported "mode": ${String(config.mode)}.`) + } + if (config.name !== undefined && typeof config.name !== 'string') { + throw configError('"name" must be a string.') + } + if (config.description !== undefined && typeof config.description !== 'string') { + throw configError('"description" must be a string.') + } + if ('icons' in config) { + throw configError('"icons" was renamed to "inputFolder".') + } + if (config.inputFolder !== undefined && ( + typeof config.inputFolder !== 'string' || config.inputFolder.trim() === '' + )) { + throw configError('"inputFolder" must be a non-empty string.') + } + if (config.inputFiles !== undefined && ( + !Array.isArray(config.inputFiles) + || config.inputFiles.some((filePath) => typeof filePath !== 'string' || filePath.trim() === '') + )) { + throw configError('"inputFiles" must be an array of non-empty strings.') + } + if (config.transform !== undefined) { + if ( + config.transform === null + || typeof config.transform !== 'object' + || Array.isArray(config.transform) + ) { + throw configError('"transform" must be an object.') + } + + const transform = config.transform as Record + for (const field of Object.keys(transform)) { + if (!TRANSFORM_FIELDS.has(field)) { + throw configError(`unknown field "transform.${field}".`) + } + } + for (const option of ['removeSize', 'replaceColors', 'addTransition']) { + if (transform[option] !== undefined && typeof transform[option] !== 'boolean') { + throw configError(`"transform.${option}" must be a boolean.`) + } + } + } + if (config.generatedNotice !== undefined && typeof config.generatedNotice !== 'boolean') { + throw configError('"generatedNotice" must be a boolean.') + } +} + +function getModuleDefault(value: unknown, configPath: string): unknown { + if (!value || typeof value !== 'object' || !('default' in value)) { + throw new Error( + `Sprite config file must have a default export: ${configPath}\n` + + 'Use: export default defineSpriteConfig({ ... })', + ) + } + return (value as { default: unknown }).default +} + +/** Загружает явно указанный JS, JSON или TS config-файл. */ +export async function loadSpriteConfig(configFile: string): Promise { + const configPath = path.resolve(configFile) + const extension = path.extname(configPath).toLowerCase() + + if (!CONFIG_EXTENSIONS.has(extension)) { + throw new Error(`Unsupported sprite config extension: ${extension || '(none)'}. Supported: .js, .json, .ts.`) + } + if (!fs.existsSync(configPath) || !fs.statSync(configPath).isFile()) { + throw new Error(`Sprite config file does not exist: ${configPath}`) + } + + let config: unknown + if (extension === '.json') { + try { + config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + } catch (error) { + const reason = error instanceof Error ? error.message : String(error) + throw new Error(`Cannot parse sprite config: ${configPath}\n${reason}`) + } + } else { + const jiti = createJiti(path.dirname(configPath)) + config = getModuleDefault(await jiti.import(configPath), configPath) + } + + validateSpriteConfig(config) + return config +} + +/** Разрешает позиционный путь как config-файл либо config-less корень модуля. */ +export async function resolveSpriteConfigSource(source: string): Promise { + const resolved = path.resolve(source) + + if (!fs.existsSync(resolved)) { + throw new Error(`Sprite config or module directory does not exist: ${resolved}`) + } + + const stats = fs.statSync(resolved) + if (stats.isDirectory()) { + return { rootDir: resolved, configPath: null, config: {} } + } + if (!stats.isFile()) { + throw new Error(`Sprite config path must be a file or directory: ${resolved}`) + } + + return { + rootDir: path.dirname(resolved), + configPath: resolved, + config: await loadSpriteConfig(resolved), + } +} + +function definedEntries(value: T): Partial { + return Object.fromEntries( + Object.entries(value).filter(([, entry]) => entry !== undefined), + ) as Partial +} + +/** Объединяет defaults, config и overrides и разрешает пути от корня модуля. */ +export function resolveSpriteConfig( + rootDir: string, + config: SpriteConfig = {}, + overrides: SpriteConfig = {}, +): ResolvedSpriteConfig { + validateSpriteConfig(config) + validateSpriteConfig(overrides) + + const configValues = definedEntries(config) + const overrideValues = definedEntries(overrides) + const transform: TransformOptions = { + ...config.transform, + ...overrides.transform, + } + const merged: SpriteConfig = { + ...configValues, + ...overrideValues, + transform, + } + + if (!merged.mode) { + throw new Error('Sprite mode is required. Set "mode" in the config or pass it through CLI/API.') + } + + const name = merged.name ?? getDefaultName(rootDir) + validateSpriteName(name) + const inputFiles = (merged.inputFiles ?? []).map((filePath) => path.resolve(rootDir, filePath)) + const defaultInputFolder = path.resolve(rootDir, 'icons') + const inputFolder = merged.inputFolder === undefined + && inputFiles.length > 0 + && !fs.existsSync(defaultInputFolder) + ? null + : path.resolve(rootDir, merged.inputFolder ?? 'icons') + + return { + mode: merged.mode, + name, + description: merged.description, + inputFolder, + inputFiles, + transform: { + removeSize: transform.removeSize ?? true, + replaceColors: transform.replaceColors ?? true, + addTransition: transform.addTransition ?? true, + }, + generatedNotice: merged.generatedNotice ?? true, + } +} diff --git a/src/core/compiled-artifact.ts b/src/core/compiled-artifact.ts new file mode 100644 index 0000000..bc7d9a4 --- /dev/null +++ b/src/core/compiled-artifact.ts @@ -0,0 +1,70 @@ +import { getSpriteShapeId } from '../shape-id.js' +import type { SpriteFormat } from '../types.js' + +export type CompiledIconColor = { + readonly variable: string + readonly fallback: string +} + +export type CompiledIcon = { + readonly name: string + readonly id: string + readonly viewBox: string | null + readonly colors: readonly CompiledIconColor[] +} + +export type CompiledSpriteArtifact = { + readonly format: SpriteFormat + readonly bytes: Uint8Array + readonly icons: readonly CompiledIcon[] +} + +function extractColors(fragment: string): CompiledIconColor[] { + const colors = new Map() + const regex = /var\((--icon-color-\d+),\s*((?:[^()]|\([^()]*\))*)\)/g + let match: RegExpExecArray | null + + while ((match = regex.exec(fragment)) !== null) { + if (!colors.has(match[1])) colors.set(match[1], match[2].trim()) + } + + return [...colors].map(([variable, fallback]) => ({ variable, fallback })) +} + +/** Преобразует compiled SVG в нейтральный artifact для adapter codegen. */ +export function createCompiledSpriteArtifact( + bytes: Uint8Array, + iconNames: readonly string[], + format: SpriteFormat, +): CompiledSpriteArtifact { + const shapes = new Map() + const content = new TextDecoder().decode(bytes) + const shapeRegex = /<(symbol|svg)\b((?=[^>]*\bid="[^"]+")[^>]*)>[\s\S]*?<\/\1>/g + let match: RegExpExecArray | null + + while ((match = shapeRegex.exec(content)) !== null) { + const attributes = match[2] + const id = attributes.match(/\bid="([^"]+)"/)?.[1] + if (!id) continue + + shapes.set(id, { + viewBox: attributes.match(/\bviewBox="([^"]+)"/)?.[1] ?? null, + fragment: match[0], + }) + } + + const icons = iconNames.map((name) => { + const id = getSpriteShapeId(name) + const shape = shapes.get(id) + if (!shape) throw new Error(`Cannot find SVG shape "${id}" for icon "${name}".`) + + return { + name, + id, + viewBox: shape.viewBox, + colors: extractColors(shape.fragment), + } + }) + + return { format, bytes, icons } +} diff --git a/src/core/generated-markers.ts b/src/core/generated-markers.ts new file mode 100644 index 0000000..3637013 --- /dev/null +++ b/src/core/generated-markers.ts @@ -0,0 +1,3 @@ +export const GENERATOR = '@gromlab/svg-sprites' +export const GENERATED_MARKER = '@generated by @gromlab/svg-sprites' +export const GENERATED_NOTICE_MARKER = 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ' diff --git a/src/core/mode-adapter.ts b/src/core/mode-adapter.ts new file mode 100644 index 0000000..7416c99 --- /dev/null +++ b/src/core/mode-adapter.ts @@ -0,0 +1,53 @@ +import type { + NextAssetTarget, + NextBundler, + NextRouter, + ReactAssetTarget, + SpriteMode, +} from '../targets/types.js' +import type { ResolvedSpriteConfig, SpriteFolder } from '../types.js' + +export type GeneratedFile = { + readonly path: string + readonly content: string | Uint8Array +} + +export type PreparedSprite = { + readonly folder: SpriteFolder + readonly iconNames: readonly string[] +} + +export type ModeAdapterContext = { + readonly rootDir: string + readonly config: ResolvedSpriteConfig + readonly prepared: PreparedSprite +} + +export type ReactModeResultMetadata = { + readonly target: ReactAssetTarget +} + +export type NextModeResultMetadata = { + readonly target: NextAssetTarget + readonly router: NextRouter + readonly bundler: NextBundler +} + +export type ModeResultMetadata = ReactModeResultMetadata | NextModeResultMetadata + +export type OutputPlan = { + readonly files: readonly GeneratedFile[] + readonly paths: { + readonly generatedDir: '.svg-sprite' + readonly sprite: string + readonly manifest: string + readonly entry: string + } + readonly result: ModeResultMetadata +} + +export interface ModeAdapter { + readonly mode: M + readonly contractVersion: number + generate(context: ModeAdapterContext): Promise +} diff --git a/src/core/naming.ts b/src/core/naming.ts new file mode 100644 index 0000000..1483edd --- /dev/null +++ b/src/core/naming.ts @@ -0,0 +1,17 @@ +/** Преобразует имя каталога в kebab-case имя спрайта. */ +export function toKebabCase(value: string): string { + return value + .normalize('NFKD') + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') + .replace(/[^a-zA-Z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .toLowerCase() +} + +export function validateSpriteName(name: string): void { + if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name)) { + throw new Error( + `Sprite config: "name" must be kebab-case and start with a letter. Received: "${name}".`, + ) + } +} diff --git a/src/core/output-writer.ts b/src/core/output-writer.ts new file mode 100644 index 0000000..d5bbbe4 --- /dev/null +++ b/src/core/output-writer.ts @@ -0,0 +1,297 @@ +import { randomUUID } from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import type { SpriteMode } from '../targets/types.js' +import type { GeneratedFile } from './mode-adapter.js' +import { + GENERATED_MARKER, + GENERATED_NOTICE_MARKER, + GENERATOR, +} from './generated-markers.js' + +const STATE_PATH = '.svg-sprite/state.json' +const PREVIOUS_STATE_PATHS = [ + '.svg-sprite-data/state.json', + 'generated/state.json', +] as const +const LEGACY_STATE_PATH = 'generated/.svg-sprites.manifest.json' +const DATA_ROOT = '.svg-sprite' +const PREVIOUS_DATA_ROOT = '.svg-sprite-data' +const SYSTEM_FILES: readonly GeneratedFile[] = [ + { + path: '.gitignore', + content: `# ${GENERATED_MARKER}. Do not edit.\n/${DATA_ROOT}/\n`, + }, +] + +type OutputState = { + schemaVersion: 1 + generator: typeof GENERATOR + owner: { + mode: SpriteMode + contractVersion: number + } + files: string[] + warning?: string +} + +type LegacyState = { + version: 1 + generator: typeof GENERATOR + files: string[] +} + +function normalizeManagedPath(relativePath: string): string { + const normalized = relativePath.replaceAll('\\', '/') + const parts = normalized.split('/') + + if ( + normalized === '' + || path.posix.isAbsolute(normalized) + || parts.some((part) => part === '' || part === '.' || part === '..') + || normalized === STATE_PATH + || PREVIOUS_STATE_PATHS.includes(normalized as typeof PREVIOUS_STATE_PATHS[number]) + || normalized === LEGACY_STATE_PATH + ) { + throw new Error(`Invalid generated file path: ${relativePath}`) + } + + return normalized +} + +function resolveManagedPath(rootDir: string, relativePath: string): string { + const normalized = normalizeManagedPath(relativePath) + const resolved = path.resolve(rootDir, normalized) + const relative = path.relative(rootDir, resolved) + + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new Error(`Invalid generated file path: ${relativePath}`) + } + + return resolved +} + +function resolveInternalPath(rootDir: string, relativePath: string): string { + const resolved = path.resolve(rootDir, relativePath) + const relative = path.relative(rootDir, resolved) + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new Error(`Invalid internal output path: ${relativePath}`) + } + return resolved +} + +function assertNoSymlinks(rootDir: string, filePath: string): void { + const relative = path.relative(rootDir, filePath) + let currentPath = rootDir + + for (const segment of relative.split(path.sep)) { + currentPath = path.join(currentPath, segment) + try { + if (fs.lstatSync(currentPath).isSymbolicLink()) { + throw new Error(`Symbolic links are not allowed in generated paths: ${currentPath}`) + } + } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') continue + throw error + } + } +} + +function parseJsonFile(filePath: string): unknown { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf-8')) + } catch { + throw new Error(`Cannot parse generated state: ${filePath}`) + } +} + +function readOutputState(statePath: string): OutputState { + const state = parseJsonFile(statePath) + if ( + !state + || typeof state !== 'object' + || !('schemaVersion' in state) + || state.schemaVersion !== 1 + || !('generator' in state) + || state.generator !== GENERATOR + || !('owner' in state) + || !state.owner + || typeof state.owner !== 'object' + || !('mode' in state.owner) + || typeof state.owner.mode !== 'string' + || !('contractVersion' in state.owner) + || typeof state.owner.contractVersion !== 'number' + || !('files' in state) + || !Array.isArray(state.files) + || !state.files.every((file) => typeof file === 'string') + ) { + throw new Error(`Invalid generated state: ${statePath}`) + } + return state as OutputState +} + +function readPreviousFiles(rootDir: string): { + files: string[] + obsoleteStatePath: string | null +} { + for (const relativeStatePath of [STATE_PATH, ...PREVIOUS_STATE_PATHS]) { + const statePath = resolveInternalPath(rootDir, relativeStatePath) + assertNoSymlinks(rootDir, statePath) + if (!fs.existsSync(statePath)) continue + return { + files: readOutputState(statePath).files.map(normalizeManagedPath), + obsoleteStatePath: relativeStatePath === STATE_PATH ? null : statePath, + } + } + + const legacyStatePath = resolveInternalPath(rootDir, LEGACY_STATE_PATH) + assertNoSymlinks(rootDir, legacyStatePath) + if (!fs.existsSync(legacyStatePath)) return { files: [], obsoleteStatePath: null } + + const legacyState = parseJsonFile(legacyStatePath) + if ( + !legacyState + || typeof legacyState !== 'object' + || !('version' in legacyState) + || legacyState.version !== 1 + || !('generator' in legacyState) + || legacyState.generator !== GENERATOR + || !('files' in legacyState) + || !Array.isArray(legacyState.files) + || !legacyState.files.every((file) => typeof file === 'string') + ) { + throw new Error(`Invalid generated state: ${legacyStatePath}`) + } + + return { + files: (legacyState as LegacyState).files.map(normalizeManagedPath), + obsoleteStatePath: legacyStatePath, + } +} + +function removeEmptyStateDirectory(rootDir: string, statePath: string): void { + const directory = path.dirname(statePath) + if (directory === rootDir) return + try { + fs.rmdirSync(directory) + } catch (error) { + if (error instanceof Error && 'code' in error && ( + error.code === 'ENOENT' || error.code === 'ENOTEMPTY' + )) return + throw error + } +} + +function removeEmptyManagedParents(rootDir: string, filePath: string): void { + for (const relativeDataRoot of [DATA_ROOT, PREVIOUS_DATA_ROOT]) { + const dataRoot = resolveInternalPath(rootDir, relativeDataRoot) + let directory = path.dirname(filePath) + const relative = path.relative(dataRoot, directory) + if (relative.startsWith('..') || path.isAbsolute(relative)) continue + + while (directory !== dataRoot) { + try { + fs.rmdirSync(directory) + } catch (error) { + if (error instanceof Error && 'code' in error && ( + error.code === 'ENOENT' || error.code === 'ENOTEMPTY' + )) return + throw error + } + directory = path.dirname(directory) + } + return + } +} + +function hasGeneratedMarker(filePath: string): boolean { + if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return false + const content = fs.readFileSync(filePath, 'utf-8') + return content.includes(GENERATED_MARKER) || content.includes(GENERATED_NOTICE_MARKER) +} + +function writeFileAtomic(rootDir: string, filePath: string, content: string | Uint8Array): void { + assertNoSymlinks(rootDir, filePath) + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + assertNoSymlinks(rootDir, filePath) + + const temporaryPath = path.join( + path.dirname(filePath), + `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`, + ) + + try { + fs.writeFileSync(temporaryPath, content, { flag: 'wx' }) + fs.renameSync(temporaryPath, filePath) + } finally { + if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath) + } +} + +/** Применяет mode output plan и последним записывает ownership state. */ +export function writeOutputPlan( + rootDir: string, + mode: SpriteMode, + contractVersion: number, + files: readonly GeneratedFile[], + generatedNotice: boolean, +): void { + const normalizedFiles = [...SYSTEM_FILES, ...files].map((file) => ({ + ...file, + path: normalizeManagedPath(file.path), + })) + const nextFiles = normalizedFiles.map((file) => file.path) + + if (new Set(nextFiles).size !== nextFiles.length) { + throw new Error(`Mode "${mode}" produced duplicate generated file paths.`) + } + + const previous = readPreviousFiles(rootDir) + const obsoleteFiles: string[] = [] + + for (const file of normalizedFiles) { + const filePath = resolveManagedPath(rootDir, file.path) + assertNoSymlinks(rootDir, filePath) + if (fs.existsSync(filePath) && !hasGeneratedMarker(filePath)) { + throw new Error( + `Refusing to overwrite a user file: ${filePath}\n` + + 'Move the file or choose another sprite directory.', + ) + } + } + + for (const relativePath of previous.files) { + if (nextFiles.includes(relativePath)) continue + const filePath = resolveManagedPath(rootDir, relativePath) + assertNoSymlinks(rootDir, filePath) + + if (fs.existsSync(filePath)) { + if (!hasGeneratedMarker(filePath)) { + throw new Error(`Refusing to delete a user file: ${filePath}`) + } + obsoleteFiles.push(filePath) + } + } + + for (const file of normalizedFiles) { + writeFileAtomic(rootDir, resolveManagedPath(rootDir, file.path), file.content) + } + for (const filePath of obsoleteFiles) fs.unlinkSync(filePath) + for (const filePath of obsoleteFiles) removeEmptyManagedParents(rootDir, filePath) + if (previous.obsoleteStatePath && fs.existsSync(previous.obsoleteStatePath)) { + fs.unlinkSync(previous.obsoleteStatePath) + removeEmptyStateDirectory(rootDir, previous.obsoleteStatePath) + } + + const state: OutputState = { + schemaVersion: 1, + generator: GENERATOR, + owner: { mode, contractVersion }, + files: nextFiles, + ...(generatedNotice && { + warning: 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ. Не редактируйте вручную: изменения будут перезаписаны.', + }), + } + const statePath = resolveInternalPath(rootDir, STATE_PATH) + writeFileAtomic(rootDir, statePath, `${JSON.stringify(state, null, 2)}\n`) +} diff --git a/src/core/prepare-sprite.ts b/src/core/prepare-sprite.ts new file mode 100644 index 0000000..a63046a --- /dev/null +++ b/src/core/prepare-sprite.ts @@ -0,0 +1,38 @@ +import path from 'node:path' +import { resolveSpriteSources } from '../scanner.js' +import { getSpriteShapeId } from '../shape-id.js' +import type { ResolvedSpriteConfig } from '../types.js' +import type { PreparedSprite } from './mode-adapter.js' + +function validateIconIds(iconNames: readonly string[]): void { + const namesById = new Map() + + for (const iconName of iconNames) { + const id = getSpriteShapeId(iconName) + const existingName = namesById.get(id) + + if (existingName) { + throw new Error( + `Icons "${existingName}" and "${iconName}" produce the same SVG id "${id}". Rename one of the files.`, + ) + } + + namesById.set(id, iconName) + } +} + +/** Подготавливает mode-neutral набор исходников. */ +export function prepareSprite(config: ResolvedSpriteConfig): PreparedSprite { + const folder = resolveSpriteSources({ + name: config.name, + format: 'stack', + inputFolder: config.inputFolder, + inputFiles: config.inputFiles, + }) + const iconNames = folder.files + .map((filePath) => path.basename(filePath, '.svg')) + .sort() + + validateIconIds(iconNames) + return { folder, iconNames } +} diff --git a/src/core/result.ts b/src/core/result.ts new file mode 100644 index 0000000..eeee287 --- /dev/null +++ b/src/core/result.ts @@ -0,0 +1,15 @@ +import type { SpriteAssetTarget, SpriteMode } from '../targets/types.js' + +export type SpriteGenerationBaseResult< + TMode extends SpriteMode = SpriteMode, + TTarget extends SpriteAssetTarget = SpriteAssetTarget, +> = { + name: string + rootDir: string + generatedDir: string + spritePath: string + manifestPath: string + iconCount: number + mode: TMode + target: TTarget +} diff --git a/src/generate.ts b/src/generate.ts new file mode 100644 index 0000000..3235abb --- /dev/null +++ b/src/generate.ts @@ -0,0 +1,65 @@ +import path from 'node:path' +import { + resolveSpriteConfig, + resolveSpriteConfigSource, +} from './config.js' +import type { ModeResultMetadata } from './core/mode-adapter.js' +import { writeOutputPlan } from './core/output-writer.js' +import { prepareSprite } from './core/prepare-sprite.js' +import type { SpriteGenerationBaseResult } from './core/result.js' +import { log } from './logger.js' +import { getModeAdapter } from './mode-registry.js' +import type { SpriteConfig } from './types.js' + +export type SpriteGenerationResult = SpriteGenerationBaseResult & ModeResultMetadata + +/** Генерирует один output через adapter разрешённого exact mode. */ +export async function generateSprite( + source: string, + overrides: SpriteConfig = {}, +): Promise { + const resolvedSource = await resolveSpriteConfigSource(source) + const config = resolveSpriteConfig( + resolvedSource.rootDir, + resolvedSource.config, + overrides, + ) + const adapter = getModeAdapter(config.mode) + const prepared = prepareSprite(config) + const plan = await adapter.generate({ + rootDir: resolvedSource.rootDir, + config, + prepared, + }) + const plannedPaths = new Set(plan.files.map((file) => file.path.replaceAll('\\', '/'))) + + for (const requiredPath of [plan.paths.entry, plan.paths.sprite, plan.paths.manifest]) { + if (!plannedPaths.has(requiredPath)) { + throw new Error(`Mode "${config.mode}" result path is missing from its output plan: ${requiredPath}`) + } + } + + writeOutputPlan( + resolvedSource.rootDir, + adapter.mode, + adapter.contractVersion, + plan.files, + config.generatedNotice, + ) + + const generatedDir = path.resolve(resolvedSource.rootDir, plan.paths.generatedDir) + const iconLabel = prepared.iconNames.length === 1 ? 'icon' : 'icons' + log.success(`✓ ${config.name} · ${prepared.iconNames.length} ${iconLabel} · ${config.mode}`) + log.detail(` → ${path.relative(process.cwd(), generatedDir)}`) + + return { + name: config.name, + rootDir: resolvedSource.rootDir, + generatedDir, + spritePath: path.resolve(resolvedSource.rootDir, plan.paths.sprite), + manifestPath: path.resolve(resolvedSource.rootDir, plan.paths.manifest), + iconCount: prepared.iconNames.length, + mode: config.mode, + ...plan.result, + } +} diff --git a/src/index.ts b/src/index.ts index 08109f6..f02f376 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,36 @@ -import type { SvgSpritesConfig } from './types.js' -import type { NextSpriteConfig } from './modes/next/types.js' -import type { ReactSpriteConfig } from './modes/react/types.js' +import type { NextSpriteConfig } from './api/next.js' +import type { ReactSpriteConfig } from './api/react.js' +import type { SpriteConfig } from './types.js' -export { generateLegacy } from './modes/legacy/generate.js' -export { resolveSprites, resolveSpriteEntry } from './scanner.js' +export { + isSpriteMode, + loadSpriteConfig, + resolveSpriteConfig, + resolveSpriteConfigSource, + validateSpriteConfig, +} from './config.js' +export { generateSprite } from './generate.js' +export type { SpriteGenerationResult } from './generate.js' +export { generateNextSprite } from './api/next.js' +export { generateReactSprite } from './api/react.js' export { compileSprite, compileSpriteContent } from './compiler.js' export type { CompileSpriteOptions } from './compiler.js' export { createShapeTransform } from './transforms.js' -export { generatePreview } from './preview.js' -export { loadLegacyConfig } from './modes/legacy/config.js' -export { generateNextSprite } from './modes/next/index.js' -export { - generateReactSprite, - loadReactSpriteConfig, -} from './modes/react/index.js' + export type { NextAssetTarget, NextBundler, NextRouter, ReactAssetTarget, + ReactSpriteMode, SpriteAssetTarget, + SpriteMode, ViteAssetTarget, WebpackAssetTarget, } from './targets/types.js' - export type { - SvgSpritesConfig, - SpriteEntry, - SpriteResult, + ResolvedSpriteConfig, + SpriteConfig, SpriteFolder, SpriteFormat, TransformOptions, @@ -36,24 +39,24 @@ export type { NextSpriteConfig, NextSpriteGenerationOptions, NextSpriteGenerationResult, -} from './modes/next/types.js' +} from './api/next.js' export type { ReactSpriteConfig, ReactSpriteGenerationResult, ResolvedReactSpriteConfig, -} from './modes/react/types.js' +} from './api/react.js' -/** Хелпер для типизации legacy-конфига. */ -export function defineLegacyConfig(config: SvgSpritesConfig): SvgSpritesConfig { +/** Хелпер для типизации единого sprite-конфига. */ +export function defineSpriteConfig(config: SpriteConfig): SpriteConfig { return config } -/** Хелпер для типизации локального React-конфига. */ +/** @deprecated Используйте defineSpriteConfig. */ export function defineReactSpriteConfig(config: ReactSpriteConfig): ReactSpriteConfig { return config } -/** Хелпер для типизации локального Next.js-конфига. */ +/** @deprecated Используйте defineSpriteConfig. */ export function defineNextSpriteConfig(config: NextSpriteConfig): NextSpriteConfig { return config } diff --git a/src/mode-registry.ts b/src/mode-registry.ts new file mode 100644 index 0000000..61c1f51 --- /dev/null +++ b/src/mode-registry.ts @@ -0,0 +1,21 @@ +import type { ModeAdapter } from './core/mode-adapter.js' +import { nextAppTurbopackAdapter } from './modes/next-app-turbopack/adapter.js' +import { nextAppWebpackAdapter } from './modes/next-app-webpack/adapter.js' +import { nextPagesTurbopackAdapter } from './modes/next-pages-turbopack/adapter.js' +import { nextPagesWebpackAdapter } from './modes/next-pages-webpack/adapter.js' +import { reactViteAdapter } from './modes/react-vite/adapter.js' +import { reactWebpackAdapter } from './modes/react-webpack/adapter.js' +import type { SpriteMode } from './targets/types.js' + +const modeRegistry: Record = { + 'react@vite': reactViteAdapter, + 'react@webpack': reactWebpackAdapter, + 'next@app/turbopack': nextAppTurbopackAdapter, + 'next@app/webpack': nextAppWebpackAdapter, + 'next@pages/turbopack': nextPagesTurbopackAdapter, + 'next@pages/webpack': nextPagesWebpackAdapter, +} + +export function getModeAdapter(mode: SpriteMode): ModeAdapter { + return modeRegistry[mode] +} diff --git a/src/modes/legacy/config.ts b/src/modes/legacy/config.ts deleted file mode 100644 index 4b5a602..0000000 --- a/src/modes/legacy/config.ts +++ /dev/null @@ -1,74 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { createJiti } from 'jiti' -import type { SvgSpritesConfig } from '../../types.js' - -const CONFIG_NAME = 'svg-sprites.config' - -/** Загружает legacy-конфиг из указанной директории. */ -export async function loadLegacyConfig(cwd: string = process.cwd()): Promise { - const configPath = path.join(cwd, `${CONFIG_NAME}.ts`) - - if (!fs.existsSync(configPath)) { - throw new Error( - `Config file not found: ${configPath}\n` + - `Create a ${CONFIG_NAME}.ts file in the project root.`, - ) - } - - const jiti = createJiti(cwd) - const mod = await jiti.import(configPath) as { default?: SvgSpritesConfig } - const config = mod.default - - if (!config) { - throw new Error( - `Config file must have a default export: ${configPath}\n` + - 'Use: export default defineLegacyConfig({ ... })', - ) - } - - validateLegacyConfig(config) - return { - ...config, - output: path.resolve(cwd, config.output), - sprites: config.sprites.map((sprite) => ({ - ...sprite, - input: Array.isArray(sprite.input) - ? sprite.input.map((filePath) => path.resolve(cwd, filePath)) - : path.resolve(cwd, sprite.input), - })), - } -} - -/** Валидирует legacy-конфиг. */ -export function validateLegacyConfig(config: SvgSpritesConfig): void { - if (!config.output) { - throw new Error('Config: "output" is required.') - } - - if (!config.sprites || config.sprites.length === 0) { - throw new Error('Config: "sprites" must be a non-empty array.') - } - - for (const sprite of config.sprites) { - if ('mode' in sprite) { - throw new Error( - `Config: sprite "${sprite.name}" uses deprecated "mode". Use "format" instead.`, - ) - } - - if (!sprite.name) { - throw new Error('Config: each sprite must have a "name".') - } - - if (!sprite.input) { - throw new Error(`Config: sprite "${sprite.name}" must have an "input".`) - } - - if (sprite.format && sprite.format !== 'stack' && sprite.format !== 'symbol') { - throw new Error( - `Config: sprite "${sprite.name}" has invalid format "${sprite.format}". Supported: stack, symbol.`, - ) - } - } -} diff --git a/src/modes/legacy/generate.ts b/src/modes/legacy/generate.ts deleted file mode 100644 index 7833fed..0000000 --- a/src/modes/legacy/generate.ts +++ /dev/null @@ -1,52 +0,0 @@ -import path from 'node:path' -import { compileSprite } from '../../compiler.js' -import { log } from '../../logger.js' -import { generatePreview } from '../../preview.js' -import { resolveSprites } from '../../scanner.js' -import type { SpriteResult, SvgSpritesConfig } from '../../types.js' - -/** Генерирует SVG-спрайты через legacy pipeline. */ -export async function generateLegacy(config: SvgSpritesConfig): Promise { - const { - output, - preview = true, - transform = {}, - sprites, - } = config - const outputDir = path.resolve(output) - - log.title('Resolving legacy sprites...') - - const folders = resolveSprites(sprites) - - if (folders.length === 0) { - log.warn('No sprites to generate.') - return [] - } - - log.info(`Found ${folders.length} sprite(s)\n`) - - const results: SpriteResult[] = [] - - for (const folder of folders) { - const spritePath = await compileSprite(folder, outputDir, transform) - log.success(` [${folder.format}] ${folder.name} → ${path.relative(process.cwd(), spritePath)} (${folder.files.length} icons)`) - - results.push({ - name: folder.name, - format: folder.format, - spritePath, - iconCount: folder.files.length, - }) - } - - if (preview) { - const previewPath = generatePreview(results, outputDir) - log.success(`\n [preview] → ${path.relative(process.cwd(), previewPath)}`) - } - - console.log('') - log.success(`Done! Generated ${results.length} sprite(s).`) - - return results -} diff --git a/src/modes/legacy/index.ts b/src/modes/legacy/index.ts deleted file mode 100644 index 7d5a636..0000000 --- a/src/modes/legacy/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { loadLegacyConfig, validateLegacyConfig } from './config.js' -export { generateLegacy } from './generate.js' diff --git a/src/modes/next-app-turbopack/adapter.ts b/src/modes/next-app-turbopack/adapter.ts new file mode 100644 index 0000000..c059912 --- /dev/null +++ b/src/modes/next-app-turbopack/adapter.ts @@ -0,0 +1,26 @@ +import { compileSpriteContent } from '../../compiler.js' +import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import type { ModeAdapter } from '../../core/mode-adapter.js' +import { generateOutputFiles } from './output.js' + +export const nextAppTurbopackAdapter: ModeAdapter<'next@app/turbopack'> = { + mode: 'next@app/turbopack', + contractVersion: 5, + + async generate(context) { + const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { + rootViewBox: true, + }) + const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') + return { + files: generateOutputFiles(context.config, artifact), + paths: { + generatedDir: '.svg-sprite', + sprite: '.svg-sprite/sprite.svg', + manifest: '.svg-sprite/svg-sprite.manifest.js', + entry: '.svg-sprite/index.js', + }, + result: { target: 'next@app/turbopack', router: 'app', bundler: 'turbopack' }, + } + }, +} diff --git a/src/modes/next-app-turbopack/output.ts b/src/modes/next-app-turbopack/output.ts new file mode 100644 index 0000000..7bea198 --- /dev/null +++ b/src/modes/next-app-turbopack/output.ts @@ -0,0 +1,170 @@ +import path from 'node:path' +import type { CompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import { GENERATED_MARKER } from '../../core/generated-markers.js' +import type { GeneratedFile } from '../../core/mode-adapter.js' +import type { ResolvedSpriteConfig } from '../../types.js' + +const MODE = 'next@app/turbopack' +const OUTPUT_DIR = '.svg-sprite' +const NOTICE = 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ. Не редактируйте вручную.' + +function pascal(value: string): string { + return value.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('') +} + +function camel(value: string): string { + return value.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()) +} + +function header(enabled: boolean): string { + return enabled ? `/*\n * ${NOTICE}\n * ${GENERATED_MARKER}.\n */` : `/* ${GENERATED_MARKER}. Do not edit. */` +} + +function svg(bytes: Uint8Array, enabled: boolean): string { + const marker = enabled ? `${NOTICE}\n ${GENERATED_MARKER}.` : `${GENERATED_MARKER}. Do not edit.` + const content = new TextDecoder().decode(bytes) + return content.startsWith(')\s*/, `$1\n\n`) + : `\n${content}` +} + +function comment(config: ResolvedSpriteConfig): string[] { + const text = config.description ?? `Имена иконок SVG-спрайта «${config.name}».` + return ['/**', ...text.split(/\r?\n/).map((line) => ` * ${line.replace(/\*\//g, '* /')}`), ' */'] +} + +function iconData(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const prefix = camel(config.name) + const names = artifact.icons.map((icon) => icon.name) + const ids = Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.id])) + return [header(config.generatedNotice), '', ...comment(config), `export const ${prefix}IconNames = ${JSON.stringify(names, null, 2)}`, '', `export const ${prefix}IconIds = ${JSON.stringify(ids, null, 2)}`, ''].join('\n') +} + +function component(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const ids = `${camel(config.name)}IconIds` + return [ + header(config.generatedNotice), + "import { jsx } from 'react/jsx-runtime'", + "import styles from './react-component.module.css'", + `import { ${ids} } from '../icon-data.js'`, + '', + "const spriteUrl = new URL('../sprite.svg', import.meta.url).href", + '', + `export const ${componentName} = (props) => {`, + ' const { icon, wrapped, className, ...rest } = props', + ` const href = spriteUrl + '#' + ${ids}[icon]`, + ' if (wrapped) {', + " return jsx('span', {", + ' ...rest,', + " className: [styles.wrap, className].filter(Boolean).join(' '),", + " children: jsx('svg', { children: jsx('use', { href }) }),", + ' })', + ' }', + " return jsx('svg', {", + ' ...rest,', + " className: [styles.root, className].filter(Boolean).join(' '),", + " children: jsx('use', { href }),", + ' })', + '}', + '', + ].join('\n') +} + +function iconDeclarations(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const names = `${camel(config.name)}IconNames` + const ids = `${camel(config.name)}IconIds` + return [ + header(config.generatedNotice), + '', + ...comment(config), + `export declare const ${names}: readonly [`, + ...artifact.icons.map((icon) => ` ${JSON.stringify(icon.name)},`), + ']', + `export type ${typeName} = typeof ${names}[number]`, + `export declare const ${ids}: {`, + ...artifact.icons.map((icon) => ` readonly ${JSON.stringify(icon.name)}: ${JSON.stringify(icon.id)}`), + '}', + '', + ].join('\n') +} + +function componentDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const styleName = `${componentName}Style` + const propsName = `${componentName}Props` + return [header(config.generatedNotice), + "import type { CSSProperties, HTMLAttributes, ReactElement, SVGAttributes } from 'react'", + `import type { ${typeName} } from '../icon-data.js'`, + '', + `export type ${styleName} = CSSProperties & Partial>`, + `type ${componentName}BaseProps = { icon: ${typeName} }`, + `type ${componentName}SvgProps = ${componentName}BaseProps & { wrapped?: false } & Omit, 'style'> & { style?: ${styleName} }`, + `type ${componentName}WrappedProps = ${componentName}BaseProps & { wrapped: true } & Omit, 'style'> & { style?: ${styleName} }`, + `export type ${propsName} = ${componentName}SvgProps | ${componentName}WrappedProps`, + `export declare const ${componentName}: (props: ${propsName}) => ReactElement`, + '', + ].join('\n') +} + +function indexDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const names = `${camel(config.name)}IconNames` + return [ + header(config.generatedNotice), + `export { ${componentName} } from './react/react-component.js'`, + `export type { ${componentName}Props, ${componentName}Style } from './react/react-component.js'`, + `export { ${names} } from './icon-data.js'`, + `export type { ${componentName}Name } from './icon-data.js'`, + '', + ].join('\n') +} + +function index(config: ResolvedSpriteConfig): string { + return [header(config.generatedNotice), `export { ${pascal(config.name)}Icon } from './react/react-component.js'`, `export { ${camel(config.name)}IconNames } from './icon-data.js'`, ''].join('\n') +} + +function styles(config: ResolvedSpriteConfig): string { + const transition = config.transform.addTransition ? [' transition-property: fill, stroke, color;', ' transition-duration: 0.3s;', ' transition-timing-function: ease;'] : [] + return [header(config.generatedNotice), '', '.root {', ...transition, '}', '', '.wrap {', ' display: inline-flex;', '}', '', '.wrap svg {', ' width: 100%;', ' height: 100%;', ...transition, '}', ''].join('\n') +} + +function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const data = { + schemaVersion: 1, + generator: '@gromlab/svg-sprites', + name: config.name, + ...(config.description === undefined ? {} : { description: config.description }), + componentName: `${pascal(config.name)}Icon`, + mode: MODE, + target: MODE, + format: artifact.format, + iconCount: artifact.icons.length, + spriteUrl: '__SPRITE_URL__', + icons: artifact.icons, + } + const source = JSON.stringify(data, null, 2).replace('"__SPRITE_URL__"', 'spriteUrl') + return [header(config.generatedNotice), "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", '', `export const spriteManifest = ${source}`, '', 'export default spriteManifest', ''].join('\n') +} + +function manifestTypes(config: ResolvedSpriteConfig): string { + return [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') +} + +export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { + return [ + { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config) }, + { path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: svg(artifact.bytes, config.generatedNotice) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: iconData(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.d.ts'), content: iconDeclarations(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.js'), content: component(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.d.ts'), content: componentDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.module.css'), content: styles(config) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.js'), content: manifest(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.d.ts'), content: manifestTypes(config) }, + ] +} diff --git a/src/modes/next-app-webpack/adapter.ts b/src/modes/next-app-webpack/adapter.ts new file mode 100644 index 0000000..9d20de5 --- /dev/null +++ b/src/modes/next-app-webpack/adapter.ts @@ -0,0 +1,18 @@ +import { compileSpriteContent } from '../../compiler.js' +import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import type { ModeAdapter } from '../../core/mode-adapter.js' +import { generateOutputFiles } from './output.js' + +export const nextAppWebpackAdapter: ModeAdapter<'next@app/webpack'> = { + mode: 'next@app/webpack', + contractVersion: 5, + async generate(context) { + const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true }) + const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') + return { + files: generateOutputFiles(context.config, artifact), + paths: { generatedDir: '.svg-sprite', sprite: '.svg-sprite/sprite.svg', manifest: '.svg-sprite/svg-sprite.manifest.js', entry: '.svg-sprite/index.js' }, + result: { target: 'next@app/webpack', router: 'app', bundler: 'webpack' }, + } + }, +} diff --git a/src/modes/next-app-webpack/output.ts b/src/modes/next-app-webpack/output.ts new file mode 100644 index 0000000..cebb996 --- /dev/null +++ b/src/modes/next-app-webpack/output.ts @@ -0,0 +1,88 @@ +import path from 'node:path' +import type { CompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import { GENERATED_MARKER } from '../../core/generated-markers.js' +import type { GeneratedFile } from '../../core/mode-adapter.js' +import type { ResolvedSpriteConfig } from '../../types.js' + +const MODE = 'next@app/webpack' +const OUTPUT_DIR = '.svg-sprite' +const NOTICE = 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ. Не редактируйте вручную.' +const pascal = (value: string) => value.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('') +const camel = (value: string) => value.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()) +const header = (enabled: boolean) => enabled ? `/*\n * ${NOTICE}\n * ${GENERATED_MARKER}.\n */` : `/* ${GENERATED_MARKER}. Do not edit. */` + +function markedSvg(bytes: Uint8Array, enabled: boolean): string { + const marker = enabled ? `${NOTICE}\n ${GENERATED_MARKER}.` : `${GENERATED_MARKER}. Do not edit.` + const content = new TextDecoder().decode(bytes) + return content.startsWith(')\s*/, `$1\n\n`) : `\n${content}` +} + +function docs(config: ResolvedSpriteConfig): string[] { + const text = config.description ?? `Имена иконок SVG-спрайта «${config.name}».` + return ['/**', ...text.split(/\r?\n/).map((line) => ` * ${line.replace(/\*\//g, '* /')}`), ' */'] +} + +function iconData(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const prefix = camel(config.name) + return [header(config.generatedNotice), '', ...docs(config), `export const ${prefix}IconNames = ${JSON.stringify(artifact.icons.map((icon) => icon.name), null, 2)}`, '', `export const ${prefix}IconIds = ${JSON.stringify(Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.id])), null, 2)}`, ''].join('\n') +} + +function component(config: ResolvedSpriteConfig): string { + const name = `${pascal(config.name)}Icon` + const ids = `${camel(config.name)}IconIds` + return [header(config.generatedNotice), "import { jsx } from 'react/jsx-runtime'", "import styles from './react-component.module.css'", `import { ${ids} } from '../icon-data.js'`, '', "const spriteUrl = new URL('../sprite.svg', import.meta.url).href", '', `export const ${name} = (props) => {`, ' const { icon, wrapped, className, ...rest } = props', ` const href = spriteUrl + '#' + ${ids}[icon]`, ' if (wrapped) {', " return jsx('span', {", ' ...rest,', " className: [styles.wrap, className].filter(Boolean).join(' '),", " children: jsx('svg', { children: jsx('use', { href }) }),", ' })', ' }', " return jsx('svg', {", ' ...rest,', " className: [styles.root, className].filter(Boolean).join(' '),", " children: jsx('use', { href }),", ' })', '}', ''].join('\n') +} + +function iconDeclarations(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const names = `${camel(config.name)}IconNames` + const ids = `${camel(config.name)}IconIds` + return [header(config.generatedNotice), '', ...docs(config), `export declare const ${names}: readonly [`, ...artifact.icons.map((icon) => ` ${JSON.stringify(icon.name)},`), ']', `export type ${typeName} = typeof ${names}[number]`, `export declare const ${ids}: {`, ...artifact.icons.map((icon) => ` readonly ${JSON.stringify(icon.name)}: ${JSON.stringify(icon.id)}`), '}', ''].join('\n') +} + +function componentDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const styleName = `${componentName}Style` + const propsName = `${componentName}Props` + return [header(config.generatedNotice), "import type { CSSProperties, HTMLAttributes, ReactElement, SVGAttributes } from 'react'", `import type { ${typeName} } from '../icon-data.js'`, '', `export type ${styleName} = CSSProperties & Partial>`, `type ${componentName}BaseProps = { icon: ${typeName} }`, `type ${componentName}SvgProps = ${componentName}BaseProps & { wrapped?: false } & Omit, 'style'> & { style?: ${styleName} }`, `type ${componentName}WrappedProps = ${componentName}BaseProps & { wrapped: true } & Omit, 'style'> & { style?: ${styleName} }`, `export type ${propsName} = ${componentName}SvgProps | ${componentName}WrappedProps`, `export declare const ${componentName}: (props: ${propsName}) => ReactElement`, ''].join('\n') +} + +function indexDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const names = `${camel(config.name)}IconNames` + return [header(config.generatedNotice), `export { ${componentName} } from './react/react-component.js'`, `export type { ${componentName}Props, ${componentName}Style } from './react/react-component.js'`, `export { ${names} } from './icon-data.js'`, `export type { ${componentName}Name } from './icon-data.js'`, ''].join('\n') +} + +function index(config: ResolvedSpriteConfig): string { + return [header(config.generatedNotice), `export { ${pascal(config.name)}Icon } from './react/react-component.js'`, `export { ${camel(config.name)}IconNames } from './icon-data.js'`, ''].join('\n') +} + +function styles(config: ResolvedSpriteConfig): string { + const transition = config.transform.addTransition ? [' transition-property: fill, stroke, color;', ' transition-duration: 0.3s;', ' transition-timing-function: ease;'] : [] + return [header(config.generatedNotice), '', '.root {', ...transition, '}', '', '.wrap {', ' display: inline-flex;', '}', '', '.wrap svg {', ' width: 100%;', ' height: 100%;', ...transition, '}', ''].join('\n') +} + +function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const data = { schemaVersion: 1, generator: '@gromlab/svg-sprites', name: config.name, ...(config.description === undefined ? {} : { description: config.description }), componentName: `${pascal(config.name)}Icon`, mode: MODE, target: MODE, format: artifact.format, iconCount: artifact.icons.length, spriteUrl: '__SPRITE_URL__', icons: artifact.icons } + const source = JSON.stringify(data, null, 2).replace('"__SPRITE_URL__"', 'spriteUrl') + return [header(config.generatedNotice), "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", '', `export const spriteManifest = ${source}`, '', 'export default spriteManifest', ''].join('\n') +} + +const manifestTypes = (config: ResolvedSpriteConfig) => [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') + +export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { + return [ + { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config) }, + { path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: markedSvg(artifact.bytes, config.generatedNotice) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: iconData(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.d.ts'), content: iconDeclarations(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.js'), content: component(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.d.ts'), content: componentDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.module.css'), content: styles(config) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.js'), content: manifest(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.d.ts'), content: manifestTypes(config) }, + ] +} diff --git a/src/modes/next-pages-turbopack/adapter.ts b/src/modes/next-pages-turbopack/adapter.ts new file mode 100644 index 0000000..8f02f7e --- /dev/null +++ b/src/modes/next-pages-turbopack/adapter.ts @@ -0,0 +1,18 @@ +import { compileSpriteContent } from '../../compiler.js' +import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import type { ModeAdapter } from '../../core/mode-adapter.js' +import { generateOutputFiles } from './output.js' + +export const nextPagesTurbopackAdapter: ModeAdapter<'next@pages/turbopack'> = { + mode: 'next@pages/turbopack', + contractVersion: 5, + async generate(context) { + const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true }) + const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') + return { + files: generateOutputFiles(context.config, artifact), + paths: { generatedDir: '.svg-sprite', sprite: '.svg-sprite/sprite.svg', manifest: '.svg-sprite/svg-sprite.manifest.js', entry: '.svg-sprite/index.js' }, + result: { target: 'next@pages/turbopack', router: 'pages', bundler: 'turbopack' }, + } + }, +} diff --git a/src/modes/next-pages-turbopack/output.ts b/src/modes/next-pages-turbopack/output.ts new file mode 100644 index 0000000..816f987 --- /dev/null +++ b/src/modes/next-pages-turbopack/output.ts @@ -0,0 +1,88 @@ +import path from 'node:path' +import type { CompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import { GENERATED_MARKER } from '../../core/generated-markers.js' +import type { GeneratedFile } from '../../core/mode-adapter.js' +import type { ResolvedSpriteConfig } from '../../types.js' + +const MODE = 'next@pages/turbopack' +const OUTPUT_DIR = '.svg-sprite' +const NOTICE = 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ. Не редактируйте вручную.' +const pascal = (value: string) => value.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('') +const camel = (value: string) => value.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()) +const header = (enabled: boolean) => enabled ? `/*\n * ${NOTICE}\n * ${GENERATED_MARKER}.\n */` : `/* ${GENERATED_MARKER}. Do not edit. */` + +function markedSvg(bytes: Uint8Array, enabled: boolean): string { + const marker = enabled ? `${NOTICE}\n ${GENERATED_MARKER}.` : `${GENERATED_MARKER}. Do not edit.` + const content = new TextDecoder().decode(bytes) + return content.startsWith(')\s*/, `$1\n\n`) : `\n${content}` +} + +function docs(config: ResolvedSpriteConfig): string[] { + const text = config.description ?? `Имена иконок SVG-спрайта «${config.name}».` + return ['/**', ...text.split(/\r?\n/).map((line) => ` * ${line.replace(/\*\//g, '* /')}`), ' */'] +} + +function iconData(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const prefix = camel(config.name) + return [header(config.generatedNotice), '', ...docs(config), `export const ${prefix}IconNames = ${JSON.stringify(artifact.icons.map((icon) => icon.name), null, 2)}`, '', `export const ${prefix}IconIds = ${JSON.stringify(Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.id])), null, 2)}`, ''].join('\n') +} + +function component(config: ResolvedSpriteConfig): string { + const name = `${pascal(config.name)}Icon` + const ids = `${camel(config.name)}IconIds` + return [header(config.generatedNotice), "import { jsx } from 'react/jsx-runtime'", "import styles from './react-component.module.css'", `import { ${ids} } from '../icon-data.js'`, '', "const spriteUrl = new URL('../sprite.svg', import.meta.url).href", '', `export const ${name} = (props) => {`, ' const { icon, wrapped, className, ...rest } = props', ` const href = spriteUrl + '#' + ${ids}[icon]`, ' if (wrapped) {', " return jsx('span', {", ' ...rest,', " className: [styles.wrap, className].filter(Boolean).join(' '),", " children: jsx('svg', { children: jsx('use', { href }) }),", ' })', ' }', " return jsx('svg', {", ' ...rest,', " className: [styles.root, className].filter(Boolean).join(' '),", " children: jsx('use', { href }),", ' })', '}', ''].join('\n') +} + +function iconDeclarations(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const names = `${camel(config.name)}IconNames` + const ids = `${camel(config.name)}IconIds` + return [header(config.generatedNotice), '', ...docs(config), `export declare const ${names}: readonly [`, ...artifact.icons.map((icon) => ` ${JSON.stringify(icon.name)},`), ']', `export type ${typeName} = typeof ${names}[number]`, `export declare const ${ids}: {`, ...artifact.icons.map((icon) => ` readonly ${JSON.stringify(icon.name)}: ${JSON.stringify(icon.id)}`), '}', ''].join('\n') +} + +function componentDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const styleName = `${componentName}Style` + const propsName = `${componentName}Props` + return [header(config.generatedNotice), "import type { CSSProperties, HTMLAttributes, ReactElement, SVGAttributes } from 'react'", `import type { ${typeName} } from '../icon-data.js'`, '', `export type ${styleName} = CSSProperties & Partial>`, `type ${componentName}BaseProps = { icon: ${typeName} }`, `type ${componentName}SvgProps = ${componentName}BaseProps & { wrapped?: false } & Omit, 'style'> & { style?: ${styleName} }`, `type ${componentName}WrappedProps = ${componentName}BaseProps & { wrapped: true } & Omit, 'style'> & { style?: ${styleName} }`, `export type ${propsName} = ${componentName}SvgProps | ${componentName}WrappedProps`, `export declare const ${componentName}: (props: ${propsName}) => ReactElement`, ''].join('\n') +} + +function indexDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const names = `${camel(config.name)}IconNames` + return [header(config.generatedNotice), `export { ${componentName} } from './react/react-component.js'`, `export type { ${componentName}Props, ${componentName}Style } from './react/react-component.js'`, `export { ${names} } from './icon-data.js'`, `export type { ${componentName}Name } from './icon-data.js'`, ''].join('\n') +} + +function index(config: ResolvedSpriteConfig): string { + return [header(config.generatedNotice), `export { ${pascal(config.name)}Icon } from './react/react-component.js'`, `export { ${camel(config.name)}IconNames } from './icon-data.js'`, ''].join('\n') +} + +function styles(config: ResolvedSpriteConfig): string { + const transition = config.transform.addTransition ? [' transition-property: fill, stroke, color;', ' transition-duration: 0.3s;', ' transition-timing-function: ease;'] : [] + return [header(config.generatedNotice), '', '.root {', ...transition, '}', '', '.wrap {', ' display: inline-flex;', '}', '', '.wrap svg {', ' width: 100%;', ' height: 100%;', ...transition, '}', ''].join('\n') +} + +function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const data = { schemaVersion: 1, generator: '@gromlab/svg-sprites', name: config.name, ...(config.description === undefined ? {} : { description: config.description }), componentName: `${pascal(config.name)}Icon`, mode: MODE, target: MODE, format: artifact.format, iconCount: artifact.icons.length, spriteUrl: '__SPRITE_URL__', icons: artifact.icons } + const source = JSON.stringify(data, null, 2).replace('"__SPRITE_URL__"', 'spriteUrl') + return [header(config.generatedNotice), "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", '', `export const spriteManifest = ${source}`, '', 'export default spriteManifest', ''].join('\n') +} + +const manifestTypes = (config: ResolvedSpriteConfig) => [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') + +export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { + return [ + { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config) }, + { path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: markedSvg(artifact.bytes, config.generatedNotice) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: iconData(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.d.ts'), content: iconDeclarations(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.js'), content: component(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.d.ts'), content: componentDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.module.css'), content: styles(config) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.js'), content: manifest(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.d.ts'), content: manifestTypes(config) }, + ] +} diff --git a/src/modes/next-pages-webpack/adapter.ts b/src/modes/next-pages-webpack/adapter.ts new file mode 100644 index 0000000..a09142c --- /dev/null +++ b/src/modes/next-pages-webpack/adapter.ts @@ -0,0 +1,18 @@ +import { compileSpriteContent } from '../../compiler.js' +import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import type { ModeAdapter } from '../../core/mode-adapter.js' +import { generateOutputFiles } from './output.js' + +export const nextPagesWebpackAdapter: ModeAdapter<'next@pages/webpack'> = { + mode: 'next@pages/webpack', + contractVersion: 5, + async generate(context) { + const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true }) + const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') + return { + files: generateOutputFiles(context.config, artifact), + paths: { generatedDir: '.svg-sprite', sprite: '.svg-sprite/sprite.svg', manifest: '.svg-sprite/svg-sprite.manifest.js', entry: '.svg-sprite/index.js' }, + result: { target: 'next@pages/webpack', router: 'pages', bundler: 'webpack' }, + } + }, +} diff --git a/src/modes/next-pages-webpack/output.ts b/src/modes/next-pages-webpack/output.ts new file mode 100644 index 0000000..431cfeb --- /dev/null +++ b/src/modes/next-pages-webpack/output.ts @@ -0,0 +1,88 @@ +import path from 'node:path' +import type { CompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import { GENERATED_MARKER } from '../../core/generated-markers.js' +import type { GeneratedFile } from '../../core/mode-adapter.js' +import type { ResolvedSpriteConfig } from '../../types.js' + +const MODE = 'next@pages/webpack' +const OUTPUT_DIR = '.svg-sprite' +const NOTICE = 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ. Не редактируйте вручную.' +const pascal = (value: string) => value.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('') +const camel = (value: string) => value.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()) +const header = (enabled: boolean) => enabled ? `/*\n * ${NOTICE}\n * ${GENERATED_MARKER}.\n */` : `/* ${GENERATED_MARKER}. Do not edit. */` + +function markedSvg(bytes: Uint8Array, enabled: boolean): string { + const marker = enabled ? `${NOTICE}\n ${GENERATED_MARKER}.` : `${GENERATED_MARKER}. Do not edit.` + const content = new TextDecoder().decode(bytes) + return content.startsWith(')\s*/, `$1\n\n`) : `\n${content}` +} + +function docs(config: ResolvedSpriteConfig): string[] { + const text = config.description ?? `Имена иконок SVG-спрайта «${config.name}».` + return ['/**', ...text.split(/\r?\n/).map((line) => ` * ${line.replace(/\*\//g, '* /')}`), ' */'] +} + +function iconData(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const prefix = camel(config.name) + return [header(config.generatedNotice), '', ...docs(config), `export const ${prefix}IconNames = ${JSON.stringify(artifact.icons.map((icon) => icon.name), null, 2)}`, '', `export const ${prefix}IconIds = ${JSON.stringify(Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.id])), null, 2)}`, ''].join('\n') +} + +function component(config: ResolvedSpriteConfig): string { + const name = `${pascal(config.name)}Icon` + const ids = `${camel(config.name)}IconIds` + return [header(config.generatedNotice), "import { jsx } from 'react/jsx-runtime'", "import styles from './react-component.module.css'", `import { ${ids} } from '../icon-data.js'`, '', "const spriteUrl = new URL('../sprite.svg', import.meta.url).href", '', `export const ${name} = (props) => {`, ' const { icon, wrapped, className, ...rest } = props', ` const href = spriteUrl + '#' + ${ids}[icon]`, ' if (wrapped) {', " return jsx('span', {", ' ...rest,', " className: [styles.wrap, className].filter(Boolean).join(' '),", " children: jsx('svg', { children: jsx('use', { href }) }),", ' })', ' }', " return jsx('svg', {", ' ...rest,', " className: [styles.root, className].filter(Boolean).join(' '),", " children: jsx('use', { href }),", ' })', '}', ''].join('\n') +} + +function iconDeclarations(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const names = `${camel(config.name)}IconNames` + const ids = `${camel(config.name)}IconIds` + return [header(config.generatedNotice), '', ...docs(config), `export declare const ${names}: readonly [`, ...artifact.icons.map((icon) => ` ${JSON.stringify(icon.name)},`), ']', `export type ${typeName} = typeof ${names}[number]`, `export declare const ${ids}: {`, ...artifact.icons.map((icon) => ` readonly ${JSON.stringify(icon.name)}: ${JSON.stringify(icon.id)}`), '}', ''].join('\n') +} + +function componentDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const styleName = `${componentName}Style` + const propsName = `${componentName}Props` + return [header(config.generatedNotice), "import type { CSSProperties, HTMLAttributes, ReactElement, SVGAttributes } from 'react'", `import type { ${typeName} } from '../icon-data.js'`, '', `export type ${styleName} = CSSProperties & Partial>`, `type ${componentName}BaseProps = { icon: ${typeName} }`, `type ${componentName}SvgProps = ${componentName}BaseProps & { wrapped?: false } & Omit, 'style'> & { style?: ${styleName} }`, `type ${componentName}WrappedProps = ${componentName}BaseProps & { wrapped: true } & Omit, 'style'> & { style?: ${styleName} }`, `export type ${propsName} = ${componentName}SvgProps | ${componentName}WrappedProps`, `export declare const ${componentName}: (props: ${propsName}) => ReactElement`, ''].join('\n') +} + +function indexDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const names = `${camel(config.name)}IconNames` + return [header(config.generatedNotice), `export { ${componentName} } from './react/react-component.js'`, `export type { ${componentName}Props, ${componentName}Style } from './react/react-component.js'`, `export { ${names} } from './icon-data.js'`, `export type { ${componentName}Name } from './icon-data.js'`, ''].join('\n') +} + +function index(config: ResolvedSpriteConfig): string { + return [header(config.generatedNotice), `export { ${pascal(config.name)}Icon } from './react/react-component.js'`, `export { ${camel(config.name)}IconNames } from './icon-data.js'`, ''].join('\n') +} + +function styles(config: ResolvedSpriteConfig): string { + const transition = config.transform.addTransition ? [' transition-property: fill, stroke, color;', ' transition-duration: 0.3s;', ' transition-timing-function: ease;'] : [] + return [header(config.generatedNotice), '', '.root {', ...transition, '}', '', '.wrap {', ' display: inline-flex;', '}', '', '.wrap svg {', ' width: 100%;', ' height: 100%;', ...transition, '}', ''].join('\n') +} + +function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const data = { schemaVersion: 1, generator: '@gromlab/svg-sprites', name: config.name, ...(config.description === undefined ? {} : { description: config.description }), componentName: `${pascal(config.name)}Icon`, mode: MODE, target: MODE, format: artifact.format, iconCount: artifact.icons.length, spriteUrl: '__SPRITE_URL__', icons: artifact.icons } + const source = JSON.stringify(data, null, 2).replace('"__SPRITE_URL__"', 'spriteUrl') + return [header(config.generatedNotice), "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", '', `export const spriteManifest = ${source}`, '', 'export default spriteManifest', ''].join('\n') +} + +const manifestTypes = (config: ResolvedSpriteConfig) => [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') + +export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { + return [ + { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config) }, + { path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: markedSvg(artifact.bytes, config.generatedNotice) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: iconData(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.d.ts'), content: iconDeclarations(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.js'), content: component(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.d.ts'), content: componentDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.module.css'), content: styles(config) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.js'), content: manifest(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.d.ts'), content: manifestTypes(config) }, + ] +} diff --git a/src/modes/next/generate.ts b/src/modes/next/generate.ts deleted file mode 100644 index d1422cb..0000000 --- a/src/modes/next/generate.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { NextAssetTarget } from '../../targets/types.js' -import { generateSpriteModule } from '../react/module-generator.js' -import type { - NextSpriteGenerationOptions, - NextSpriteGenerationResult, -} from './types.js' - -/** Генерирует Next.js sprite-модуль для явно выбранных роутера и сборщика. */ -export async function generateNextSprite( - root: string, - options: NextSpriteGenerationOptions, -): Promise { - if (!options || (options.router !== 'app' && options.router !== 'pages')) { - throw new Error(`Unsupported Next.js router: ${String(options?.router)}`) - } - if (options.bundler !== 'turbopack' && options.bundler !== 'webpack') { - throw new Error(`Unsupported Next.js bundler: ${String(options.bundler)}`) - } - - const { router, bundler } = options - const target: NextAssetTarget = `next@${router}/${bundler}` - const result = await generateSpriteModule(root, target, { - mode: target, - rootViewBox: true, - }) - - return { - ...result, - router, - bundler, - } -} diff --git a/src/modes/next/index.ts b/src/modes/next/index.ts deleted file mode 100644 index f94e9c9..0000000 --- a/src/modes/next/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { generateNextSprite } from './generate.js' -export type { - NextSpriteConfig, - NextSpriteGenerationOptions, - NextSpriteGenerationResult, -} from './types.js' diff --git a/src/modes/next/types.ts b/src/modes/next/types.ts deleted file mode 100644 index 2185a6b..0000000 --- a/src/modes/next/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { - NextAssetTarget, - NextBundler, - NextRouter, -} from '../../targets/types.js' -import type { - ReactSpriteConfig, - SpriteModuleGenerationResult, -} from '../react/types.js' - -/** Конфигурация Next.js sprite-модуля. Совпадает с React-конфигурацией. */ -export type NextSpriteConfig = ReactSpriteConfig - -export type NextSpriteGenerationOptions = { - router: NextRouter - bundler: NextBundler -} - -export type NextSpriteGenerationResult = SpriteModuleGenerationResult & { - router: NextRouter - bundler: NextBundler -} diff --git a/src/modes/react-vite/adapter.ts b/src/modes/react-vite/adapter.ts new file mode 100644 index 0000000..976f41a --- /dev/null +++ b/src/modes/react-vite/adapter.ts @@ -0,0 +1,29 @@ +import { compileSpriteContent } from '../../compiler.js' +import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import type { ModeAdapter } from '../../core/mode-adapter.js' +import { generateOutputFiles } from './output.js' + +export const reactViteAdapter: ModeAdapter<'react@vite'> = { + mode: 'react@vite', + contractVersion: 5, + + async generate(context) { + const bytes = await compileSpriteContent( + context.prepared.folder, + context.config.transform, + { rootViewBox: false }, + ) + const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') + + return { + files: generateOutputFiles(context.config, artifact), + paths: { + generatedDir: '.svg-sprite', + sprite: '.svg-sprite/sprite.svg', + manifest: '.svg-sprite/svg-sprite.manifest.js', + entry: '.svg-sprite/index.js', + }, + result: { target: 'vite' }, + } + }, +} diff --git a/src/modes/react-vite/output.ts b/src/modes/react-vite/output.ts new file mode 100644 index 0000000..c63a1af --- /dev/null +++ b/src/modes/react-vite/output.ts @@ -0,0 +1,268 @@ +import path from 'node:path' +import type { CompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import { GENERATED_MARKER } from '../../core/generated-markers.js' +import type { GeneratedFile } from '../../core/mode-adapter.js' +import type { ResolvedSpriteConfig } from '../../types.js' + +const MODE = 'react@vite' +const TARGET = 'vite' +const OUTPUT_DIR = '.svg-sprite' +const GENERATED_NOTICE = [ + '----------------------------------------------------------------------', + '## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##', + '## ##', + '## Не редактируйте вручную: изменения будут перезаписаны. ##', + '## Для изменений перегенерируйте SVG-спрайт. ##', + '## ##', + '## Генератор: @gromlab/svg-sprites ##', + '## Репозиторий: https://github.com/gromlab-ru/svg-sprites ##', + '----------------------------------------------------------------------', +] + +function toPascalCase(value: string): string { + return value.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('') +} + +function toCamelCase(value: string): string { + return value.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()) +} + +function blockHeader(enabled: boolean): string { + if (!enabled) return `/* ${GENERATED_MARKER}. Do not edit. */` + return ['/*', ...GENERATED_NOTICE.map((line) => ` * ${line}`), ' */'].join('\n') +} + +function svgHeader(enabled: boolean): string { + if (!enabled) return `` + return [''].join('\n') +} + +function formatSvg(content: string): string { + let depth = 0 + const lines = content.replace(/>\n<').split('\n') + const formatted = lines.map((line) => { + const trimmed = line.trim() + const tags = trimmed.match(/<[^>]+>/g) ?? [] + const startsWithClosingTag = trimmed.startsWith('')) { + depth += 1 + } + } + return formattedLine + }) + return `${formatted.join('\n')}\n` +} + +function markedSvg(bytes: Uint8Array, notice: boolean): string { + const svg = formatSvg(new TextDecoder().decode(bytes)) + const header = svgHeader(notice) + return svg.startsWith(')\s*/, `$1\n${header}\n`) + : `${header}\n${svg}` +} + +function descriptionComment(name: string, description?: string): string[] { + const text = description ?? `Имена иконок SVG-спрайта «${name}».` + return ['/**', ...text.split(/\r?\n/).map((line) => ` * ${line.replace(/\*\//g, '* /')}`), ' */'] +} + +function generateIconData(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const camelName = toCamelCase(config.name) + const names = artifact.icons.map((icon) => icon.name) + const ids = Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.id])) + return [ + blockHeader(config.generatedNotice), + '', + ...descriptionComment(config.name, config.description), + `export const ${camelName}IconNames = ${JSON.stringify(names, null, 2)}`, + '', + `export const ${camelName}IconIds = ${JSON.stringify(ids, null, 2)}`, + '', + ].join('\n') +} + +function generateComponent(config: ResolvedSpriteConfig): string { + const pascalName = toPascalCase(config.name) + const camelName = toCamelCase(config.name) + return [ + blockHeader(config.generatedNotice), + "import { jsx } from 'react/jsx-runtime'", + "import styles from './react-component.module.css'", + `import { ${camelName}IconIds } from '../icon-data.js'`, + "import spriteUrl from '../sprite.svg?no-inline'", + '', + `/** Иконка из SVG-спрайта «${config.name}». */`, + `export const ${pascalName}Icon = (props) => {`, + ' const { icon, wrapped, className, ...rest } = props', + ` const href = spriteUrl + '#' + ${camelName}IconIds[icon]`, + '', + ' if (wrapped) {', + " return jsx('span', {", + ' ...rest,', + " className: [styles.wrap, className].filter(Boolean).join(' '),", + " children: jsx('svg', { children: jsx('use', { href }) }),", + ' })', + ' }', + '', + " return jsx('svg', {", + ' ...rest,', + " className: [styles.root, className].filter(Boolean).join(' '),", + " children: jsx('use', { href }),", + ' })', + '}', + '', + ].join('\n') +} + +function generateIconDeclarations(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const pascalName = toPascalCase(config.name) + const camelName = toCamelCase(config.name) + const tuple = artifact.icons.map((icon) => ` ${JSON.stringify(icon.name)},`).join('\n') + const ids = artifact.icons.map((icon) => ` readonly ${JSON.stringify(icon.name)}: ${JSON.stringify(icon.id)}`).join('\n') + return [ + blockHeader(config.generatedNotice), + '', + ...descriptionComment(config.name, config.description), + `export declare const ${camelName}IconNames: readonly [`, + tuple, + ']', + '', + `export type ${pascalName}IconName = typeof ${camelName}IconNames[number]`, + `export declare const ${camelName}IconIds: {`, + ids, + '}', + '', + ].join('\n') +} + +function generateComponentDeclarations(config: ResolvedSpriteConfig): string { + const pascalName = toPascalCase(config.name) + return [ + blockHeader(config.generatedNotice), + "import type { CSSProperties, HTMLAttributes, ReactElement, SVGAttributes } from 'react'", + `import type { ${pascalName}IconName } from '../icon-data.js'`, + '', + `export type ${pascalName}IconStyle = CSSProperties & Partial>`, + '', + `type ${pascalName}IconBaseProps = { icon: ${pascalName}IconName }`, + `type ${pascalName}IconSvgProps = ${pascalName}IconBaseProps & { wrapped?: false } & Omit, 'style'> & { style?: ${pascalName}IconStyle }`, + `type ${pascalName}IconWrappedProps = ${pascalName}IconBaseProps & { wrapped: true } & Omit, 'style'> & { style?: ${pascalName}IconStyle }`, + `export type ${pascalName}IconProps = ${pascalName}IconSvgProps | ${pascalName}IconWrappedProps`, + '', + `export declare const ${pascalName}Icon: (props: ${pascalName}IconProps) => ReactElement`, + '', + ].join('\n') +} + +function generateIndexDeclarations(config: ResolvedSpriteConfig): string { + const pascalName = toPascalCase(config.name) + const camelName = toCamelCase(config.name) + return [ + blockHeader(config.generatedNotice), + `export { ${pascalName}Icon } from './react/react-component.js'`, + `export type { ${pascalName}IconProps, ${pascalName}IconStyle } from './react/react-component.js'`, + `export { ${camelName}IconNames } from './icon-data.js'`, + `export type { ${pascalName}IconName } from './icon-data.js'`, + '', + ].join('\n') +} + +function generateIndex(config: ResolvedSpriteConfig): string { + const pascalName = toPascalCase(config.name) + const camelName = toCamelCase(config.name) + return [ + blockHeader(config.generatedNotice), + `export { ${pascalName}Icon } from './react/react-component.js'`, + `export { ${camelName}IconNames } from './icon-data.js'`, + '', + ].join('\n') +} + +function generateStyles(config: ResolvedSpriteConfig): string { + const transition = config.transform.addTransition + ? [' transition-property: fill, stroke, color;', ' transition-duration: 0.3s;', ' transition-timing-function: ease;'] + : [] + return [ + blockHeader(config.generatedNotice), + '', + '.root {', + ...transition, + '}', + '', + '.wrap {', + ' display: inline-flex;', + '}', + '', + '.wrap svg {', + ' width: 100%;', + ' height: 100%;', + ...transition, + '}', + '', + ].join('\n') +} + +function manifestData(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact) { + return { + schemaVersion: 1, + generator: '@gromlab/svg-sprites', + name: config.name, + ...(config.description === undefined ? {} : { description: config.description }), + componentName: `${toPascalCase(config.name)}Icon`, + mode: MODE, + target: TARGET, + format: artifact.format, + iconCount: artifact.icons.length, + spriteUrl: '__SPRITE_URL__', + icons: artifact.icons, + } +} + +function generateManifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const serialized = JSON.stringify(manifestData(config, artifact), null, 2) + .replace('"__SPRITE_URL__"', 'spriteUrl') + return [ + blockHeader(config.generatedNotice), + "import spriteUrl from './sprite.svg?no-inline'", + '', + `export const spriteManifest = ${serialized}`, + '', + 'export default spriteManifest', + '', + ].join('\n') +} + +function generateManifestDeclarations(config: ResolvedSpriteConfig): string { + return [ + blockHeader(config.generatedNotice), + "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", + '', + 'export declare const spriteManifest: SpriteManifest', + 'export default spriteManifest', + '', + ].join('\n') +} + +export function generateOutputFiles( + config: ResolvedSpriteConfig, + artifact: CompiledSpriteArtifact, +): GeneratedFile[] { + return [ + { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: generateIndex(config) }, + { path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: generateIndexDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: markedSvg(artifact.bytes, config.generatedNotice) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: generateIconData(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.d.ts'), content: generateIconDeclarations(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.js'), content: generateComponent(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.d.ts'), content: generateComponentDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.module.css'), content: generateStyles(config) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.js'), content: generateManifest(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.d.ts'), content: generateManifestDeclarations(config) }, + ] +} diff --git a/src/modes/react-webpack/adapter.ts b/src/modes/react-webpack/adapter.ts new file mode 100644 index 0000000..cebb82e --- /dev/null +++ b/src/modes/react-webpack/adapter.ts @@ -0,0 +1,26 @@ +import { compileSpriteContent } from '../../compiler.js' +import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import type { ModeAdapter } from '../../core/mode-adapter.js' +import { generateOutputFiles } from './output.js' + +export const reactWebpackAdapter: ModeAdapter<'react@webpack'> = { + mode: 'react@webpack', + contractVersion: 5, + + async generate(context) { + const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { + rootViewBox: false, + }) + const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') + return { + files: generateOutputFiles(context.config, artifact), + paths: { + generatedDir: '.svg-sprite', + sprite: '.svg-sprite/sprite.svg', + manifest: '.svg-sprite/svg-sprite.manifest.js', + entry: '.svg-sprite/index.js', + }, + result: { target: 'webpack' }, + } + }, +} diff --git a/src/modes/react-webpack/output.ts b/src/modes/react-webpack/output.ts new file mode 100644 index 0000000..425323e --- /dev/null +++ b/src/modes/react-webpack/output.ts @@ -0,0 +1,187 @@ +import path from 'node:path' +import type { CompiledSpriteArtifact } from '../../core/compiled-artifact.js' +import { GENERATED_MARKER } from '../../core/generated-markers.js' +import type { GeneratedFile } from '../../core/mode-adapter.js' +import type { ResolvedSpriteConfig } from '../../types.js' + +const MODE = 'react@webpack' +const TARGET = 'webpack' +const OUTPUT_DIR = '.svg-sprite' +const NOTICE = 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ. Не редактируйте вручную.' + +function pascal(value: string): string { + return value.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('') +} + +function camel(value: string): string { + return value.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()) +} + +function header(enabled: boolean): string { + return enabled + ? `/*\n * ${NOTICE}\n * ${GENERATED_MARKER}.\n */` + : `/* ${GENERATED_MARKER}. Do not edit. */` +} + +function svg(bytes: Uint8Array, enabled: boolean): string { + const marker = enabled ? `${NOTICE}\n ${GENERATED_MARKER}.` : `${GENERATED_MARKER}. Do not edit.` + const content = new TextDecoder().decode(bytes) + return content.startsWith(')\s*/, `$1\n\n`) + : `\n${content}` +} + +function comment(config: ResolvedSpriteConfig): string[] { + const text = config.description ?? `Имена иконок SVG-спрайта «${config.name}».` + return ['/**', ...text.split(/\r?\n/).map((line) => ` * ${line.replace(/\*\//g, '* /')}`), ' */'] +} + +function iconData(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const prefix = camel(config.name) + return [ + header(config.generatedNotice), + '', + ...comment(config), + `export const ${prefix}IconNames = ${JSON.stringify(artifact.icons.map((icon) => icon.name), null, 2)}`, + '', + `export const ${prefix}IconIds = ${JSON.stringify(Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.id])), null, 2)}`, + '', + ].join('\n') +} + +function component(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const ids = `${camel(config.name)}IconIds` + return [ + header(config.generatedNotice), + "import { jsx } from 'react/jsx-runtime'", + "import styles from './react-component.module.css'", + `import { ${ids} } from '../icon-data.js'`, + '', + "const spriteUrl = new URL('../sprite.svg', import.meta.url).href", + '', + `export const ${componentName} = (props) => {`, + ' const { icon, wrapped, className, ...rest } = props', + ` const href = spriteUrl + '#' + ${ids}[icon]`, + ' if (wrapped) {', + " return jsx('span', {", + ' ...rest,', + " className: [styles.wrap, className].filter(Boolean).join(' '),", + " children: jsx('svg', { children: jsx('use', { href }) }),", + ' })', + ' }', + " return jsx('svg', {", + ' ...rest,', + " className: [styles.root, className].filter(Boolean).join(' '),", + " children: jsx('use', { href }),", + ' })', + '}', + '', + ].join('\n') +} + +function iconDeclarations(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const names = `${camel(config.name)}IconNames` + const ids = `${camel(config.name)}IconIds` + return [ + header(config.generatedNotice), + '', + ...comment(config), + `export declare const ${names}: readonly [`, + ...artifact.icons.map((icon) => ` ${JSON.stringify(icon.name)},`), + ']', + `export type ${typeName} = typeof ${names}[number]`, + `export declare const ${ids}: {`, + ...artifact.icons.map((icon) => ` readonly ${JSON.stringify(icon.name)}: ${JSON.stringify(icon.id)}`), + '}', + '', + ].join('\n') +} + +function componentDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const typeName = `${componentName}Name` + const styleName = `${componentName}Style` + const propsName = `${componentName}Props` + return [ + header(config.generatedNotice), + "import type { CSSProperties, HTMLAttributes, ReactElement, SVGAttributes } from 'react'", + `import type { ${typeName} } from '../icon-data.js'`, + '', + `export type ${styleName} = CSSProperties & Partial>`, + `type ${componentName}BaseProps = { icon: ${typeName} }`, + `type ${componentName}SvgProps = ${componentName}BaseProps & { wrapped?: false } & Omit, 'style'> & { style?: ${styleName} }`, + `type ${componentName}WrappedProps = ${componentName}BaseProps & { wrapped: true } & Omit, 'style'> & { style?: ${styleName} }`, + `export type ${propsName} = ${componentName}SvgProps | ${componentName}WrappedProps`, + `export declare const ${componentName}: (props: ${propsName}) => ReactElement`, + '', + ].join('\n') +} + +function indexDeclarations(config: ResolvedSpriteConfig): string { + const componentName = `${pascal(config.name)}Icon` + const names = `${camel(config.name)}IconNames` + return [ + header(config.generatedNotice), + `export { ${componentName} } from './react/react-component.js'`, + `export type { ${componentName}Props, ${componentName}Style } from './react/react-component.js'`, + `export { ${names} } from './icon-data.js'`, + `export type { ${componentName}Name } from './icon-data.js'`, + '', + ].join('\n') +} + +function index(config: ResolvedSpriteConfig): string { + return [ + header(config.generatedNotice), + `export { ${pascal(config.name)}Icon } from './react/react-component.js'`, + `export { ${camel(config.name)}IconNames } from './icon-data.js'`, + '', + ].join('\n') +} + +function styles(config: ResolvedSpriteConfig): string { + const transition = config.transform.addTransition + ? [' transition-property: fill, stroke, color;', ' transition-duration: 0.3s;', ' transition-timing-function: ease;'] + : [] + return [header(config.generatedNotice), '', '.root {', ...transition, '}', '', '.wrap {', ' display: inline-flex;', '}', '', '.wrap svg {', ' width: 100%;', ' height: 100%;', ...transition, '}', ''].join('\n') +} + +function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { + const data = { + schemaVersion: 1, + generator: '@gromlab/svg-sprites', + name: config.name, + ...(config.description === undefined ? {} : { description: config.description }), + componentName: `${pascal(config.name)}Icon`, + mode: MODE, + target: TARGET, + format: artifact.format, + iconCount: artifact.icons.length, + spriteUrl: '__SPRITE_URL__', + icons: artifact.icons, + } + const source = JSON.stringify(data, null, 2).replace('"__SPRITE_URL__"', 'spriteUrl') + return [header(config.generatedNotice), "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", '', `export const spriteManifest = ${source}`, '', 'export default spriteManifest', ''].join('\n') +} + +function manifestTypes(config: ResolvedSpriteConfig): string { + return [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') +} + +export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { + return [ + { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config) }, + { path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: svg(artifact.bytes, config.generatedNotice) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: iconData(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'icon-data.d.ts'), content: iconDeclarations(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.js'), content: component(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.d.ts'), content: componentDeclarations(config) }, + { path: path.posix.join(OUTPUT_DIR, 'react', 'react-component.module.css'), content: styles(config) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.js'), content: manifest(config, artifact) }, + { path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.d.ts'), content: manifestTypes(config) }, + ] +} diff --git a/src/modes/react/codegen.ts b/src/modes/react/codegen.ts deleted file mode 100644 index bcd95b2..0000000 --- a/src/modes/react/codegen.ts +++ /dev/null @@ -1,288 +0,0 @@ -import path from 'node:path' -import { getSpriteShapeId } from '../../shape-id.js' -import { generateReactAssetUrlCode } from '../../targets/index.js' -import type { SpriteAssetTarget } from '../../targets/types.js' -import type { SpriteFormat } from '../../types.js' -import { generateSpriteManifest } from './manifest.js' -import { toPascalCase } from './naming.js' -import type { ResolvedReactSpriteConfig } from './types.js' - -const GENERATED_MARKER = '@generated by @gromlab/svg-sprites. Do not edit.' -const GENERATED_NOTICE = [ - '----------------------------------------------------------------------', - '## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##', - '## ##', - '## Не редактируйте вручную: изменения будут перезаписаны. ##', - '## Для изменений перегенерируйте SVG-спрайт. ##', - '## ##', - '## Генератор: @gromlab/svg-sprites ##', - '## Репозиторий: https://github.com/gromlab-ru/svg-sprites ##', - '----------------------------------------------------------------------', -] - -type ReactCodegenOptions = { - config: ResolvedReactSpriteConfig - format: SpriteFormat - iconNames: string[] - sprite: Uint8Array - target: SpriteAssetTarget -} - -export type GeneratedFile = { - path: string - content: string | Uint8Array -} - -function toCamelCase(name: string): string { - return name.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()) -} - -function generateBlockHeader(generatedNotice: boolean): string { - if (!generatedNotice) return `/* ${GENERATED_MARKER} */` - - return [ - '/*', - ...GENERATED_NOTICE.map((line) => ` * ${line}`), - ' */', - ].join('\n') -} - -function generateSvgHeader(generatedNotice: boolean): string { - if (!generatedNotice) return `` - - return [ - '', - ].join('\n') -} - -function generateGitignore(): string { - return `# ${GENERATED_MARKER}\n/generated/\n/index.ts\n/manifest.ts\n` -} - -function formatSvg(content: string): string { - let depth = 0 - const lines = content.replace(/>\n<').split('\n') - - const formatted = lines.map((line) => { - const trimmed = line.trim() - const tags = trimmed.match(/<[^>]+>/g) ?? [] - const startsWithClosingTag = trimmed.startsWith('')) { - depth += 1 - } - } - - return formattedLine - }) - - return `${formatted.join('\n')}\n` -} - -function markSvg(content: Uint8Array, generatedNotice: boolean): string { - const svg = formatSvg(new TextDecoder().decode(content)) - const header = generateSvgHeader(generatedNotice) - return svg.startsWith(')\s*/, `$1\n${header}\n`) - : `${header}\n${svg}` -} - -function generateDescriptionComment(name: string, description?: string): string[] { - const text = description ?? `Имена иконок SVG-спрайта «${name}».` - return [ - '/**', - ...text.split(/\r?\n/).map((line) => ` * ${line.replace(/\*\//g, '* /')}`), - ' */', - ] -} - -function generateTypes( - name: string, - description: string | undefined, - iconNames: string[], - generatedNotice: boolean, -): string { - const pascalName = toPascalCase(name) - const camelName = toCamelCase(name) - const iconNamesVariable = `${camelName}IconNames` - - return [ - generateBlockHeader(generatedNotice), - '', - ...generateDescriptionComment(name, description), - `export const ${iconNamesVariable} = [`, - ...iconNames.map((iconName) => ` ${JSON.stringify(iconName)},`), - '] as const', - '', - `export type ${pascalName}IconName = typeof ${iconNamesVariable}[number]`, - '', - ].join('\n') -} - -function generateComponent( - name: string, - generatedNotice: boolean, - target: SpriteAssetTarget, - iconNames: string[], -): string { - const pascalName = toPascalCase(name) - const assetUrlCode = generateReactAssetUrlCode(target, 'sprite.svg') - - return [ - generateBlockHeader(generatedNotice), - "import type { CSSProperties, HTMLAttributes, SVGAttributes } from 'react'", - "import styles from './styles.module.css'", - `import type { ${pascalName}IconName } from './types'`, - ...assetUrlCode.imports, - '', - ...assetUrlCode.declarations, - ...(assetUrlCode.declarations.length > 0 ? [''] : []), - `const iconIds: Record<${pascalName}IconName, string> = {`, - ...iconNames.map((iconName) => ` ${JSON.stringify(iconName)}: ${JSON.stringify(getSpriteShapeId(iconName))},`), - '}', - '', - `export type ${pascalName}IconStyle = CSSProperties & Partial>`, - '', - `type ${pascalName}IconBaseProps = {`, - ` icon: ${pascalName}IconName`, - '}', - '', - `type ${pascalName}IconSvgProps = ${pascalName}IconBaseProps & {`, - ' wrapped?: false', - `} & Omit, 'style'> & {`, - ` style?: ${pascalName}IconStyle`, - '}', - '', - `type ${pascalName}IconWrappedProps = ${pascalName}IconBaseProps & {`, - ' wrapped: true', - `} & Omit, 'style'> & {`, - ` style?: ${pascalName}IconStyle`, - '}', - '', - `export type ${pascalName}IconProps =`, - ` | ${pascalName}IconSvgProps`, - ` | ${pascalName}IconWrappedProps`, - '', - `/** Иконка из SVG-спрайта «${name}». */`, - `export const ${pascalName}Icon = (props: ${pascalName}IconProps) => {`, - ' const { icon, wrapped, className, ...rest } = props', - ` const href = ${assetUrlCode.variableName} + '#' + iconIds[icon]`, - '', - ' if (wrapped) {', - ' const htmlAttributes = rest as HTMLAttributes', - ' return (', - " ", - ' ', - ' ', - ' ', - ' ', - ' )', - ' }', - '', - ' const svgAttributes = rest as SVGAttributes', - ' return (', - " ", - ' ', - ' ', - ' )', - '}', - '', - ].join('\n') -} - -function generateCss(generatedNotice: boolean, addTransition: boolean): string { - const transitionRules = addTransition - ? [ - ' transition-property: fill, stroke, color;', - ' transition-duration: 0.3s;', - ' transition-timing-function: ease;', - ] - : [] - - return [ - generateBlockHeader(generatedNotice), - '', - '.root {', - ...transitionRules, - '}', - '', - '.wrap {', - ' display: inline-flex;', - '}', - '', - '.wrap svg {', - ' width: 100%;', - ' height: 100%;', - ...transitionRules, - '}', - '', - ].join('\n') -} - -function generateRootIndex(name: string, generatedNotice: boolean): string { - const pascalName = toPascalCase(name) - const iconNamesVariable = `${toCamelCase(name)}IconNames` - - return [ - generateBlockHeader(generatedNotice), - `export { ${pascalName}Icon } from './generated/react-component'`, - `export type { ${pascalName}IconProps, ${pascalName}IconStyle } from './generated/react-component'`, - `export { ${iconNamesVariable} } from './generated/types'`, - `export type { ${pascalName}IconName } from './generated/types'`, - '', - ].join('\n') -} - -/** Создаёт все управляемые файлы React sprite-модуля в памяти. */ -export function generateReactFiles(options: ReactCodegenOptions): GeneratedFile[] { - const { config, format, iconNames, sprite, target } = options - const { name, description, generatedNotice } = config - - return [ - { - path: '.gitignore', - content: generateGitignore(), - }, - { - path: path.posix.join('generated', 'sprite.svg'), - content: markSvg(sprite, generatedNotice), - }, - { - path: path.posix.join('generated', 'types.ts'), - content: generateTypes(name, description, iconNames, generatedNotice), - }, - { - path: path.posix.join('generated', 'react-component.tsx'), - content: generateComponent(name, generatedNotice, target, iconNames), - }, - { - path: path.posix.join('generated', 'styles.module.css'), - content: generateCss(generatedNotice, config.transform.addTransition ?? true), - }, - { - path: 'manifest.ts', - content: generateSpriteManifest({ - header: generateBlockHeader(generatedNotice), - name, - description, - format, - iconNames, - sprite, - target, - }), - }, - { - path: 'index.ts', - content: generateRootIndex(name, generatedNotice), - }, - ] -} diff --git a/src/modes/react/config.ts b/src/modes/react/config.ts deleted file mode 100644 index 2329b3c..0000000 --- a/src/modes/react/config.ts +++ /dev/null @@ -1,108 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { createJiti } from 'jiti' -import { toKebabCase, validateSpriteName } from './naming.js' -import type { ReactSpriteConfig, ResolvedReactSpriteConfig } from './types.js' - -export const REACT_CONFIG_FILE = 'svg-sprite.config.ts' - -function getDefaultName(rootDir: string): string { - const rootName = path.basename(rootDir) - const source = rootName === 'svg-sprite' || rootName === 'svg-sprites' - ? path.basename(path.dirname(rootDir)) - : rootName - const name = toKebabCase(source) - - if (!name) { - throw new Error(`Cannot infer sprite name from directory: ${rootDir}`) - } - - return name -} - -/** Загружает локальный React-конфиг из корня sprite-модуля. */ -export async function loadReactSpriteConfig(rootDir: string): Promise { - const configPath = path.join(rootDir, REACT_CONFIG_FILE) - - if (!fs.existsSync(configPath)) { - throw new Error( - `React config file not found: ${configPath}\n` + - `Create ${REACT_CONFIG_FILE} inside the sprite directory.`, - ) - } - - const jiti = createJiti(rootDir) - const mod = await jiti.import(configPath) as { default?: ReactSpriteConfig } - const config = mod.default - - if (!config || typeof config !== 'object') { - throw new Error( - `React config file must have a default export: ${configPath}\n` + - 'Use: export default defineReactSpriteConfig({ ... })', - ) - } - - if (config.name !== undefined && typeof config.name !== 'string') { - throw new Error('React config: "name" must be a string.') - } - - if (config.description !== undefined && typeof config.description !== 'string') { - throw new Error('React config: "description" must be a string.') - } - - if ('icons' in config) { - throw new Error('React config: "icons" was renamed to "inputFolder".') - } - - if (config.inputFolder !== undefined && ( - typeof config.inputFolder !== 'string' || config.inputFolder.trim() === '' - )) { - throw new Error('React config: "inputFolder" must be a non-empty string.') - } - - if (config.inputFiles !== undefined && ( - !Array.isArray(config.inputFiles) - || config.inputFiles.some((filePath) => typeof filePath !== 'string' || filePath.trim() === '') - )) { - throw new Error('React config: "inputFiles" must be an array of non-empty strings.') - } - - if (config.transform !== undefined) { - if ( - config.transform === null - || typeof config.transform !== 'object' - || Array.isArray(config.transform) - ) { - throw new Error('React config: "transform" must be an object.') - } - - for (const option of ['removeSize', 'replaceColors', 'addTransition'] as const) { - if (config.transform[option] !== undefined && typeof config.transform[option] !== 'boolean') { - throw new Error(`React config: "transform.${option}" must be a boolean.`) - } - } - } - - if (config.generatedNotice !== undefined && typeof config.generatedNotice !== 'boolean') { - throw new Error('React config: "generatedNotice" must be a boolean.') - } - - const name = config.name ?? getDefaultName(rootDir) - validateSpriteName(name) - const inputFiles = (config.inputFiles ?? []).map((filePath) => path.resolve(rootDir, filePath)) - const defaultInputFolder = path.resolve(rootDir, 'icons') - const inputFolder = config.inputFolder === undefined - && inputFiles.length > 0 - && !fs.existsSync(defaultInputFolder) - ? null - : path.resolve(rootDir, config.inputFolder ?? 'icons') - - return { - name, - description: config.description, - inputFolder, - inputFiles, - transform: { ...config.transform }, - generatedNotice: config.generatedNotice ?? true, - } -} diff --git a/src/modes/react/generate.ts b/src/modes/react/generate.ts deleted file mode 100644 index b7aef82..0000000 --- a/src/modes/react/generate.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ReactAssetTarget } from '../../targets/types.js' -import { generateSpriteModule } from './module-generator.js' -import type { ReactSpriteGenerationResult } from './types.js' - -/** Генерирует один локальный React sprite-модуль для Vite или Webpack. */ -export function generateReactSprite( - root: string, - target: ReactAssetTarget, -): Promise { - if (target !== 'vite' && target !== 'webpack') { - throw new Error(`Unsupported React asset target: ${String(target)}`) - } - - return generateSpriteModule(root, target, { - mode: `react@${target}`, - rootViewBox: false, - }) -} diff --git a/src/modes/react/index.ts b/src/modes/react/index.ts deleted file mode 100644 index c4e7530..0000000 --- a/src/modes/react/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { generateReactSprite } from './generate.js' -export { loadReactSpriteConfig, REACT_CONFIG_FILE } from './config.js' -export type { - ReactSpriteConfig, - ReactSpriteGenerationResult, - ResolvedReactSpriteConfig, -} from './types.js' diff --git a/src/modes/react/manifest.ts b/src/modes/react/manifest.ts deleted file mode 100644 index 0d84eb3..0000000 --- a/src/modes/react/manifest.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { getSpriteShapeId } from '../../shape-id.js' -import { generateReactAssetUrlCode } from '../../targets/index.js' -import type { SpriteAssetTarget } from '../../targets/types.js' -import type { SpriteFormat } from '../../types.js' -import { toPascalCase } from './naming.js' - -type ManifestIconColor = { - variable: string - fallback: string -} - -type ManifestIcon = { - name: string - id: string - viewBox: string | null - colors: ManifestIconColor[] -} - -type GenerateManifestOptions = { - header: string - name: string - description?: string - format: SpriteFormat - iconNames: string[] - sprite: Uint8Array - target: SpriteAssetTarget -} - -function extractColors(fragment: string): ManifestIconColor[] { - const colors = new Map() - const regex = /var\((--icon-color-\d+),\s*((?:[^()]|\([^()]*\))*)\)/g - let match: RegExpExecArray | null - - while ((match = regex.exec(fragment)) !== null) { - if (!colors.has(match[1])) colors.set(match[1], match[2].trim()) - } - - return [...colors].map(([variable, fallback]) => ({ variable, fallback })) -} - -function extractManifestIcons(sprite: Uint8Array, iconNames: string[]): ManifestIcon[] { - const shapes = new Map() - const content = new TextDecoder().decode(sprite) - const shapeRegex = /<(symbol|svg)\b((?=[^>]*\bid="[^"]+")[^>]*)>[\s\S]*?<\/\1>/g - let match: RegExpExecArray | null - - while ((match = shapeRegex.exec(content)) !== null) { - const attributes = match[2] - const id = attributes.match(/\bid="([^"]+)"/)?.[1] - if (!id) continue - - shapes.set(id, { - viewBox: attributes.match(/\bviewBox="([^"]+)"/)?.[1] ?? null, - fragment: match[0], - }) - } - - return iconNames.map((name) => { - const id = getSpriteShapeId(name) - const shape = shapes.get(id) - if (!shape) throw new Error(`Cannot find SVG shape "${id}" for icon "${name}".`) - - return { - name, - id, - viewBox: shape.viewBox, - colors: extractColors(shape.fragment), - } - }) -} - -function generateIcon(icon: ManifestIcon): string[] { - return [ - ' {', - ` name: ${JSON.stringify(icon.name)},`, - ` id: ${JSON.stringify(icon.id)},`, - ` viewBox: ${JSON.stringify(icon.viewBox)},`, - ' colors: [', - ...icon.colors.flatMap((color) => [ - ' {', - ` variable: ${JSON.stringify(color.variable)},`, - ` fallback: ${JSON.stringify(color.fallback)},`, - ' },', - ]), - ' ],', - ' },', - ] -} - -/** Генерирует отдельную debug-точку входа с метаданными спрайта. */ -export function generateSpriteManifest(options: GenerateManifestOptions): string { - const { header, name, description, format, iconNames, sprite, target } = options - const assetUrlCode = generateReactAssetUrlCode(target, 'generated/sprite.svg') - const icons = extractManifestIcons(sprite, iconNames) - - return [ - header, - ...assetUrlCode.imports, - ...(assetUrlCode.imports.length > 0 ? [''] : []), - ...assetUrlCode.declarations, - ...(assetUrlCode.declarations.length > 0 ? [''] : []), - 'export const spriteManifest = {', - ' schemaVersion: 1,', - " generator: '@gromlab/svg-sprites',", - ` name: ${JSON.stringify(name)},`, - ...(description === undefined ? [] : [` description: ${JSON.stringify(description)},`]), - ` componentName: ${JSON.stringify(`${toPascalCase(name)}Icon`)},`, - ` target: ${JSON.stringify(target)},`, - ` format: ${JSON.stringify(format)},`, - ` iconCount: ${icons.length},`, - ` spriteUrl: ${assetUrlCode.variableName},`, - ' icons: [', - ...icons.flatMap(generateIcon), - ' ],', - '} as const', - '', - 'export default spriteManifest', - '', - ].join('\n') -} diff --git a/src/modes/react/module-generator.ts b/src/modes/react/module-generator.ts deleted file mode 100644 index af0caf4..0000000 --- a/src/modes/react/module-generator.ts +++ /dev/null @@ -1,78 +0,0 @@ -import path from 'node:path' -import { compileSpriteContent } from '../../compiler.js' -import { log } from '../../logger.js' -import { resolveSpriteSources } from '../../scanner.js' -import { getSpriteShapeId } from '../../shape-id.js' -import type { SpriteAssetTarget } from '../../targets/types.js' -import { generateReactFiles } from './codegen.js' -import { loadReactSpriteConfig } from './config.js' -import type { SpriteModuleGenerationResult } from './types.js' -import { writeReactFiles } from './writer.js' - -type GenerateSpriteModuleOptions = { - mode: string - rootViewBox: boolean -} - -function validateIconIds(iconNames: string[]): void { - const namesById = new Map() - - for (const iconName of iconNames) { - const id = getSpriteShapeId(iconName) - const existingName = namesById.get(id) - - if (existingName) { - throw new Error( - `Icons "${existingName}" and "${iconName}" produce the same SVG id "${id}". Rename one of the files.`, - ) - } - - namesById.set(id, iconName) - } -} - -/** Общая генерация типизированного React-компонента для React и Next.js modes. */ -export async function generateSpriteModule( - root: string, - target: TTarget, - options: GenerateSpriteModuleOptions, -): Promise> { - const rootDir = path.resolve(root) - const config = await loadReactSpriteConfig(rootDir) - const format = 'stack' - const folder = resolveSpriteSources({ - name: config.name, - format, - inputFolder: config.inputFolder, - inputFiles: config.inputFiles, - }) - - const iconNames = folder.files - .map((filePath) => path.basename(filePath, '.svg')) - .sort() - validateIconIds(iconNames) - - const sprite = await compileSpriteContent(folder, config.transform, { - rootViewBox: options.rootViewBox, - }) - const files = generateReactFiles({ config, format, iconNames, sprite, target }) - - writeReactFiles(rootDir, files, config.generatedNotice) - - const generatedDir = path.join(rootDir, 'generated') - const spritePath = path.join(generatedDir, 'sprite.svg') - const manifestPath = path.join(rootDir, 'manifest.ts') - const iconLabel = iconNames.length === 1 ? 'icon' : 'icons' - log.success(`✓ ${config.name} · ${iconNames.length} ${iconLabel} · ${options.mode}`) - log.detail(` → ${path.relative(process.cwd(), generatedDir)}`) - - return { - name: config.name, - rootDir, - generatedDir, - spritePath, - manifestPath, - iconCount: iconNames.length, - target, - } -} diff --git a/src/modes/react/naming.ts b/src/modes/react/naming.ts deleted file mode 100644 index 3de2340..0000000 --- a/src/modes/react/naming.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** Преобразует kebab-case имя в PascalCase. */ -export function toPascalCase(value: string): string { - return value - .split('-') - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join('') -} - -/** Преобразует имя папки в допустимое kebab-case имя спрайта. */ -export function toKebabCase(value: string): string { - return value - .normalize('NFKD') - .replace(/([a-z0-9])([A-Z])/g, '$1-$2') - .replace(/[^a-zA-Z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .toLowerCase() -} - -export function validateSpriteName(name: string): void { - if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name)) { - throw new Error( - `React config: "name" must be kebab-case and start with a letter. Received: "${name}".`, - ) - } -} diff --git a/src/modes/react/types.ts b/src/modes/react/types.ts deleted file mode 100644 index 21c5f64..0000000 --- a/src/modes/react/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { - ReactAssetTarget, - SpriteAssetTarget, -} from '../../targets/types.js' -import type { TransformOptions } from '../../types.js' - -export type ReactSpriteConfig = { - /** Логическое имя спрайта. По умолчанию выводится из пути модуля. */ - name?: string - /** Описание спрайта для документации и будущего инспектора. */ - description?: string - /** Папка с исходными SVG относительно svg-sprite.config.ts. По умолчанию: ./icons. */ - inputFolder?: string - /** Дополнительные SVG-файлы относительно svg-sprite.config.ts. По умолчанию: []. */ - inputFiles?: string[] - /** Настройки трансформации SVG. По умолчанию все трансформации включены. */ - transform?: TransformOptions - /** Добавлять развёрнутое предупреждение в generated-файлы. По умолчанию: true. */ - generatedNotice?: boolean -} - -export type ResolvedReactSpriteConfig = { - name: string - description?: string - inputFolder: string | null - inputFiles: string[] - transform: TransformOptions - generatedNotice: boolean -} - -export type SpriteModuleGenerationResult = { - name: string - rootDir: string - generatedDir: string - spritePath: string - manifestPath: string - iconCount: number - /** Среда, для которой сгенерирован способ получения URL SVG asset. */ - target: TTarget -} - -export type ReactSpriteGenerationResult = SpriteModuleGenerationResult diff --git a/src/modes/react/writer.ts b/src/modes/react/writer.ts deleted file mode 100644 index 3f028f1..0000000 --- a/src/modes/react/writer.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { randomUUID } from 'node:crypto' -import fs from 'node:fs' -import path from 'node:path' -import type { GeneratedFile } from './codegen.js' - -const MANIFEST_FILE = '.svg-sprites.manifest.json' -const GENERATOR = '@gromlab/svg-sprites' -const GENERATED_MARKER = '@generated by @gromlab/svg-sprites' -const GENERATED_NOTICE_MARKER = 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ' -const ROOT_MANAGED_FILES = new Set(['.gitignore', 'index.ts', 'manifest.ts']) - -type Manifest = { - version: 1 - generator: typeof GENERATOR - files: string[] - warning?: string -} - -function normalizeManagedPath(relativePath: string): string { - const normalized = relativePath.replaceAll('\\', '/') - const parts = normalized.split('/') - const isRootFile = ROOT_MANAGED_FILES.has(normalized) - const isGeneratedFile = parts.length === 2 - && parts[0] === 'generated' - && parts[1] !== '' - && parts[1] !== '.' - && parts[1] !== '..' - - if (!isRootFile && !isGeneratedFile) { - throw new Error(`Invalid generated file path: ${relativePath}`) - } - - return normalized -} - -function resolveManagedPath(rootDir: string, relativePath: string): string { - const normalized = normalizeManagedPath(relativePath) - const resolved = path.resolve(rootDir, normalized) - const relative = path.relative(rootDir, resolved) - - if (relative.startsWith('..') || path.isAbsolute(relative)) { - throw new Error(`Invalid generated file path: ${relativePath}`) - } - - return resolved -} - -function assertNoSymlinks(rootDir: string, filePath: string): void { - const relative = path.relative(rootDir, filePath) - let currentPath = rootDir - - for (const segment of relative.split(path.sep)) { - currentPath = path.join(currentPath, segment) - try { - if (fs.lstatSync(currentPath).isSymbolicLink()) { - throw new Error(`Symbolic links are not allowed in generated paths: ${currentPath}`) - } - } catch (error) { - if (error instanceof Error && 'code' in error && error.code === 'ENOENT') continue - throw error - } - } -} - -function readManifest(rootDir: string, manifestPath: string): Manifest | null { - assertNoSymlinks(rootDir, manifestPath) - if (!fs.existsSync(manifestPath)) return null - - let manifest: unknown - try { - manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) - } catch { - throw new Error(`Cannot parse generated files manifest: ${manifestPath}`) - } - - if ( - !manifest - || typeof manifest !== 'object' - || !('version' in manifest) - || manifest.version !== 1 - || !('generator' in manifest) - || manifest.generator !== GENERATOR - || !('files' in manifest) - || !Array.isArray(manifest.files) - || !manifest.files.every((file) => typeof file === 'string') - ) { - throw new Error(`Invalid generated files manifest: ${manifestPath}`) - } - - return { - ...(manifest as Manifest), - files: (manifest as Manifest).files.map(normalizeManagedPath), - } -} - -function hasGeneratedMarker(filePath: string): boolean { - if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return false - const content = fs.readFileSync(filePath, 'utf-8') - return content.includes(GENERATED_MARKER) || content.includes(GENERATED_NOTICE_MARKER) -} - -function writeFileAtomic(rootDir: string, filePath: string, content: string | Uint8Array): void { - assertNoSymlinks(rootDir, filePath) - fs.mkdirSync(path.dirname(filePath), { recursive: true }) - assertNoSymlinks(rootDir, filePath) - - const temporaryPath = path.join( - path.dirname(filePath), - `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`, - ) - - try { - fs.writeFileSync(temporaryPath, content, { flag: 'wx' }) - fs.renameSync(temporaryPath, filePath) - } finally { - if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath) - } -} - -/** Обновляет только файлы, которыми владеет React-генератор. */ -export function writeReactFiles( - rootDir: string, - files: GeneratedFile[], - generatedNotice: boolean, -): void { - const generatedDir = path.join(rootDir, 'generated') - const manifestPath = path.join(generatedDir, MANIFEST_FILE) - const previousManifest = readManifest(rootDir, manifestPath) - const previousFiles = new Set(previousManifest?.files ?? []) - const nextFiles = files.map((file) => normalizeManagedPath(file.path)) - const obsoleteFiles: string[] = [] - - for (const file of files) { - const filePath = resolveManagedPath(rootDir, file.path) - assertNoSymlinks(rootDir, filePath) - if (fs.existsSync(filePath) && !hasGeneratedMarker(filePath)) { - throw new Error( - `Refusing to overwrite a user file: ${filePath}\n` + - 'Move the file or choose another sprite directory.', - ) - } - } - - for (const relativePath of previousFiles) { - if (nextFiles.includes(relativePath)) continue - const filePath = resolveManagedPath(rootDir, relativePath) - assertNoSymlinks(rootDir, filePath) - - if (fs.existsSync(filePath)) { - if (!hasGeneratedMarker(filePath)) { - throw new Error(`Refusing to delete a user file: ${filePath}`) - } - obsoleteFiles.push(filePath) - } - } - - for (const filePath of obsoleteFiles) fs.unlinkSync(filePath) - - for (const file of files) { - writeFileAtomic(rootDir, resolveManagedPath(rootDir, file.path), file.content) - } - - const manifest: Manifest = { - version: 1, - generator: GENERATOR, - files: nextFiles, - ...(generatedNotice && { - warning: 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ. Не редактируйте вручную: изменения будут перезаписаны.', - }), - } - writeFileAtomic(rootDir, manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) -} diff --git a/src/preview.ts b/src/preview.ts deleted file mode 100644 index 1eeeaa8..0000000 --- a/src/preview.ts +++ /dev/null @@ -1,185 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import type { SpriteResult } from './types.js' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -/** Извлекает id иконок из SVG-спрайта. */ -function extractIconIds(spritePath: string): string[] { - const content = fs.readFileSync(spritePath, 'utf-8') - const ids: string[] = [] - const regex = /<(?:svg|symbol)\b[^>]*\bid="([^"]+)"/g - let match: RegExpExecArray | null - while ((match = regex.exec(content)) !== null) { - ids.push(match[1]) - } - return ids.sort() -} - -/** Извлекает CSS-переменные var(--icon-color-N, fallback) из SVG-фрагмента иконки. */ -function extractIconVars(svgFragment: string): { varName: string; fallback: string }[] { - const vars = new Map() - const regex = /var\((--icon-color-\d+),\s*([^)]+)\)/g - let match: RegExpExecArray | null - while ((match = regex.exec(svgFragment)) !== null) { - if (!vars.has(match[1])) { - vars.set(match[1], match[2].trim()) - } - } - return [...vars.entries()].map(([varName, fallback]) => ({ varName, fallback })) -} - -/** Парсит SVG-спрайт и возвращает маппинг id → SVG-фрагмент. */ -function extractIconFragments(spritePath: string): Map { - const content = fs.readFileSync(spritePath, 'utf-8') - const fragments = new Map() - const regex = /<(?:svg|symbol)\b[^>]*\bid="([^"]+)"[^>]*>[\s\S]*?<\/(?:svg|symbol)>/g - let match: RegExpExecArray | null - while ((match = regex.exec(content)) !== null) { - fragments.set(match[1], match[0]) - } - return fragments -} - -/** Извлекает viewBox из SVG-фрагмента иконки. */ -function extractViewBox(svgFragment: string): { x: number; y: number; width: number; height: number } | null { - const match = svgFragment.match(/viewBox="([^"]+)"/) - if (!match) return null - const parts = match[1].split(/\s+/).map(Number) - if (parts.length !== 4) return null - return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] } -} - -/** Конвертирует CSS-цвет в hex. */ -function colorToHex(color: string): string { - const named: Record = { - red: '#ff0000', blue: '#0000ff', green: '#008000', white: '#ffffff', - black: '#000000', yellow: '#ffff00', cyan: '#00ffff', magenta: '#ff00ff', - orange: '#ffa500', purple: '#800080', pink: '#ffc0cb', gray: '#808080', - grey: '#808080', currentcolor: '#000000', - } - const lower = color.toLowerCase().trim() - if (lower.startsWith('#')) { - if (lower.length === 4) return `#${lower[1]}${lower[1]}${lower[2]}${lower[2]}${lower[3]}${lower[3]}` - return lower - } - return named[lower] || '#000000' -} - -/** Подготавливает SVG-спрайт для инлайна — конвертирует вложенные в . */ -function prepareInlineSprite(spritePath: string): string { - let content = fs.readFileSync(spritePath, 'utf-8') - content = content.replace(/<\?xml[^?]*\?>\s*/g, '') - content = content.replace(/' : '' - } - depth++ - if (depth > 1) { - const cleanAttrs = attrs.replace(/\s*xmlns="[^"]*"/g, '') - return `` - } - return `` - }) - return content -} - -interface IconData { - id: string - group: string - mode: string - spriteFile: string - viewBox: { x: number; y: number; width: number; height: number } | null - vars: { varName: string; fallback: string; hex: string; isCurrentColor: boolean }[] -} - -interface SpriteGroup { - name: string - mode: string - spriteFile: string - icons: IconData[] -} - -/** - * Генерирует HTML-файл превью для всех спрайтов. - * - * Использует pre-built React-приложение из dist/preview-template.html, - * инжектирует данные спрайтов и inline SVG. - */ -export function generatePreview( - results: SpriteResult[], - outputDir: string, -): string { - // Собираем данные - const groups: SpriteGroup[] = results.map((r) => { - const fragments = extractIconFragments(r.spritePath) - const ids = extractIconIds(r.spritePath) - const spriteFile = `${r.name}.sprite.svg` - - const icons: IconData[] = ids.map((id) => { - const fragment = fragments.get(id) || '' - return { - id, - group: r.name, - mode: r.format, - spriteFile, - viewBox: extractViewBox(fragment), - vars: extractIconVars(fragment).map((v) => ({ - varName: v.varName, - fallback: v.fallback, - hex: colorToHex(v.fallback), - isCurrentColor: v.fallback.toLowerCase() === 'currentcolor', - })), - } - }) - - return { name: r.name, mode: r.format, spriteFile, icons } - }) - - // Inline SVG спрайтов - const inlineSprites = results - .map((r) => prepareInlineSprite(r.spritePath)) - .join('\n') - - // Скрипт с данными + DOM injection - const svgEscaped = inlineSprites.replace(/`/g, '\\`').replace(/\$/g, '\\$') - const dataScript = [ - '', - ].join('\n') - - // Читаем шаблон - const templatePath = path.join(__dirname, 'preview-template.html') - - if (!fs.existsSync(templatePath)) { - throw new Error( - `Preview template not found: ${templatePath}\n` + - 'Run "npm run build" in the preview/ directory first.', - ) - } - - let html = fs.readFileSync(templatePath, 'utf-8') - html = html.replace('', dataScript) - - // Записываем результат - const outputPath = path.join(outputDir, 'preview.html') - fs.mkdirSync(outputDir, { recursive: true }) - fs.writeFileSync(outputPath, html) - - return outputPath -} diff --git a/src/react/types.ts b/src/react/types.ts index 23afcdb..cea60f1 100644 --- a/src/react/types.ts +++ b/src/react/types.ts @@ -1,4 +1,4 @@ -import type { SpriteAssetTarget } from '../targets/types.js' +import type { SpriteAssetTarget, SpriteMode } from '../targets/types.js' export type SpriteManifestColor = { variable: `--icon-color-${number}` @@ -19,6 +19,7 @@ export type SpriteManifest = { name: string description?: string componentName: string + mode?: SpriteMode target: SpriteAssetTarget format: 'stack' | 'symbol' iconCount: number diff --git a/src/scanner.ts b/src/scanner.ts index bd94908..055cdcf 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -1,6 +1,6 @@ import fs from 'node:fs' import path from 'node:path' -import type { SpriteEntry, SpriteFolder } from './types.js' +import type { SpriteFolder } from './types.js' /** * Сканирует папку и возвращает отсортированные абсолютные пути к SVG-файлам. @@ -61,46 +61,3 @@ export function resolveSpriteSources(options: ResolveSpriteSourcesOptions): Spri files, } } - -/** - * Преобразует SpriteEntry из конфига в SpriteFolder для компиляции. - */ -export function resolveSpriteEntry(entry: SpriteEntry): SpriteFolder { - const format = entry.format ?? 'stack' - - if (Array.isArray(entry.input)) { - const files = resolveFiles(entry.input) - - if (files.length === 0) { - throw new Error(`Sprite "${entry.name}" has empty input array.`) - } - - return { - name: entry.name, - format, - path: null, - files, - } - } - - const dirPath = path.resolve(entry.input) - const files = scanDirectory(dirPath) - - if (files.length === 0) { - throw new Error(`Sprite "${entry.name}" has no SVG files in "${dirPath}".`) - } - - return { - name: entry.name, - format, - path: dirPath, - files, - } -} - -/** - * Преобразует массив SpriteEntry из конфига в массив SpriteFolder. - */ -export function resolveSprites(entries: SpriteEntry[]): SpriteFolder[] { - return entries.map(resolveSpriteEntry) -} diff --git a/src/targets/index.ts b/src/targets/index.ts deleted file mode 100644 index bafff21..0000000 --- a/src/targets/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { generateViteAssetUrlCode } from './vite.js' -import { generateWebpackAssetUrlCode } from './webpack.js' -import type { SpriteAssetTarget, SpriteAssetUrlCode } from './types.js' - -/** Возвращает codegen-фрагменты для выбранной среды сборки React-модуля. */ -export function generateReactAssetUrlCode( - target: SpriteAssetTarget, - spriteFileName: string, -): SpriteAssetUrlCode { - switch (target) { - case 'vite': - return generateViteAssetUrlCode(spriteFileName) - case 'webpack': - case 'next@app/turbopack': - case 'next@app/webpack': - case 'next@pages/turbopack': - case 'next@pages/webpack': - return generateWebpackAssetUrlCode(spriteFileName) - default: - throw new Error(`Unsupported sprite asset target: ${String(target)}`) - } -} - -export type { - NextAssetTarget, - NextBundler, - NextRouter, - ReactAssetTarget, - SpriteAssetTarget, - SpriteAssetUrlCode, - ViteAssetTarget, - WebpackAssetTarget, -} from './types.js' diff --git a/src/targets/types.ts b/src/targets/types.ts index 77492a4..b974536 100644 --- a/src/targets/types.ts +++ b/src/targets/types.ts @@ -31,9 +31,15 @@ export type NextAssetTarget = `next@${NextRouter}/${NextBundler}` */ export type ReactAssetTarget = ViteAssetTarget | WebpackAssetTarget +/** Полный ключ React mode, используемый конфигом, CLI и manifest. */ +export type ReactSpriteMode = `react@${ReactAssetTarget}` + /** Любая среда, для которой может быть сгенерирован React sprite-модуль. */ export type SpriteAssetTarget = ReactAssetTarget | NextAssetTarget +/** Режим генерации sprite-модуля. В будущем расширяется standalone mode. */ +export type SpriteMode = ReactSpriteMode | NextAssetTarget + /** Фрагменты кода, необходимые компоненту для получения URL SVG asset. */ export type SpriteAssetUrlCode = { /** Импорты, которые добавляются в начало generated-компонента. */ diff --git a/src/targets/vite.ts b/src/targets/vite.ts deleted file mode 100644 index 1d8da1f..0000000 --- a/src/targets/vite.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { SpriteAssetUrlCode } from './types.js' - -/** Генерирует Vite-импорт отдельного, запрещённого к inline SVG asset. */ -export function generateViteAssetUrlCode(spriteFileName: string): SpriteAssetUrlCode { - return { - imports: [`import spriteUrl from './${spriteFileName}?no-inline'`], - declarations: [], - variableName: 'spriteUrl', - } -} diff --git a/src/targets/webpack.ts b/src/targets/webpack.ts deleted file mode 100644 index c06a502..0000000 --- a/src/targets/webpack.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { SpriteAssetUrlCode } from './types.js' - -/** Генерирует Webpack 5 Asset Module через статический import.meta.url. */ -export function generateWebpackAssetUrlCode(spriteFileName: string): SpriteAssetUrlCode { - return { - imports: [], - declarations: [ - `const spriteUrl = new URL('./${spriteFileName}', import.meta.url).href`, - ], - variableName: 'spriteUrl', - } -} diff --git a/src/types.ts b/src/types.ts index 335e7db..f98ab9f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,23 +1,8 @@ +import type { SpriteMode } from './targets/types.js' + /** Формат спрайта: stack или symbol. */ export type SpriteFormat = 'stack' | 'symbol' -/** Описание одного спрайта в конфиге. */ -export type SpriteEntry = { - /** Уникальное имя спрайта (используется как имя файла и в типах). */ - name: string - /** - * Источник SVG-файлов. - * Строка — путь к папке с SVG-файлами. - * Массив — пути к конкретным SVG-файлам. - */ - input: string | string[] - /** - * Формат спрайта. - * По умолчанию: 'stack'. - */ - format?: SpriteFormat -} - /** Параметры трансформации SVG. Все включены по умолчанию. */ export type TransformOptions = { /** @@ -39,22 +24,33 @@ export type TransformOptions = { addTransition?: boolean } -/** Конфигурация генерации SVG-спрайтов. */ -export type SvgSpritesConfig = { - /** Путь к папке для сгенерированных SVG-спрайтов. */ - output: string - /** - * Генерировать HTML-превью со всеми иконками. - * По умолчанию: true. - */ - preview?: boolean - /** - * Настройки трансформации SVG. - * По умолчанию: все трансформации включены. - */ +/** Единая конфигурация локального sprite-модуля. */ +export type SpriteConfig = { + /** Режим можно определить в конфиге либо передать через CLI/API. */ + mode?: SpriteMode + /** Логическое имя спрайта. По умолчанию выводится из каталога модуля. */ + name?: string + /** Описание спрайта для generated types и manifest. */ + description?: string + /** Папка с исходными SVG относительно корня модуля. По умолчанию: ./icons. */ + inputFolder?: string + /** Дополнительные SVG-файлы относительно корня модуля. По умолчанию: []. */ + inputFiles?: string[] + /** Настройки трансформации SVG. По умолчанию все включены. */ transform?: TransformOptions - /** Список спрайтов для генерации. */ - sprites: SpriteEntry[] + /** Добавлять развёрнутое предупреждение в generated-файлы. По умолчанию: true. */ + generatedNotice?: boolean +} + +/** Полностью разрешённая конфигурация, готовая к генерации. */ +export type ResolvedSpriteConfig = { + mode: SpriteMode + name: string + description?: string + inputFolder: string | null + inputFiles: string[] + transform: Required + generatedNotice: boolean } /** Результат парсинга спрайта — данные для компиляции. */ @@ -68,15 +64,3 @@ export type SpriteFolder = { /** Абсолютные пути к SVG-файлам. */ files: string[] } - -/** Результат компиляции одного спрайта. */ -export type SpriteResult = { - /** Имя спрайта. */ - name: string - /** Формат спрайта. */ - format: SpriteFormat - /** Путь к сгенерированному SVG-спрайту. */ - spritePath: string - /** Количество иконок в спрайте. */ - iconCount: number -} diff --git a/svg-sprites.config.ts b/svg-sprites.config.ts deleted file mode 100644 index a21098c..0000000 --- a/svg-sprites.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineLegacyConfig } from './src/index.js' - -export default defineLegacyConfig({ - output: 'preview/public', - preview: true, - - sprites: [ - { - name: 'icons', - input: 'test/assets/icons', - format: 'stack', - }, - { - name: 'logos', - input: 'test/assets/logos', - format: 'stack', - }, - ], -})