diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e266f17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 S. Gromov + +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/README.md b/README.md index d925018..8242cc9 100644 --- a/README.md +++ b/README.md @@ -2,213 +2,394 @@ ![npm](https://img.shields.io/npm/v/@gromlab/svg-sprites) ![license](https://img.shields.io/npm/l/@gromlab/svg-sprites) -Генерация SVG-спрайтов из папок с иконками. TypeScript-типизация, SVG-трансформации, React-компонент и HTML-превью из коробки. +CLI для генерации SVG-спрайтов и типизированных компонентов иконок для React и Next.js. ![Preview](https://gromlab.ru/gromov/svg-sprites/media/branch/master/preview-image.png) -**Чем отличается от аналогов:** +## Навигация -- Генерирует готовый типизированный React-компонент — не нужно писать обёртку -- Автоматически заменяет цвета на CSS-переменные — для тем и hover-состояний -- Выдаёт HTML-превью всех иконок — удобно пересылать дизайнеру +- [Возможности](#возможности) +- [Таблица поддержки](#таблица-поддержки) +- [Требования](#требования) +- [Быстрый старт](#быстрый-старт) + - [React + Vite](docs/ru/react-vite.md) + - [React + Webpack 5](docs/ru/react-webpack.md) + - [Next.js App Router](docs/ru/next-app.md) + - [Next.js Pages Router](docs/ru/next-pages.md) +- [Конфигурация](#конфигурация) + - [React](#react) + - [Next.js](#nextjs) +- [Множественные спрайты](#множественные-спрайты) +- [TypeScript](#typescript) +- [Форматы спрайтов](#форматы-спрайтов) +- [Способы отображения](#способы-отображения) +- [Трансформации](#трансформации) +- [Управление цветом иконок](#управление-цветом-иконок) +- [Кеширование](#кеширование) +- [SpriteViewer](#spriteviewer) +- [Миграция с 0.1.x](docs/ru/migration-1.md) +- [Документация](#документация) + +## Возможности + +- **TypeScript-friendly** — типизированные React-компоненты, union-типы и runtime-списки доступных иконок. +- **Чистая генерация** — generated-файлы автоматически исключаются из Git, спрайт не нужно вручную размещать в `public`, а генератор обновляет только принадлежащие ему файлы. +- **Общие иконки без копирования** — SVG из локальной папки и `inputFiles` объединяются в один спрайт; один файл можно использовать в нескольких спрайтах. +- **Встроенное интерактивное превью** — `` подключается как страница приложения и показывает переданные React- и Next.js-спрайты с поиском, настройкой цветов и примерами использования. +- **Настраиваемые трансформации SVG** — удаление `width` и `height` с сохранением `viewBox`, замена исходных цветов на CSS-переменные и transitions для `fill` и `stroke`. +- **Отдельный кешируемый SVG asset** — SVG path-данные не попадают в JavaScript chunks, а сборщик выпускает файл с content hash. +- **Множественные спрайты** — независимые React- и Next.js-модули со своими компонентами, типами и SVG assets. +- **Server-first Next.js** — generated-компоненты работают в Server Components, SSR и SSG без директивы `'use client'`. +- **Форматы под разные сценарии** — React и Next.js используют `stack`, legacy-режим также поддерживает `symbol` для существующих интеграций. + +## Таблица поддержки + +| Среда | Ключ мода API | Статус | +|---|---|---| +| React + Vite | `react@vite` | Готово | +| React + Webpack 5 | `react@webpack` | Готово | +| Next.js 16.2+ App Router + Turbopack | `next@app/turbopack` | Готово | +| Next.js 13.4+ App Router + Webpack 5 | `next@app/webpack` | Готово | +| Next.js 16.2+ Pages Router + Turbopack | `next@pages/turbopack` | Готово | +| Next.js 12.2+ Pages Router + Webpack 5 | `next@pages/webpack` | Готово | +| Vue | — | Скоро | +| Standalone | — | Скоро | ## Требования -- Node.js 18+ -- React 18+ (опционально, только если нужен генерируемый компонент) - -## Установка - -```bash -npm install @gromlab/svg-sprites -``` +- Node.js 18 или новее; +- пакет распространяется только как ESM и подключается через `import`; +- React 18 или 19 требуется только для generated-компонентов и точки входа `@gromlab/svg-sprites/react`; +- для типизации subpath exports используйте TypeScript 5+ с `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`. ## Быстрый старт -Создайте файл `svg-sprites.config.ts` в корне проекта: +Для быстрого старта воспользуйтесь инструкцией для вашего стека: -```ts -import { defineConfig } from '@gromlab/svg-sprites' - -export default defineConfig({ - // Папка для сгенерированных SVG-спрайтов - output: 'public/sprites', - - // URL-путь к спрайтам (для href в React-компоненте) - publicPath: '/sprites', - - // Папка для React-компонента и типов - react: 'src/shared/ui/svg-sprite', - - sprites: [ - { name: 'icons', input: 'src/assets/icons' }, - { name: 'logos', input: 'src/assets/logos' }, - ], -}) -``` - -Запустите генерацию: - -```bash -# или добавьте "sprite": "svg-sprites" в scripts вашего package.json -npx svg-sprites -``` - -В результате будут сгенерированы SVG-спрайты, типизированный React-компонент и HTML-превью. - -Цвета иконок автоматически заменяются на CSS-переменные — см. раздел [Управление цветом](#управление-цветом). - -## Использование компонента - -```tsx -import { SvgSprite } from './shared/ui/svg-sprite' - -// Иконка из первого спрайта (по умолчанию) - - -// Иконка из другого спрайта - - -// Обёртка в (удобно для inline-элементов) - -``` - -Компонент полностью типизирован — автодополнение работает для имён иконок и спрайтов. Типы экспортируются из того же модуля: - -```ts -import type { - IconsIconName, // 'check' | 'arrow-left' | ... - LogosIconName, // 'github' | 'twitter' | ... - SpriteName, // 'icons' | 'logos' - SpriteMap, // { icons: IconsIconName, logos: LogosIconName } -} from './shared/ui/svg-sprite' -``` - -## Управление цветом - -При сборке цвета иконок заменяются на CSS-переменные. Это ключевая фича — иконки адаптируются к теме без дублирования SVG. - -**Моно-иконка** (один цвет) — наследует `color` текста: - -```css -.button { color: red; } -``` - -Или точечно через CSS-переменную: - -```css -.button { --icon-color-1: #ff0000; } -``` - -**Мульти-иконка** (несколько цветов) — каждый цвет задаётся отдельной переменной: - -```css -.card { - --icon-color-1: #ff0000; - --icon-color-2: #00ff00; -} -``` - -## Способы рендера - -| Способ | Управление цветом | Пример | -|--------|-------------------|--------| -| React / SVG `` | CSS-переменные, `color` | `` | -| CSS `mask-image` | `background-color` (монохром) | `.icon { mask: url(...); background-color: red; }` | -| `` | нет | `` | +- [React + Vite](docs/ru/react-vite.md) +- [React + Webpack 5](docs/ru/react-webpack.md) +- [Next.js App Router](docs/ru/next-app.md) +- [Next.js Pages Router](docs/ru/next-pages.md) ## Конфигурация -### Основные опции - -| Опция | Обязательная | Описание | -|-------|-------------|----------| -| `output` | да | Папка для сгенерированных SVG-спрайтов | -| `sprites` | да | Массив спрайтов для генерации | -| `publicPath` | нет | URL-путь к спрайтам (для `href` в React-компоненте) | -| `react` | нет | Путь для генерации React-компонента и типов | -| `preview` | нет | Генерация HTML-превью (по умолчанию: `true`) | - -### `sprites` +### React ```ts -sprites: [ - { - name: 'icons', // имя спрайта → icons.sprite.svg - input: 'src/assets/icons', // папка с SVG-файлами - mode: 'stack', // 'stack' (по умолчанию) или 'symbol' +import { defineReactSpriteConfig } from '@gromlab/svg-sprites' + +export default defineReactSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', + inputFolder: './icons', + inputFiles: [ + '../../shared/icons/check.svg', + ], + transform: { + removeSize: true, + replaceColors: true, + addTransition: true, }, - { - name: 'flags', - input: [ // или массив конкретных файлов - 'src/components/button/arrow.svg', - 'src/components/modal/close.svg', - ], - }, -] -``` - -### `publicPath` - -Публичный URL-путь к спрайтам. Зашивается в React-компонент для формирования `href`. - -```ts -publicPath: '/sprites' -// → -``` - -> **Примечание:** путь не должен включать имя папки `public` — она не входит в URL в Vite/Next. - -### `react` - -Имена файлов берутся из названия папки: - -```ts -react: 'src/shared/ui/svg-sprite' -// → index.ts + svg-sprite.tsx + svg-sprite.module.css -``` - -Если не задан — компонент и типы не генерируются. - -### `transform` - -Настройки трансформации SVG. Все опции включены по умолчанию. - -```ts -transform: { - removeSize: true, // удаляет width/height с - replaceColors: true, // заменяет цвета на CSS-переменные - addTransition: true, // добавляет transition к элементам с цветом -} -``` - -Например, для спрайта с фиксированными цветами: - -```ts -transform: { - replaceColors: false, -} -``` - -## Ограничения - -- `mode: 'symbol'` — поддерживается, но превью и примеры кода оптимизированы под `stack` -- `replaceColors` может некорректно обработать иконки со сложными градиентами — используйте `transform: { replaceColors: false }` для таких случаев -- Генерируемый React-компонент предназначен для React 18+. Для Vue/Svelte используйте спрайты напрямую через SVG `` - -## Программный API - -```ts -import { generate, defineConfig } from '@gromlab/svg-sprites' - -const config = defineConfig({ - output: 'public', - sprites: [{ name: 'icons', input: 'assets/icons' }], + generatedNotice: true, }) - -const results = await generate(config) ``` +| Опция | Тип | По умолчанию | Назначение | +|---|---|---|---| +| `name` | `string` | Имя папки | Имя спрайта, компонента и публичных типов | +| `description` | `string` | Нет | Описание для типов и debug-манифеста | +| `inputFolder` | `string` | `./icons` | Папка с исходными SVG относительно конфига | +| `inputFiles` | `string[]` | `[]` | Дополнительные SVG-файлы относительно конфига | +| `transform` | `TransformOptions` | Все включены | [Настройки трансформации](#трансформации) исходных SVG | +| `generatedNotice` | `boolean` | `true` | Полное либо короткое предупреждение в generated-файлах | + +`inputFolder` и `inputFiles` объединяются в один спрайт, поэтому один SVG-файл можно использовать в нескольких спрайтах без копирования. Если неявной папки `./icons` нет, но `inputFiles` заполнен, генерация продолжается только по списку. Явно указанная отсутствующая папка считается ошибкой. Одинаковые пути дедуплицируются, а разные файлы с одинаковым именем иконки считаются ошибкой. + +`name` записывается в kebab-case и должно начинаться с латинской буквы. React и Next.js presets создают формат `stack`. + +### Next.js + +Next.js использует тот же `svg-sprite.config.ts` и набор опций. Для типизации можно использовать отдельный хелпер: + +```ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', + inputFolder: './icons', +}) +``` + +Роутер и сборщик выбираются через mode key, поэтому переключение между Turbopack и Webpack всегда явно отражено в команде генерации. + +## Множественные спрайты + +Приложение может содержать несколько независимых спрайтов с разной областью использования: + +**Проблема:** один глобальный спрайт загружает иконки, которые текущему экрану не нужны. + +**Решение:** общие иконки хранить глобально, а наборы страниц и крупных компонентов — в отдельных спрайтах, загружаемых вместе с ними. + +```text +global → GlobalIcon → общие иконки приложения +analytics-page → AnalyticsPageIcon → иконки отдельной страницы +file-manager → FileManagerIcon → иконки крупного компонента +``` + +- **Глобальный спрайт** содержит небольшие общие иконки, используемые в разных частях приложения: навигацию, состояния и базовые действия. +- **Спрайт страницы** загружается вместе с конкретным разделом и не увеличивает общий спрайт иконками, которые больше нигде не нужны. +- **Спрайт крупного компонента** инкапсулирует собственный набор иконок сложного UI-модуля, например файлового менеджера или редактора. + +Каждая группа получает: + +- собственный SVG asset; +- собственный типизированный компонент; +- отдельный список имён иконок; +- отдельный debug-манифест; +- независимый cache lifecycle. + + +## TypeScript + +Главная возможность TypeScript API — автодополнение имён иконок непосредственно в prop `icon`: + +```tsx + +// ↑ редактор предлагает все иконки спрайта +``` + +Имена SVG-файлов становятся допустимыми значениями `icon`. Опечатка или неизвестное имя сразу становятся ошибкой TypeScript: + +```tsx + // ошибка TypeScript +``` + +Для программного доступа generated-модуль экспортирует readonly-массив всех доступных иконок конкретного спрайта: + +```ts +import { fileManagerIconNames } from './svg-sprite' + +// readonly ['check', 'folder', ...] +``` + +Этот список можно использовать в собственных каталогах, select-компонентах, тестах и других runtime-сценариях. Из него также выводится union-тип `FileManagerIconName`. + +Имена файлов с пробелами и другими небезопасными для SVG ID символами остаются частью публичного TypeScript API. Для внутреннего `` генератор создаёт стабильный hash ID. + +```text +folder open.svg → icon="folder open" → id="icon-" +``` + +Для таких имён используйте generated-компонент или `id` из debug-манифеста. Ручные примеры ниже с `#<имя>` подходят только для имён, которые уже являются безопасными SVG ID. + +## Форматы спрайтов + +`stack` — более современный формат, поэтому он используется по умолчанию. Иконки можно отображать через ``, `` и CSS `background-image`. + +`symbol` сохраняется для совместимости с существующими интеграциями и поддерживает отображение только через ``. + +## Способы отображения + +### React-компонент — рекомендуется + +Generated-компонент предоставляет типизацию, автодополнение имён иконок и сам формирует URL SVG asset. + +```tsx + +``` + +Через `color` и `--icon-color-N` доступны одноцветные и многоцветные иконки. + +### Самостоятельно через `` + +Хороший низкоуровневый способ с полным управлением размерами и цветами. React-компонент под капотом использует именно его. + +Способ получения `spriteUrl` зависит от сборщика. + +**Vite:** + +```tsx +import spriteUrl from './svg-sprite/generated/sprite.svg?no-inline' +``` + +**Webpack 5:** + +```tsx +const spriteUrl = new URL( + './svg-sprite/generated/sprite.svg', + import.meta.url, +).href +``` + +**Next.js с Webpack 5 или Turbopack:** + +```tsx +const spriteUrl = new URL( + './svg-sprite/generated/sprite.svg', + import.meta.url, +).href +``` + +После получения URL иконка отображается одинаково: + +```tsx + + + +``` + +Vite, Webpack 5 и Next.js сами заменяют исходный путь на итоговый URL asset с hash. + +### Через `` — менее эффективно + +```tsx +Готово +``` + +SVG загружается как изолированное изображение: изменить его цвета через `color` или `--icon-color-N` нельзя. + +### Через CSS `background-image` — менее эффективно + +```css +.icon { + background: url('./svg-sprite/generated/sprite.svg#check') center / contain no-repeat; +} +``` + +Как и ``, этот способ не позволяет управлять внутренними цветами SVG. Путь указывается относительно CSS-файла, а Vite/Webpack заменяет его на итоговый URL с hash при сборке. + +### Через CSS mask — менее эффективно + +```css +.icon { + background-color: currentColor; + mask: url('./svg-sprite/generated/sprite.svg#check') center / contain no-repeat; +} +``` + +Mask оставляет только силуэт и окрашивает его одним цветом. Исходные цвета, gradients и различия между `fill` и `stroke` теряются. + +## Трансформации + +Все трансформации включены по умолчанию и настраиваются независимо через `transform`. + +| Опция | По умолчанию | Что делает | +|---|---|---| +| `removeSize` | `true` | Удаляет `width` и `height` с корневого ``, сохраняя существующий `viewBox`. Размер иконки после этого задаётся снаружи. | +| `replaceColors` | `true` | Заменяет цвета `fill` и `stroke` на `--icon-color-N`. Для одноцветной иконки fallback становится `currentColor`, для многоцветной сохраняются исходные цвета. | +| `addTransition` | `true` | Добавляет `style="transition:fill 0.3s,stroke 0.3s;"` непосредственно цветным элементам SVG. Существующий `transition` не перезаписывается. | + +Чтобы отключить преобразование, передайте для соответствующей опции `false`. Подробнее о результате `replaceColors` — в разделе [«Управление цветом иконок»](#управление-цветом-иконок). + +## Управление цветом иконок + +При включённой замене цветов генератор анализирует `fill` и `stroke` и преобразует их в CSS custom properties. + +### Монохромные иконки + +Если найден один цвет, fallback заменяется на `currentColor`: + +```svg +stroke="var(--icon-color-1, currentColor)" +``` + +Цветом управляет CSS-свойство `color` внешнего `` или его родителя. + +### Многоцветные иконки + +Каждый уникальный цвет получает отдельную переменную с исходным fallback: + +```svg +fill="var(--icon-color-1, #798198)" +fill="var(--icon-color-2, #ffffff)" +fill="var(--icon-color-3, #129d9d)" +``` + +Страница может заменить только необходимые цвета: + +```css +.icon { + --icon-color-1: #4b5563; + --icon-color-3: #14b8a6; +} +``` + +### Ограничения цветов + +- `none`, `transparent`, `inherit`, `unset` и `initial` не заменяются; +- цвета в атрибутах `fill`, `stroke` и inline `style` обрабатываются надёжнее всего; +- CSS-классы и внешние stylesheets внутри исходного SVG не являются основным сценарием трансформации; +- gradients, patterns, filters и значения `url(#...)` требуют отдельной проверки и могут быть несовместимы с автоматической заменой цветов; +- CSS-переменные страницы доступны при ``, но недоступны внутри `` и `background-image`. + +## Кеширование + +Vite, Webpack и Next.js target выпускают спрайт отдельным asset с content hash: + +```text +/assets/sprite-.svg +``` + +Это даёт следующие свойства: + +- SVG кешируется независимо от JavaScript; +- изменение React-кода не меняет содержимое спрайта; +- изменение иконок создаёт новый hash asset; +- один файл используется всеми экземплярами generated-компонента; +- SVG path-данные отсутствуют в JavaScript chunks. + +Vite target запрещает inline через `?no-inline`. Webpack 5 target использует Asset Modules через `new URL(..., import.meta.url)`. + +## SpriteViewer + +`SpriteViewer` — React-компонент для просмотра generated-спрайтов внутри debug-маршрута приложения. + +Он использует отдельные манифесты и показывает: + +- группы спрайтов; +- список и количество иконок; +- поиск и системную светлую/тёмную тему; +- модальное превью с `viewBox` и настройкой цветовых переменных; +- примеры React, SVG, IMG и CSS с копированием кода. + +Production-компоненты не импортируют debug-манифесты. Способ подключения Viewer зависит от сборщика: + +- [React + Vite: автоматический `import.meta.glob`](docs/ru/react-vite.md#6-добавьте-debug-страницу); +- [React + Webpack 5: статические `import()`](docs/ru/react-webpack.md#6-добавьте-debug-страницу); +- [Next.js App Router](docs/ru/next-app.md#5-добавьте-spriteviewer); +- [Next.js Pages Router](docs/ru/next-pages.md#5-добавьте-spriteviewer). + +Viewer подключается из отдельной клиентской точки входа `@gromlab/svg-sprites/react` и не попадает в production-компоненты иконок. + +### Тема Viewer + +По умолчанию `colorTheme="auto"`: Viewer следует `prefers-color-scheme` и реагирует на смену системной темы. Тему приложения можно передать явно: + +```tsx + +``` + +Допустимые значения `colorTheme`: `auto`, `light`, `dark`. При управлении темой извне встроенный переключатель скрывается. Чтобы оставить его и обновлять тему приложения через Viewer, передайте callback: + +```tsx + +``` + +## Документация + +- [React + Vite](docs/ru/react-vite.md) +- [React + Webpack 5](docs/ru/react-webpack.md) +- [Next.js App Router](docs/ru/next-app.md) +- [Next.js Pages Router](docs/ru/next-pages.md) +- [Legacy mode](docs/ru/legacy.md) +- [Миграция с 0.1.x](docs/ru/migration-1.md) +- [Программный API](docs/ru/programmatic-api.md) + ## Лицензия MIT diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..b630bee --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,75 @@ +# Third-Party Notices + +The distributed React entry and preview template include code from the projects listed below. + +## React, React DOM and Scheduler + +Copyright (c) Meta Platforms, Inc. and affiliates. + +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. + +## react-colorful + +Copyright (c) 2020 Vlad Shilov + +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. + +## 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/docs/ru/legacy.md b/docs/ru/legacy.md new file mode 100644 index 0000000..5efd0cf --- /dev/null +++ b/docs/ru/legacy.md @@ -0,0 +1,102 @@ +# Legacy mode + +[← Главная](../../README.md) + +Краткая инструкция по генерации централизованных SVG-спрайтов форматов `symbol` и `stack` с optional HTML preview. + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Подготовьте иконки и конфиг + +```text +project/ +├── src/assets/icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprites.config.ts +``` + +```ts +// svg-sprites.config.ts +import { defineLegacyConfig } from '@gromlab/svg-sprites' + +export default defineLegacyConfig({ + output: 'public/sprites', + preview: true, + sprites: [ + { + name: 'icons', + input: 'src/assets/icons', + format: 'symbol', + }, + ], +}) +``` + +## 3. Запустите генерацию + +```bash +npx svg-sprites --mode legacy . +``` + +Результат: + +```text +public/sprites/ +├── icons.sprite.svg +└── preview.html +``` + +При `preview: false` HTML-файл не создаётся. Для формата `stack` укажите `format: 'stack'`. + +## 4. Используйте symbol-спрайт + +```html + + + +``` + +## 5. Добавьте package script + +```json +{ + "scripts": { + "sprites": "svg-sprites --mode legacy .", + "prebuild": "npm run sprites" + } +} +``` + +## Несколько спрайтов + +Добавьте несколько записей в `sprites`: + +```ts +sprites: [ + { + name: 'icons', + input: 'src/assets/icons', + format: 'symbol', + }, + { + name: 'logos', + input: 'src/assets/logos', + format: 'stack', + }, +] +``` + +Все результаты и общий `preview.html` будут записаны в `output`. + +## Если что-то не работает + +- Не найден конфиг: убедитесь, что `svg-sprites.config.ts` находится в переданном корне. +- Нет иконок: проверьте `sprites[].input` и расширение `.svg`. +- Не нужен preview: установите `preview: false`. + +Для программного запуска используйте [`generateLegacy`](programmatic-api.md#generatelegacy). diff --git a/docs/ru/migration-1.md b/docs/ru/migration-1.md new file mode 100644 index 0000000..a41d3d9 --- /dev/null +++ b/docs/ru/migration-1.md @@ -0,0 +1,96 @@ +# Миграция с 0.1.x на 1.0 + +[← Главная](../../README.md) + +Версия 1.0 разделяет локальную генерацию для React и Next.js и централизованный legacy-режим. Старый config нельзя смешивать с новым API в одном вызове CLI. + +## CLI + +CLI теперь всегда требует явный `--mode` и путь к каталогу конфигурации: + +```text +svg-sprites +→ svg-sprites --mode +``` + +Выберите mode по окружению: + +| Окружение | Mode | +|---|---| +| React + Vite | `react@vite` | +| React + Webpack 5 | `react@webpack` | +| Next.js App Router + Turbopack | `next@app/turbopack` | +| Next.js App Router + Webpack 5 | `next@app/webpack` | +| Next.js Pages Router + Turbopack | `next@pages/turbopack` | +| Next.js Pages Router + Webpack 5 | `next@pages/webpack` | +| Централизованная старая схема | `legacy` | + +## React и Next.js + +Вместо корневого `svg-sprites.config.ts` создайте локальный `svg-sprite.config.ts` рядом с набором иконок: + +```ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'global', + inputFolder: './icons', +}) +``` + +Для обычного React используйте `defineReactSpriteConfig`. Папку и явный список общих SVG можно объединить через `inputFolder` и `inputFiles`. + +Старые `publicPath` и `react` больше не нужны. Generated-модуль создаётся рядом с конфигом, сам добавляет `.gitignore`, а Vite, Webpack или Next.js выпускает SVG как отдельный asset с content hash. + +Компонент `` заменяется компонентом, имя которого выводится из `name`: + +```tsx + +``` + +Для просмотра иконок добавьте `` как debug-страницу приложения. Отдельный `preview.html` остаётся только в legacy-режиме. + +## Legacy-режим + +Если централизованную структуру нужно сохранить, переименуйте helper и поля формата: + +```ts +import { defineLegacyConfig } from '@gromlab/svg-sprites' + +export default defineLegacyConfig({ + output: 'public/sprites', + preview: true, + sprites: [ + { + name: 'icons', + input: 'src/assets/icons', + format: 'stack', + }, + ], +}) +``` + +- `defineConfig` заменён на `defineLegacyConfig`; +- `sprites[].mode` переименован в `sprites[].format`; +- `generate` заменён на `generateLegacy`; +- `loadConfig` заменён на `loadLegacyConfig`; +- `publicPath` и генерация старого общего React-компонента удалены. + +Запуск: + +```bash +svg-sprites --mode legacy . +``` + +## Программный API + +Пакет распространяется только как ESM. Замените `require()` на `import`. + +`compileSpriteContent` теперь возвращает `Promise`, чтобы публичные декларации не требовали установки `@types/node`. В Node.js фактический результат совместим с API, принимающими `Uint8Array`. + +## После миграции + +1. Удалите старые generated-файлы и правила, которые игнорировали целиком каталог с исходными иконками. +2. Добавьте явную команду генерации перед `dev`, `build` и `typecheck`. +3. Запустите генерацию и проверку типов. +4. Проверьте все иконки и цветовые переменные через `SpriteViewer` или legacy `preview.html`. diff --git a/docs/ru/next-app.md b/docs/ru/next-app.md new file mode 100644 index 0000000..593d02d --- /dev/null +++ b/docs/ru/next-app.md @@ -0,0 +1,102 @@ +# Next.js App Router + +[← Главная](../../README.md) + +Поддерживаются два явных режима: + +| Сборщик | Mode key | Версия Next.js | +|---|---|---| +| Turbopack | `next@app/turbopack` | 16.2+ | +| Webpack 5 | `next@app/webpack` | 13.4+ | + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Создайте sprite-модуль + +```text +src/ui/file-manager/svg-sprite/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +```ts +// src/ui/file-manager/svg-sprite/svg-sprite.config.ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', +}) +``` + +## 3. Добавьте генерацию + +Для Turbopack: + +```json +{ + "scripts": { + "sprite:file-manager": "svg-sprites --mode next@app/turbopack src/ui/file-manager/svg-sprite", + "predev": "npm run sprite:file-manager", + "prebuild": "npm run sprite:file-manager" + } +} +``` + +Для Webpack замените mode key на `next@app/webpack`. В Next 13–15 Webpack используется обычной командой `next build`, в Next 16 — командой `next build --webpack`. + +## 4. Используйте в Server Component + +Generated-компонент не содержит `'use client'`, поэтому его можно импортировать непосредственно в `page.tsx` или `layout.tsx`: + +```tsx +import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' + +export default function Page() { + return ( +
+ +
+ ) +} +``` + +Next.js выпустит отдельный SVG asset с content hash. Один generated-код используется при SSR и в браузере без расхождения URL. + +## 5. Добавьте SpriteViewer + +Viewer интерактивен, поэтому для него нужна отдельная Client Component граница: + +```tsx +'use client' + +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('@/ui/file-manager/svg-sprite/manifest'), +] + +export default function SpritesPage() { + return +} +``` + +## Проверка сборщика + +```bash +# Turbopack +npx next build --turbopack + +# Webpack 5 +npx next build --webpack +``` + +Для Next 13–15 с Webpack используйте `npx next build` без флага. + +Команда Next.js и mode key генератора должны указывать один и тот же сборщик. diff --git a/docs/ru/next-pages.md b/docs/ru/next-pages.md new file mode 100644 index 0000000..d52750b --- /dev/null +++ b/docs/ru/next-pages.md @@ -0,0 +1,96 @@ +# Next.js Pages Router + +[← Главная](../../README.md) + +Поддерживаются два явных режима: + +| Сборщик | Mode key | Версия Next.js | +|---|---|---| +| Turbopack | `next@pages/turbopack` | 16.2+ | +| Webpack 5 | `next@pages/webpack` | 12.2+ | + +Для Next.js 12.2 требуется React 18. + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Создайте sprite-модуль + +```text +src/ui/file-manager/svg-sprite/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +```ts +// src/ui/file-manager/svg-sprite/svg-sprite.config.ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', +}) +``` + +## 3. Добавьте генерацию + +```json +{ + "scripts": { + "sprite:file-manager": "svg-sprites --mode next@pages/webpack src/ui/file-manager/svg-sprite", + "predev": "npm run sprite:file-manager", + "prebuild": "npm run sprite:file-manager" + } +} +``` + +Для Next.js 16.2 с Turbopack замените mode key на `next@pages/turbopack`. + +## 4. Используйте на странице + +```tsx +import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' + +export default function FilesPage() { + return +} + +export function getServerSideProps() { + return { props: {} } +} +``` + +Компонент одинаково работает при SSR, SSG и клиентских переходах. Next.js выпускает отдельный SVG asset с content hash. + +## 5. Добавьте SpriteViewer + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('@/ui/file-manager/svg-sprite/manifest'), +] + +export default function SpritesPage() { + return +} +``` + +## Проверка сборщика + +```bash +# Turbopack +npx next build --turbopack + +# Webpack 5 +npx next build --webpack +``` + +Для Next 12–15 с Webpack используйте `npx next build` без флага. + +Команда Next.js и mode key генератора должны указывать один и тот же сборщик. diff --git a/docs/ru/programmatic-api.md b/docs/ru/programmatic-api.md new file mode 100644 index 0000000..510b5fc --- /dev/null +++ b/docs/ru/programmatic-api.md @@ -0,0 +1,203 @@ +# Программный API + +[← Главная](../../README.md) + +Пакет предоставляет основную Node.js точку входа и отдельный React runtime entry. Обе точки распространяются только как ESM и подключаются через `import`. + +Для разрешения `@gromlab/svg-sprites/react` в TypeScript используйте `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`. + +## Основной entry + +```ts +import { + defineNextSpriteConfig, + defineReactSpriteConfig, + generateNextSprite, + generateReactSprite, +} from '@gromlab/svg-sprites' +``` + +Основной entry не импортирует React и может использоваться в CLI, build scripts и Node.js инструментах. + +## `generateReactSprite` + +```ts +import { generateReactSprite } from '@gromlab/svg-sprites' + +const result = await generateReactSprite( + 'src/ui/file-manager/svg-sprite', + 'vite', +) +``` + +Второй аргумент обязателен: + +```ts +type ReactAssetTarget = 'vite' | 'webpack' +``` + +Результат: + +```ts +type ReactSpriteGenerationResult = { + name: string + rootDir: string + generatedDir: string + spritePath: string + manifestPath: string + iconCount: number + target: 'vite' | 'webpack' +} +``` + +```ts +console.log(result.name) +console.log(result.iconCount) +console.log(result.spritePath) +console.log(result.manifestPath) +``` + +Функция загружает `svg-sprite.config.ts` из указанного корня, компилирует SVG и безопасно обновляет managed-файлы. + +## `generateNextSprite` + +```ts +import { generateNextSprite } from '@gromlab/svg-sprites' + +const result = await generateNextSprite( + 'src/ui/file-manager/svg-sprite', + { + router: 'app', + bundler: 'turbopack', + }, +) +``` + +Доступные значения: + +```ts +type NextSpriteGenerationOptions = { + router: 'app' | 'pages' + bundler: 'turbopack' | 'webpack' +} +``` + +Результат дополнительно содержит выбранные `router`, `bundler` и полный target вида `next@app/turbopack`. + +## `defineReactSpriteConfig` + +```ts +import { defineReactSpriteConfig } from '@gromlab/svg-sprites' + +export default defineReactSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', + inputFolder: './icons', + inputFiles: [ + '../../shared/icons/check.svg', + ], + transform: { + removeSize: true, + replaceColors: true, + addTransition: true, + }, + generatedNotice: true, +}) +``` + +`inputFolder` и `inputFiles` объединяются. Хелпер возвращает конфиг без runtime-преобразований и предоставляет TypeScript autocomplete. + +## `defineNextSpriteConfig` + +```ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', + inputFolder: './icons', +}) +``` + +Next.js использует тот же контракт конфигурации, что и React presets. + +## `generateLegacy` + +```ts +import { generateLegacy } from '@gromlab/svg-sprites' + +const results = await generateLegacy({ + output: 'public/sprites', + preview: false, + sprites: [ + { + name: 'icons', + input: 'src/assets/icons', + format: 'symbol', + }, + ], +}) +``` + +Возвращается массив: + +```ts +type SpriteResult = { + name: string + format: 'symbol' | 'stack' + spritePath: string + iconCount: number +} +``` + +Подробнее: [Legacy mode](legacy.md). + +## Низкоуровневые функции + +Основная точка входа также экспортирует: + +```ts +import { + compileSprite, + compileSpriteContent, + createShapeTransform, + generatePreview, + loadLegacyConfig, + loadReactSpriteConfig, + resolveSpriteEntry, + resolveSprites, +} from '@gromlab/svg-sprites' +``` + +Эти функции предназначены для собственного orchestration поверх существующего compiler и writer. Для стандартного использования предпочтительны `generateReactSprite` и `generateLegacy`. + +## React runtime entry + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +``` + +Типы: + +```ts +import type { + SpriteManifest, + SpriteManifestColor, + SpriteManifestIcon, + SpriteManifestLoader, + SpriteManifestModule, + SpriteViewerColorTheme, + SpriteViewerProps, + SpriteViewerSource, + SpriteViewerSources, +} from '@gromlab/svg-sprites/react' +``` + +React entry содержит `'use client'` и предназначен для debug-инструментов. Generated production-компоненты импортируются из локальных sprite-модулей приложения, а не из React entry пакета. + +`SpriteViewerProps.colorTheme` принимает `auto | light | dark`. Значение `auto` используется по умолчанию и следует `prefers-color-scheme`; для синхронизации с темой приложения передавайте вычисленное `light` или `dark`. + +## Связанные руководства + +- [React + Vite](react-vite.md) +- [React + Webpack 5](react-webpack.md) diff --git a/docs/ru/react-vite.md b/docs/ru/react-vite.md new file mode 100644 index 0000000..1aa146e --- /dev/null +++ b/docs/ru/react-vite.md @@ -0,0 +1,116 @@ +# React + Vite + +[← Главная](../../README.md) + +Краткая инструкция по установке и использованию SVG-спрайтов в проекте на React и Vite. + +В результате вы получите типизированный React-компонент и отдельный кешируемый SVG asset. + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Создайте папку спрайта + +```text +src/ui/file-manager/svg-sprite/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Поместите исходные SVG-файлы в `icons/`. + +## 3. Добавьте конфиг + +```ts +// src/ui/file-manager/svg-sprite/svg-sprite.config.ts +import { defineReactSpriteConfig } from '@gromlab/svg-sprites' + +export default defineReactSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', +}) +``` + +По умолчанию SVG берутся из `./icons`. Общие иконки из других папок можно добавить через `inputFiles`: папка и список объединяются в один спрайт. + +Полный список опций находится в разделе [Конфигурация → React](../../README.md#react). + +## 4. Добавьте генерацию в package.json + +```json +{ + "scripts": { + "sprite:file-manager": "svg-sprites --mode react@vite src/ui/file-manager/svg-sprite", + "predev": "npm run sprite:file-manager", + "prebuild": "npm run sprite:file-manager", + "pretypecheck": "npm run sprite:file-manager" + } +} +``` + +Generated-файлы исключаются из Git, поэтому генерация должна выполняться перед `dev`, `build` и `typecheck`. + +Первый запуск: + +```bash +npm run sprite:file-manager +``` + +## 5. Используйте компонент + +Имя `file-manager` преобразуется в `FileManagerIcon`: + +```tsx +import { FileManagerIcon } from './svg-sprite' + +export const OpenFolderButton = () => ( + +) +``` + +Значение `icon` проверяется TypeScript по именам файлов: + +```tsx + // допустимо + // ошибка TypeScript +``` + +Типы, способы отображения и управление цветами описаны в [основной документации](../../README.md#способы-отображения). + +Vite выпустит спрайт отдельным файлом вида `assets/sprite-.svg`. SVG path-данные не попадут в JavaScript. + +## 6. Добавьте debug-страницу + +После подключения иконок можно вывести все React-спрайты через `SpriteViewer`: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' + +const sources = import.meta.glob( + '/src/**/svg-sprite/manifest.ts', +) + +export const IconsDebugPage = () => ( + +) +``` + +Vite автоматически найдёт generated `manifest.ts` каждого React-спрайта. Шаблон `import.meta.glob` должен быть строковым литералом, а генерация должна выполниться до запуска Vite. + +Размещайте Viewer только на debug-маршруте или во внутреннем инструменте. + +## Если что-то не работает + +- Нет `index.ts`: запустите `npm run sprite:file-manager`. +- Viewer не видит спрайт: проверьте путь glob и наличие `manifest.ts`. +- Ошибка `Refusing to overwrite a user file`: в generated-пути находится пользовательский файл. +- Иконка не меняет цвет: используйте `color` или `--icon-color-N`. diff --git a/docs/ru/react-webpack.md b/docs/ru/react-webpack.md new file mode 100644 index 0000000..bd4ccd9 --- /dev/null +++ b/docs/ru/react-webpack.md @@ -0,0 +1,118 @@ +# React + Webpack 5 + +[← Главная](../../README.md) + +Краткая инструкция по установке и использованию SVG-спрайтов в проекте на React и Webpack 5. + +В результате вы получите типизированный React-компонент и отдельный SVG asset через Webpack Asset Modules. + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Создайте папку спрайта + +```text +src/ui/file-manager/svg-sprite/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Поместите исходные SVG-файлы в `icons/`. + +## 3. Добавьте конфиг + +```ts +// src/ui/file-manager/svg-sprite/svg-sprite.config.ts +import { defineReactSpriteConfig } from '@gromlab/svg-sprites' + +export default defineReactSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', +}) +``` + +По умолчанию SVG берутся из `./icons`. Общие иконки из других папок можно добавить через `inputFiles`: папка и список объединяются в один спрайт. + +Полный список опций находится в разделе [Конфигурация → React](../../README.md#react). + +## 4. Добавьте генерацию в package.json + +```json +{ + "scripts": { + "sprite:file-manager": "svg-sprites --mode react@webpack src/ui/file-manager/svg-sprite", + "predev": "npm run sprite:file-manager", + "prebuild": "npm run sprite:file-manager", + "pretypecheck": "npm run sprite:file-manager" + } +} +``` + +Generated-файлы исключаются из Git, поэтому генерация должна выполняться перед `dev`, `build` и `typecheck`. + +Первый запуск: + +```bash +npm run sprite:file-manager +``` + +## 5. Используйте компонент + +```tsx +import { FileManagerIcon } from './svg-sprite' + +export const OpenFolderButton = () => ( + +) +``` + +Значение `icon` проверяется TypeScript по именам файлов: + +```tsx + // допустимо + // ошибка TypeScript +``` + +Типы, способы отображения и управление цветами описаны в [основной документации](../../README.md#способы-отображения). + +Webpack обработает generated `new URL('./sprite.svg', import.meta.url)` через Asset Modules и выпустит отдельный SVG asset. + +Если проект уже использует собственный SVG loader, убедитесь, что он не перехватывает generated `sprite.svg` вместо Asset Modules. + +## 6. Добавьте debug-страницу + +Webpack не поддерживает Vite API `import.meta.glob`, поэтому передайте статические loaders: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('./ui/file-manager/svg-sprite/manifest'), + () => import('./ui/navigation/svg-sprite/manifest'), +] + +export const IconsDebugPage = () => ( + +) +``` + +Пути в `import()` должны быть строковыми литералами. Webpack создаст chunks для манифестов и свяжет их с SVG assets. + +Размещайте Viewer только на debug-маршруте или во внутреннем инструменте. + +## Если что-то не работает + +- Нет `index.ts`: запустите `npm run sprite:file-manager`. +- Viewer не загружает спрайт: проверьте путь в `import()` и наличие `manifest.ts`. +- Неверный URL asset: проверьте `output.publicPath`. +- SVG перехватывает другой loader: исключите generated sprite из несовместимого правила. + +Для Next.js используйте отдельные mode key из руководств [App Router](next-app.md) и [Pages Router](next-pages.md). diff --git a/package-lock.json b/package-lock.json index f54f946..e623f8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gromlab/svg-sprites", - "version": "0.1.4", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gromlab/svg-sprites", - "version": "0.1.4", + "version": "1.0.0", "license": "MIT", "dependencies": { "colorette": "^2.0.20", @@ -18,12 +18,43 @@ }, "devDependencies": { "@types/node": "^22.15.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/svg-sprite": "^0.0.39", + "jsdom": "^26.1.0", + "next": "16.2.10", + "react": "^19.2.5", + "react-colorful": "^5.6.1", + "react-dom": "^19.2.5", "tsup": "^8.4.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vite": "^6.4.3", + "webpack": "^5.108.4" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "react": ">=18 <20" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, "node_modules/@colors/colors": { @@ -35,6 +66,121 @@ "node": ">=0.1.90" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", @@ -46,10 +192,21 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -64,9 +221,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -81,9 +238,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -98,9 +255,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -115,9 +272,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -132,9 +289,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -149,9 +306,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -166,9 +323,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -183,9 +340,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -200,9 +357,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -217,9 +374,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -234,9 +391,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -251,9 +408,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -268,9 +425,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -285,9 +442,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -302,9 +459,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -319,9 +476,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -336,9 +493,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -353,9 +510,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -370,9 +527,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -387,9 +544,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -404,9 +561,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -421,9 +578,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -438,9 +595,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -455,9 +612,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -472,9 +629,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -488,6 +645,497 @@ "node": ">=18" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -509,6 +1157,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -527,6 +1186,149 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@next/env": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.10.tgz", + "integrity": "sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.10.tgz", + "integrity": "sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.10.tgz", + "integrity": "sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.10.tgz", + "integrity": "sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.10.tgz", + "integrity": "sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.10.tgz", + "integrity": "sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.10.tgz", + "integrity": "sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.10.tgz", + "integrity": "sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.10.tgz", + "integrity": "sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@resvg/resvg-js": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", @@ -1102,6 +1904,16 @@ "text-hex": "1.0.x" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1116,6 +1928,13 @@ "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": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", @@ -1126,6 +1945,26 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "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/@types/svg-sprite": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/svg-sprite/-/svg-sprite-0.0.39.tgz", @@ -1155,6 +1994,167 @@ "@types/node": "*" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.13", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", @@ -1164,6 +2164,20 @@ "node": ">=10.0.0" } }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1177,6 +2191,77 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1244,6 +2329,19 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "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/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1260,6 +2358,47 @@ "concat-map": "0.0.1" } }, + "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/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bundle-require": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", @@ -1286,6 +2425,27 @@ "node": ">=8" } }, + "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/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1302,6 +2462,23 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true, + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1506,6 +2683,41 @@ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "license": "MIT" }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1524,6 +2736,24 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "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", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -1579,6 +2809,13 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "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/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1591,6 +2828,20 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -1600,10 +2851,17 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1614,32 +2872,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -1651,6 +2909,87 @@ "node": ">=6" } }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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/esrecurse/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/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "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-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1744,6 +3083,77 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1770,6 +3180,13 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -1788,6 +3205,21 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -1808,9 +3240,19 @@ } }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -1819,6 +3261,53 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -1855,6 +3344,20 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/lodash.escape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", @@ -1884,6 +3387,13 @@ "node": ">= 12.0.0" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1900,6 +3410,23 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "license": "CC0-1.0" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -1912,6 +3439,67 @@ "node": "*" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -1952,6 +3540,96 @@ "thenify-all": "^1.0.0" } }, + "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.2.10", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.10.tgz", + "integrity": "sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/env": "16.2.10", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.10", + "@next/swc-darwin-x64": "16.2.10", + "@next/swc-linux-arm64-gnu": "16.2.10", + "@next/swc-linux-arm64-musl": "16.2.10", + "@next/swc-linux-x64-gnu": "16.2.10", + "@next/swc-linux-x64-musl": "16.2.10", + "@next/swc-win32-arm64-msvc": "16.2.10", + "@next/swc-win32-x64-msvc": "16.2.10", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "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/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -1964,6 +3642,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1992,6 +3677,32 @@ "fn.name": "1.x.x" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2049,6 +3760,35 @@ "pathe": "^2.0.1" } }, + "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/postcss-load-config": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", @@ -2103,6 +3843,50 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "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.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -2156,6 +3940,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -2211,6 +4005,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2226,6 +4027,13 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -2235,6 +4043,106 @@ "node": ">=11.0.0" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2244,6 +4152,27 @@ "node": ">=0.10.0" } }, + "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/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -2295,6 +4224,30 @@ "node": ">=8" } }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -2328,6 +4281,22 @@ "node": ">= 6" } }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/svg-sprite": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-sprite/-/svg-sprite-2.0.4.tgz", @@ -2380,6 +4349,53 @@ "node": ">=10.13.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -2433,6 +4449,52 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2459,6 +4521,13 @@ "dev": true, "license": "Apache-2.0" }, + "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" + }, "node_modules/tsup": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", @@ -2550,6 +4619,37 @@ "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/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2573,6 +4673,211 @@ "node": ">= 0.10" } }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.108.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.4.tgz", + "integrity": "sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.2", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", @@ -2660,6 +4965,45 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xpath": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", diff --git a/package.json b/package.json index 1c7ad61..0459e55 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,18 @@ { "name": "@gromlab/svg-sprites", - "version": "0.1.4", - "description": "Generate SVG sprites (stack/symbol) and TypeScript icon name types from folders of SVG files", + "version": "1.0.0", + "description": "Generate SVG sprites and typed icon components for React and Next.js", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./react": { + "types": "./dist/react.d.ts", + "import": "./dist/react.js" } }, "bin": { @@ -18,31 +22,45 @@ "dist/index.js", "dist/index.js.map", "dist/index.d.ts", + "dist/react.js", + "dist/react.js.map", + "dist/react.d.ts", "dist/cli.js", "dist/cli.js.map", "dist/cli.d.ts", + "dist/types-*.d.ts", "dist/chunk-*.js", "dist/chunk-*.js.map", - "dist/preview-template.html" + "dist/preview-template.html", + "docs/ru/*.md", + "LICENSE", + "THIRD_PARTY_NOTICES.md" ], "scripts": { "build": "tsup && npm run build:preview", - "build:preview": "cd preview && npx vite build", + "build:preview": "npm ci --prefix preview && npm run build --prefix preview", + "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", - "prepublishOnly": "npm run build" + "sprite": "node dist/cli.js --mode legacy .", + "verify": "npm run check:skill && npm run typecheck && npm test", + "prepack": "npm run verify && npm run build" }, "keywords": [ "svg", "sprite", "svg-sprite", "icons", + "nextjs", "typescript", "codegen" ], "license": "MIT", + "publishConfig": { + "access": "public" + }, "repository": { "type": "git", "url": "https://gromlab.ru/gromov/svg-sprites" @@ -59,10 +77,31 @@ "jiti": "^2.6.1", "svg-sprite": "^2.0.4" }, + "peerDependencies": { + "react": ">=18 <20" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + }, + "overrides": { + "esbuild": "^0.28.1", + "postcss": "^8.5.10" + }, "devDependencies": { "@types/node": "^22.15.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/svg-sprite": "^0.0.39", + "jsdom": "^26.1.0", + "next": "16.2.10", + "react": "^19.2.5", + "react-colorful": "^5.6.1", + "react-dom": "^19.2.5", "tsup": "^8.4.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vite": "^6.4.3", + "webpack": "^5.108.4" } } diff --git a/preview/package-lock.json b/preview/package-lock.json index b2e4a3a..e8f21dd 100644 --- a/preview/package-lock.json +++ b/preview/package-lock.json @@ -30,13 +30,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "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.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -45,9 +45,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "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": { @@ -55,21 +55,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "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.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@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", @@ -86,14 +86,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "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.0", - "@babel/types": "^7.29.0", + "@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" @@ -103,14 +103,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "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.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@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" @@ -120,9 +120,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "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": { @@ -130,29 +130,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "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.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "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.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -162,9 +162,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "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": { @@ -172,9 +172,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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": { @@ -182,9 +182,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "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": { @@ -192,27 +192,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "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.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "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.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -222,33 +222,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "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.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@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.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "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.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@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": { @@ -256,35 +256,35 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "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.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@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.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "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.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "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, @@ -293,9 +293,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "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, @@ -577,14 +577,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "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.1" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -596,9 +596,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.126.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", - "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "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": { @@ -606,9 +606,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", + "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" ], @@ -623,9 +623,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", + "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" ], @@ -640,9 +640,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", + "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" ], @@ -657,9 +657,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", + "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" ], @@ -674,9 +674,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", - "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", + "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" ], @@ -691,9 +691,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", + "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" ], @@ -708,9 +708,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", + "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" ], @@ -725,9 +725,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", + "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" ], @@ -742,9 +742,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", + "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" ], @@ -759,9 +759,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", + "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" ], @@ -776,9 +776,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", + "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" ], @@ -793,9 +793,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", + "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" ], @@ -810,9 +810,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", - "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", + "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" ], @@ -820,18 +820,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.4" + "@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.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", + "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" ], @@ -846,9 +846,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", + "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" ], @@ -870,9 +870,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "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, @@ -1123,9 +1123,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "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": { @@ -1316,9 +1316,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", - "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "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": { @@ -1353,9 +1353,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", "dev": true, "funding": [ { @@ -1373,10 +1373,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "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": { @@ -1397,9 +1397,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "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": [ { @@ -1535,9 +1535,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.341", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.341.tgz", - "integrity": "sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==", + "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" }, @@ -2014,10 +2014,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "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" @@ -2439,9 +2449,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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": [ { @@ -2465,11 +2475,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "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" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/optionator": { "version": "0.9.4", @@ -2562,9 +2575,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "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": { @@ -2575,9 +2588,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -2595,7 +2608,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2665,14 +2678,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", - "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "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.126.0", - "@rolldown/pluginutils": "1.0.0-rc.16" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2681,27 +2694,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-x64": "1.0.0-rc.16", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" + "@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.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", - "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "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" }, @@ -2781,9 +2794,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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": { @@ -2931,17 +2944,17 @@ } }, "node_modules/vite": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", - "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", + "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.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.16", - "tinyglobby": "^0.2.16" + "picomatch": "^4.0.5", + "postcss": "^8.5.16", + "rolldown": "~1.1.4", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -2957,7 +2970,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..8d74059 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,12 @@ +# AI skills + +Исходники скила `svg-sprites` находятся в `skills/svg-sprites/`. Готовый самодостаточный артефакт записывается в `skills/artifacts/svg-sprites/` и коммитится в Git. + +Основные `README.md` и `docs/ru/*.md` являются источником истины. Сборка копирует их в `references/` готового скила, поэтому вручную редактировать файлы внутри `skills/artifacts/` нельзя. + +```bash +npm run build:skill +npm run check:skill +``` + +`build:skill` обновляет артефакт, а `check:skill` без изменения файлов проверяет его содержимое и синхронность с документацией. diff --git a/skills/artifacts/svg-sprites/SKILL.md b/skills/artifacts/svg-sprites/SKILL.md new file mode 100644 index 0000000..ffdecf1 --- /dev/null +++ b/skills/artifacts/svg-sprites/SKILL.md @@ -0,0 +1,64 @@ +--- +name: svg-sprites +description: "Используй при настройке, генерации, миграции или диагностике SVG-спрайтов через @gromlab/svg-sprites. Триггеры: SVG sprite, SVG-спрайт, svg-sprites, svg-sprite.config.ts, svg-sprites.config.ts, defineReactSpriteConfig, defineNextSpriteConfig, react@vite, react@webpack, next@app, next@pages, inputFiles, SpriteViewer, icon=\"...\", --icon-color-N, generated-компонент или иконка не появилась в превью и автодополнении. НЕ используй для favicon, растровых изображений, icon fonts, выбора набора иконок или inline SVG без спрайтов." +--- + + + +# SVG Sprites + +## Назначение + +Используй этот скил для работы с `@gromlab/svg-sprites`: первичной настройки, добавления и переиспользования иконок, генерации React-компонентов, подключения `SpriteViewer`, миграции legacy-конфигурации и диагностики ошибок. + +Не навязывай проекту конкретную архитектуру каталогов. Сначала изучи существующие `package.json`, конфигурацию спрайта, используемый фреймворк, роутер и сборщик. + +## Рабочий алгоритм + +1. Определи существующий режим и не смешивай его API с другим режимом. +2. Для React выбери `react@vite` или `react@webpack` и открой соответствующий reference. +3. Для Next.js определи App Router или Pages Router, затем Turbopack или Webpack, и открой соответствующий reference. +4. Для существующего `svg-sprites.config.ts` с несколькими спрайтами используй legacy-документацию. Не мигрируй такой проект без явного запроса. +5. Изучи локальные scripts и добавляй генерацию перед `dev`, `build` и `typecheck`, если generated-файлы не хранятся в Git. +6. После изменения конфигурации или SVG запусти генерацию, затем доступную проверку типов или сборку проекта. + +## Правила React и Next.js + +- Используй локальный `svg-sprite.config.ts` и подходящий config helper: `defineReactSpriteConfig` или `defineNextSpriteConfig`. +- Не редактируй `generated/`, `index.ts`, `manifest.ts` и созданный генератором `.gitignore` вручную. +- Имена исходных SVG становятся допустимыми значениями prop `icon`; используй generated-компонент и его публичные типы вместо deep imports. +- Объединяй локальную папку и `inputFiles`, когда общая иконка нужна нескольким спрайтам. Не создавай копии одного SVG без необходимости. +- В Next.js generated-компонент можно использовать в Server Components, SSR и SSG. Не добавляй `'use client'` только ради иконки. +- Спрайт должен оставаться внешним asset сборщика: не переносить SVG path-данные в JavaScript и не класть generated-файл вручную в `public`. + +## Цвета и трансформации + +- По умолчанию генератор удаляет `width` и `height`, заменяет поддерживаемые `fill` и `stroke` на CSS-переменные и добавляет transitions. +- Для монохромной иконки сначала управляй `color`; для многоцветной используй `--icon-color-N`. +- Не обещай автоматическую замену цветов внутри внешних stylesheets, gradients, patterns, filters и значений `url(#...)` без проверки результата. +- CSS-переменные страницы работают при ``, но не проникают внутрь `` и `background-image`. + +## Превью + +Для React и Next.js подключай `` отдельной debug-страницей приложения. Передай ему manifests или lazy loaders спрайтов. Viewer поддерживает поиск, светлую и тёмную темы, настройку цветов и примеры React, SVG, IMG и CSS. + +`SpriteViewer` является клиентским debug-инструментом и импортируется из `@gromlab/svg-sprites/react`; production-компоненты иконок от него не зависят. + +## Диагностика + +- Если имя иконки отсутствует в автодополнении, проверь входную папку и `inputFiles`, затем перезапусти генерацию. +- Если два файла имеют одинаковое имя иконки, устрани конфликт вместо выбора одного файла неявно. +- Если генератор отказывается перезаписывать файл, не удаляй защитный marker и не обходи writer: перенеси пользовательский файл или выбери другой каталог спрайта. +- Если asset не загружается, сначала проверь соответствие CLI mode реальному сборщику проекта и обработку generated SVG его asset pipeline. +- Если проект использует старый API, сверь установленную версию пакета и legacy reference перед изменениями. + +## References + +- [Основная документация и API](./references/README.md) +- [React + Vite](./references/docs/ru/react-vite.md) +- [React + Webpack 5](./references/docs/ru/react-webpack.md) +- [Next.js App Router](./references/docs/ru/next-app.md) +- [Next.js Pages Router](./references/docs/ru/next-pages.md) +- [Legacy mode](./references/docs/ru/legacy.md) +- [Миграция с 0.1.x](./references/docs/ru/migration-1.md) +- [Программный API](./references/docs/ru/programmatic-api.md) diff --git a/skills/artifacts/svg-sprites/references/README.md b/skills/artifacts/svg-sprites/references/README.md new file mode 100644 index 0000000..8242cc9 --- /dev/null +++ b/skills/artifacts/svg-sprites/references/README.md @@ -0,0 +1,395 @@ +# @gromlab/svg-sprites + +![npm](https://img.shields.io/npm/v/@gromlab/svg-sprites) ![license](https://img.shields.io/npm/l/@gromlab/svg-sprites) + +CLI для генерации SVG-спрайтов и типизированных компонентов иконок для React и Next.js. + +![Preview](https://gromlab.ru/gromov/svg-sprites/media/branch/master/preview-image.png) + +## Навигация + +- [Возможности](#возможности) +- [Таблица поддержки](#таблица-поддержки) +- [Требования](#требования) +- [Быстрый старт](#быстрый-старт) + - [React + Vite](docs/ru/react-vite.md) + - [React + Webpack 5](docs/ru/react-webpack.md) + - [Next.js App Router](docs/ru/next-app.md) + - [Next.js Pages Router](docs/ru/next-pages.md) +- [Конфигурация](#конфигурация) + - [React](#react) + - [Next.js](#nextjs) +- [Множественные спрайты](#множественные-спрайты) +- [TypeScript](#typescript) +- [Форматы спрайтов](#форматы-спрайтов) +- [Способы отображения](#способы-отображения) +- [Трансформации](#трансформации) +- [Управление цветом иконок](#управление-цветом-иконок) +- [Кеширование](#кеширование) +- [SpriteViewer](#spriteviewer) +- [Миграция с 0.1.x](docs/ru/migration-1.md) +- [Документация](#документация) + +## Возможности + +- **TypeScript-friendly** — типизированные React-компоненты, union-типы и runtime-списки доступных иконок. +- **Чистая генерация** — generated-файлы автоматически исключаются из Git, спрайт не нужно вручную размещать в `public`, а генератор обновляет только принадлежащие ему файлы. +- **Общие иконки без копирования** — SVG из локальной папки и `inputFiles` объединяются в один спрайт; один файл можно использовать в нескольких спрайтах. +- **Встроенное интерактивное превью** — `` подключается как страница приложения и показывает переданные React- и Next.js-спрайты с поиском, настройкой цветов и примерами использования. +- **Настраиваемые трансформации SVG** — удаление `width` и `height` с сохранением `viewBox`, замена исходных цветов на CSS-переменные и transitions для `fill` и `stroke`. +- **Отдельный кешируемый SVG asset** — SVG path-данные не попадают в JavaScript chunks, а сборщик выпускает файл с content hash. +- **Множественные спрайты** — независимые React- и Next.js-модули со своими компонентами, типами и SVG assets. +- **Server-first Next.js** — generated-компоненты работают в Server Components, SSR и SSG без директивы `'use client'`. +- **Форматы под разные сценарии** — React и Next.js используют `stack`, legacy-режим также поддерживает `symbol` для существующих интеграций. + +## Таблица поддержки + +| Среда | Ключ мода API | Статус | +|---|---|---| +| React + Vite | `react@vite` | Готово | +| React + Webpack 5 | `react@webpack` | Готово | +| Next.js 16.2+ App Router + Turbopack | `next@app/turbopack` | Готово | +| Next.js 13.4+ App Router + Webpack 5 | `next@app/webpack` | Готово | +| Next.js 16.2+ Pages Router + Turbopack | `next@pages/turbopack` | Готово | +| Next.js 12.2+ Pages Router + Webpack 5 | `next@pages/webpack` | Готово | +| Vue | — | Скоро | +| Standalone | — | Скоро | + +## Требования + +- Node.js 18 или новее; +- пакет распространяется только как ESM и подключается через `import`; +- React 18 или 19 требуется только для generated-компонентов и точки входа `@gromlab/svg-sprites/react`; +- для типизации subpath exports используйте TypeScript 5+ с `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`. + +## Быстрый старт + +Для быстрого старта воспользуйтесь инструкцией для вашего стека: + +- [React + Vite](docs/ru/react-vite.md) +- [React + Webpack 5](docs/ru/react-webpack.md) +- [Next.js App Router](docs/ru/next-app.md) +- [Next.js Pages Router](docs/ru/next-pages.md) + +## Конфигурация + +### React + +```ts +import { defineReactSpriteConfig } from '@gromlab/svg-sprites' + +export default defineReactSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', + inputFolder: './icons', + inputFiles: [ + '../../shared/icons/check.svg', + ], + transform: { + removeSize: true, + replaceColors: true, + addTransition: true, + }, + generatedNotice: true, +}) +``` + +| Опция | Тип | По умолчанию | Назначение | +|---|---|---|---| +| `name` | `string` | Имя папки | Имя спрайта, компонента и публичных типов | +| `description` | `string` | Нет | Описание для типов и debug-манифеста | +| `inputFolder` | `string` | `./icons` | Папка с исходными SVG относительно конфига | +| `inputFiles` | `string[]` | `[]` | Дополнительные SVG-файлы относительно конфига | +| `transform` | `TransformOptions` | Все включены | [Настройки трансформации](#трансформации) исходных SVG | +| `generatedNotice` | `boolean` | `true` | Полное либо короткое предупреждение в generated-файлах | + +`inputFolder` и `inputFiles` объединяются в один спрайт, поэтому один SVG-файл можно использовать в нескольких спрайтах без копирования. Если неявной папки `./icons` нет, но `inputFiles` заполнен, генерация продолжается только по списку. Явно указанная отсутствующая папка считается ошибкой. Одинаковые пути дедуплицируются, а разные файлы с одинаковым именем иконки считаются ошибкой. + +`name` записывается в kebab-case и должно начинаться с латинской буквы. React и Next.js presets создают формат `stack`. + +### Next.js + +Next.js использует тот же `svg-sprite.config.ts` и набор опций. Для типизации можно использовать отдельный хелпер: + +```ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', + inputFolder: './icons', +}) +``` + +Роутер и сборщик выбираются через mode key, поэтому переключение между Turbopack и Webpack всегда явно отражено в команде генерации. + +## Множественные спрайты + +Приложение может содержать несколько независимых спрайтов с разной областью использования: + +**Проблема:** один глобальный спрайт загружает иконки, которые текущему экрану не нужны. + +**Решение:** общие иконки хранить глобально, а наборы страниц и крупных компонентов — в отдельных спрайтах, загружаемых вместе с ними. + +```text +global → GlobalIcon → общие иконки приложения +analytics-page → AnalyticsPageIcon → иконки отдельной страницы +file-manager → FileManagerIcon → иконки крупного компонента +``` + +- **Глобальный спрайт** содержит небольшие общие иконки, используемые в разных частях приложения: навигацию, состояния и базовые действия. +- **Спрайт страницы** загружается вместе с конкретным разделом и не увеличивает общий спрайт иконками, которые больше нигде не нужны. +- **Спрайт крупного компонента** инкапсулирует собственный набор иконок сложного UI-модуля, например файлового менеджера или редактора. + +Каждая группа получает: + +- собственный SVG asset; +- собственный типизированный компонент; +- отдельный список имён иконок; +- отдельный debug-манифест; +- независимый cache lifecycle. + + +## TypeScript + +Главная возможность TypeScript API — автодополнение имён иконок непосредственно в prop `icon`: + +```tsx + +// ↑ редактор предлагает все иконки спрайта +``` + +Имена SVG-файлов становятся допустимыми значениями `icon`. Опечатка или неизвестное имя сразу становятся ошибкой TypeScript: + +```tsx + // ошибка TypeScript +``` + +Для программного доступа generated-модуль экспортирует readonly-массив всех доступных иконок конкретного спрайта: + +```ts +import { fileManagerIconNames } from './svg-sprite' + +// readonly ['check', 'folder', ...] +``` + +Этот список можно использовать в собственных каталогах, select-компонентах, тестах и других runtime-сценариях. Из него также выводится union-тип `FileManagerIconName`. + +Имена файлов с пробелами и другими небезопасными для SVG ID символами остаются частью публичного TypeScript API. Для внутреннего `` генератор создаёт стабильный hash ID. + +```text +folder open.svg → icon="folder open" → id="icon-" +``` + +Для таких имён используйте generated-компонент или `id` из debug-манифеста. Ручные примеры ниже с `#<имя>` подходят только для имён, которые уже являются безопасными SVG ID. + +## Форматы спрайтов + +`stack` — более современный формат, поэтому он используется по умолчанию. Иконки можно отображать через ``, `` и CSS `background-image`. + +`symbol` сохраняется для совместимости с существующими интеграциями и поддерживает отображение только через ``. + +## Способы отображения + +### React-компонент — рекомендуется + +Generated-компонент предоставляет типизацию, автодополнение имён иконок и сам формирует URL SVG asset. + +```tsx + +``` + +Через `color` и `--icon-color-N` доступны одноцветные и многоцветные иконки. + +### Самостоятельно через `` + +Хороший низкоуровневый способ с полным управлением размерами и цветами. React-компонент под капотом использует именно его. + +Способ получения `spriteUrl` зависит от сборщика. + +**Vite:** + +```tsx +import spriteUrl from './svg-sprite/generated/sprite.svg?no-inline' +``` + +**Webpack 5:** + +```tsx +const spriteUrl = new URL( + './svg-sprite/generated/sprite.svg', + import.meta.url, +).href +``` + +**Next.js с Webpack 5 или Turbopack:** + +```tsx +const spriteUrl = new URL( + './svg-sprite/generated/sprite.svg', + import.meta.url, +).href +``` + +После получения URL иконка отображается одинаково: + +```tsx + + + +``` + +Vite, Webpack 5 и Next.js сами заменяют исходный путь на итоговый URL asset с hash. + +### Через `` — менее эффективно + +```tsx +Готово +``` + +SVG загружается как изолированное изображение: изменить его цвета через `color` или `--icon-color-N` нельзя. + +### Через CSS `background-image` — менее эффективно + +```css +.icon { + background: url('./svg-sprite/generated/sprite.svg#check') center / contain no-repeat; +} +``` + +Как и ``, этот способ не позволяет управлять внутренними цветами SVG. Путь указывается относительно CSS-файла, а Vite/Webpack заменяет его на итоговый URL с hash при сборке. + +### Через CSS mask — менее эффективно + +```css +.icon { + background-color: currentColor; + mask: url('./svg-sprite/generated/sprite.svg#check') center / contain no-repeat; +} +``` + +Mask оставляет только силуэт и окрашивает его одним цветом. Исходные цвета, gradients и различия между `fill` и `stroke` теряются. + +## Трансформации + +Все трансформации включены по умолчанию и настраиваются независимо через `transform`. + +| Опция | По умолчанию | Что делает | +|---|---|---| +| `removeSize` | `true` | Удаляет `width` и `height` с корневого ``, сохраняя существующий `viewBox`. Размер иконки после этого задаётся снаружи. | +| `replaceColors` | `true` | Заменяет цвета `fill` и `stroke` на `--icon-color-N`. Для одноцветной иконки fallback становится `currentColor`, для многоцветной сохраняются исходные цвета. | +| `addTransition` | `true` | Добавляет `style="transition:fill 0.3s,stroke 0.3s;"` непосредственно цветным элементам SVG. Существующий `transition` не перезаписывается. | + +Чтобы отключить преобразование, передайте для соответствующей опции `false`. Подробнее о результате `replaceColors` — в разделе [«Управление цветом иконок»](#управление-цветом-иконок). + +## Управление цветом иконок + +При включённой замене цветов генератор анализирует `fill` и `stroke` и преобразует их в CSS custom properties. + +### Монохромные иконки + +Если найден один цвет, fallback заменяется на `currentColor`: + +```svg +stroke="var(--icon-color-1, currentColor)" +``` + +Цветом управляет CSS-свойство `color` внешнего `` или его родителя. + +### Многоцветные иконки + +Каждый уникальный цвет получает отдельную переменную с исходным fallback: + +```svg +fill="var(--icon-color-1, #798198)" +fill="var(--icon-color-2, #ffffff)" +fill="var(--icon-color-3, #129d9d)" +``` + +Страница может заменить только необходимые цвета: + +```css +.icon { + --icon-color-1: #4b5563; + --icon-color-3: #14b8a6; +} +``` + +### Ограничения цветов + +- `none`, `transparent`, `inherit`, `unset` и `initial` не заменяются; +- цвета в атрибутах `fill`, `stroke` и inline `style` обрабатываются надёжнее всего; +- CSS-классы и внешние stylesheets внутри исходного SVG не являются основным сценарием трансформации; +- gradients, patterns, filters и значения `url(#...)` требуют отдельной проверки и могут быть несовместимы с автоматической заменой цветов; +- CSS-переменные страницы доступны при ``, но недоступны внутри `` и `background-image`. + +## Кеширование + +Vite, Webpack и Next.js target выпускают спрайт отдельным asset с content hash: + +```text +/assets/sprite-.svg +``` + +Это даёт следующие свойства: + +- SVG кешируется независимо от JavaScript; +- изменение React-кода не меняет содержимое спрайта; +- изменение иконок создаёт новый hash asset; +- один файл используется всеми экземплярами generated-компонента; +- SVG path-данные отсутствуют в JavaScript chunks. + +Vite target запрещает inline через `?no-inline`. Webpack 5 target использует Asset Modules через `new URL(..., import.meta.url)`. + +## SpriteViewer + +`SpriteViewer` — React-компонент для просмотра generated-спрайтов внутри debug-маршрута приложения. + +Он использует отдельные манифесты и показывает: + +- группы спрайтов; +- список и количество иконок; +- поиск и системную светлую/тёмную тему; +- модальное превью с `viewBox` и настройкой цветовых переменных; +- примеры React, SVG, IMG и CSS с копированием кода. + +Production-компоненты не импортируют debug-манифесты. Способ подключения Viewer зависит от сборщика: + +- [React + Vite: автоматический `import.meta.glob`](docs/ru/react-vite.md#6-добавьте-debug-страницу); +- [React + Webpack 5: статические `import()`](docs/ru/react-webpack.md#6-добавьте-debug-страницу); +- [Next.js App Router](docs/ru/next-app.md#5-добавьте-spriteviewer); +- [Next.js Pages Router](docs/ru/next-pages.md#5-добавьте-spriteviewer). + +Viewer подключается из отдельной клиентской точки входа `@gromlab/svg-sprites/react` и не попадает в production-компоненты иконок. + +### Тема Viewer + +По умолчанию `colorTheme="auto"`: Viewer следует `prefers-color-scheme` и реагирует на смену системной темы. Тему приложения можно передать явно: + +```tsx + +``` + +Допустимые значения `colorTheme`: `auto`, `light`, `dark`. При управлении темой извне встроенный переключатель скрывается. Чтобы оставить его и обновлять тему приложения через Viewer, передайте callback: + +```tsx + +``` + +## Документация + +- [React + Vite](docs/ru/react-vite.md) +- [React + Webpack 5](docs/ru/react-webpack.md) +- [Next.js App Router](docs/ru/next-app.md) +- [Next.js Pages Router](docs/ru/next-pages.md) +- [Legacy mode](docs/ru/legacy.md) +- [Миграция с 0.1.x](docs/ru/migration-1.md) +- [Программный API](docs/ru/programmatic-api.md) + +## Лицензия + +MIT diff --git a/skills/artifacts/svg-sprites/references/docs/ru/legacy.md b/skills/artifacts/svg-sprites/references/docs/ru/legacy.md new file mode 100644 index 0000000..5efd0cf --- /dev/null +++ b/skills/artifacts/svg-sprites/references/docs/ru/legacy.md @@ -0,0 +1,102 @@ +# Legacy mode + +[← Главная](../../README.md) + +Краткая инструкция по генерации централизованных SVG-спрайтов форматов `symbol` и `stack` с optional HTML preview. + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Подготовьте иконки и конфиг + +```text +project/ +├── src/assets/icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprites.config.ts +``` + +```ts +// svg-sprites.config.ts +import { defineLegacyConfig } from '@gromlab/svg-sprites' + +export default defineLegacyConfig({ + output: 'public/sprites', + preview: true, + sprites: [ + { + name: 'icons', + input: 'src/assets/icons', + format: 'symbol', + }, + ], +}) +``` + +## 3. Запустите генерацию + +```bash +npx svg-sprites --mode legacy . +``` + +Результат: + +```text +public/sprites/ +├── icons.sprite.svg +└── preview.html +``` + +При `preview: false` HTML-файл не создаётся. Для формата `stack` укажите `format: 'stack'`. + +## 4. Используйте symbol-спрайт + +```html + + + +``` + +## 5. Добавьте package script + +```json +{ + "scripts": { + "sprites": "svg-sprites --mode legacy .", + "prebuild": "npm run sprites" + } +} +``` + +## Несколько спрайтов + +Добавьте несколько записей в `sprites`: + +```ts +sprites: [ + { + name: 'icons', + input: 'src/assets/icons', + format: 'symbol', + }, + { + name: 'logos', + input: 'src/assets/logos', + format: 'stack', + }, +] +``` + +Все результаты и общий `preview.html` будут записаны в `output`. + +## Если что-то не работает + +- Не найден конфиг: убедитесь, что `svg-sprites.config.ts` находится в переданном корне. +- Нет иконок: проверьте `sprites[].input` и расширение `.svg`. +- Не нужен preview: установите `preview: false`. + +Для программного запуска используйте [`generateLegacy`](programmatic-api.md#generatelegacy). diff --git a/skills/artifacts/svg-sprites/references/docs/ru/migration-1.md b/skills/artifacts/svg-sprites/references/docs/ru/migration-1.md new file mode 100644 index 0000000..a41d3d9 --- /dev/null +++ b/skills/artifacts/svg-sprites/references/docs/ru/migration-1.md @@ -0,0 +1,96 @@ +# Миграция с 0.1.x на 1.0 + +[← Главная](../../README.md) + +Версия 1.0 разделяет локальную генерацию для React и Next.js и централизованный legacy-режим. Старый config нельзя смешивать с новым API в одном вызове CLI. + +## CLI + +CLI теперь всегда требует явный `--mode` и путь к каталогу конфигурации: + +```text +svg-sprites +→ svg-sprites --mode +``` + +Выберите mode по окружению: + +| Окружение | Mode | +|---|---| +| React + Vite | `react@vite` | +| React + Webpack 5 | `react@webpack` | +| Next.js App Router + Turbopack | `next@app/turbopack` | +| Next.js App Router + Webpack 5 | `next@app/webpack` | +| Next.js Pages Router + Turbopack | `next@pages/turbopack` | +| Next.js Pages Router + Webpack 5 | `next@pages/webpack` | +| Централизованная старая схема | `legacy` | + +## React и Next.js + +Вместо корневого `svg-sprites.config.ts` создайте локальный `svg-sprite.config.ts` рядом с набором иконок: + +```ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'global', + inputFolder: './icons', +}) +``` + +Для обычного React используйте `defineReactSpriteConfig`. Папку и явный список общих SVG можно объединить через `inputFolder` и `inputFiles`. + +Старые `publicPath` и `react` больше не нужны. Generated-модуль создаётся рядом с конфигом, сам добавляет `.gitignore`, а Vite, Webpack или Next.js выпускает SVG как отдельный asset с content hash. + +Компонент `` заменяется компонентом, имя которого выводится из `name`: + +```tsx + +``` + +Для просмотра иконок добавьте `` как debug-страницу приложения. Отдельный `preview.html` остаётся только в legacy-режиме. + +## Legacy-режим + +Если централизованную структуру нужно сохранить, переименуйте helper и поля формата: + +```ts +import { defineLegacyConfig } from '@gromlab/svg-sprites' + +export default defineLegacyConfig({ + output: 'public/sprites', + preview: true, + sprites: [ + { + name: 'icons', + input: 'src/assets/icons', + format: 'stack', + }, + ], +}) +``` + +- `defineConfig` заменён на `defineLegacyConfig`; +- `sprites[].mode` переименован в `sprites[].format`; +- `generate` заменён на `generateLegacy`; +- `loadConfig` заменён на `loadLegacyConfig`; +- `publicPath` и генерация старого общего React-компонента удалены. + +Запуск: + +```bash +svg-sprites --mode legacy . +``` + +## Программный API + +Пакет распространяется только как ESM. Замените `require()` на `import`. + +`compileSpriteContent` теперь возвращает `Promise`, чтобы публичные декларации не требовали установки `@types/node`. В Node.js фактический результат совместим с API, принимающими `Uint8Array`. + +## После миграции + +1. Удалите старые generated-файлы и правила, которые игнорировали целиком каталог с исходными иконками. +2. Добавьте явную команду генерации перед `dev`, `build` и `typecheck`. +3. Запустите генерацию и проверку типов. +4. Проверьте все иконки и цветовые переменные через `SpriteViewer` или legacy `preview.html`. diff --git a/skills/artifacts/svg-sprites/references/docs/ru/next-app.md b/skills/artifacts/svg-sprites/references/docs/ru/next-app.md new file mode 100644 index 0000000..593d02d --- /dev/null +++ b/skills/artifacts/svg-sprites/references/docs/ru/next-app.md @@ -0,0 +1,102 @@ +# Next.js App Router + +[← Главная](../../README.md) + +Поддерживаются два явных режима: + +| Сборщик | Mode key | Версия Next.js | +|---|---|---| +| Turbopack | `next@app/turbopack` | 16.2+ | +| Webpack 5 | `next@app/webpack` | 13.4+ | + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Создайте sprite-модуль + +```text +src/ui/file-manager/svg-sprite/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +```ts +// src/ui/file-manager/svg-sprite/svg-sprite.config.ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', +}) +``` + +## 3. Добавьте генерацию + +Для Turbopack: + +```json +{ + "scripts": { + "sprite:file-manager": "svg-sprites --mode next@app/turbopack src/ui/file-manager/svg-sprite", + "predev": "npm run sprite:file-manager", + "prebuild": "npm run sprite:file-manager" + } +} +``` + +Для Webpack замените mode key на `next@app/webpack`. В Next 13–15 Webpack используется обычной командой `next build`, в Next 16 — командой `next build --webpack`. + +## 4. Используйте в Server Component + +Generated-компонент не содержит `'use client'`, поэтому его можно импортировать непосредственно в `page.tsx` или `layout.tsx`: + +```tsx +import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' + +export default function Page() { + return ( +
+ +
+ ) +} +``` + +Next.js выпустит отдельный SVG asset с content hash. Один generated-код используется при SSR и в браузере без расхождения URL. + +## 5. Добавьте SpriteViewer + +Viewer интерактивен, поэтому для него нужна отдельная Client Component граница: + +```tsx +'use client' + +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('@/ui/file-manager/svg-sprite/manifest'), +] + +export default function SpritesPage() { + return +} +``` + +## Проверка сборщика + +```bash +# Turbopack +npx next build --turbopack + +# Webpack 5 +npx next build --webpack +``` + +Для Next 13–15 с Webpack используйте `npx next build` без флага. + +Команда Next.js и mode key генератора должны указывать один и тот же сборщик. diff --git a/skills/artifacts/svg-sprites/references/docs/ru/next-pages.md b/skills/artifacts/svg-sprites/references/docs/ru/next-pages.md new file mode 100644 index 0000000..d52750b --- /dev/null +++ b/skills/artifacts/svg-sprites/references/docs/ru/next-pages.md @@ -0,0 +1,96 @@ +# Next.js Pages Router + +[← Главная](../../README.md) + +Поддерживаются два явных режима: + +| Сборщик | Mode key | Версия Next.js | +|---|---|---| +| Turbopack | `next@pages/turbopack` | 16.2+ | +| Webpack 5 | `next@pages/webpack` | 12.2+ | + +Для Next.js 12.2 требуется React 18. + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Создайте sprite-модуль + +```text +src/ui/file-manager/svg-sprite/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +```ts +// src/ui/file-manager/svg-sprite/svg-sprite.config.ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', +}) +``` + +## 3. Добавьте генерацию + +```json +{ + "scripts": { + "sprite:file-manager": "svg-sprites --mode next@pages/webpack src/ui/file-manager/svg-sprite", + "predev": "npm run sprite:file-manager", + "prebuild": "npm run sprite:file-manager" + } +} +``` + +Для Next.js 16.2 с Turbopack замените mode key на `next@pages/turbopack`. + +## 4. Используйте на странице + +```tsx +import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' + +export default function FilesPage() { + return +} + +export function getServerSideProps() { + return { props: {} } +} +``` + +Компонент одинаково работает при SSR, SSG и клиентских переходах. Next.js выпускает отдельный SVG asset с content hash. + +## 5. Добавьте SpriteViewer + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('@/ui/file-manager/svg-sprite/manifest'), +] + +export default function SpritesPage() { + return +} +``` + +## Проверка сборщика + +```bash +# Turbopack +npx next build --turbopack + +# Webpack 5 +npx next build --webpack +``` + +Для Next 12–15 с Webpack используйте `npx next build` без флага. + +Команда Next.js и mode key генератора должны указывать один и тот же сборщик. diff --git a/skills/artifacts/svg-sprites/references/docs/ru/programmatic-api.md b/skills/artifacts/svg-sprites/references/docs/ru/programmatic-api.md new file mode 100644 index 0000000..510b5fc --- /dev/null +++ b/skills/artifacts/svg-sprites/references/docs/ru/programmatic-api.md @@ -0,0 +1,203 @@ +# Программный API + +[← Главная](../../README.md) + +Пакет предоставляет основную Node.js точку входа и отдельный React runtime entry. Обе точки распространяются только как ESM и подключаются через `import`. + +Для разрешения `@gromlab/svg-sprites/react` в TypeScript используйте `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`. + +## Основной entry + +```ts +import { + defineNextSpriteConfig, + defineReactSpriteConfig, + generateNextSprite, + generateReactSprite, +} from '@gromlab/svg-sprites' +``` + +Основной entry не импортирует React и может использоваться в CLI, build scripts и Node.js инструментах. + +## `generateReactSprite` + +```ts +import { generateReactSprite } from '@gromlab/svg-sprites' + +const result = await generateReactSprite( + 'src/ui/file-manager/svg-sprite', + 'vite', +) +``` + +Второй аргумент обязателен: + +```ts +type ReactAssetTarget = 'vite' | 'webpack' +``` + +Результат: + +```ts +type ReactSpriteGenerationResult = { + name: string + rootDir: string + generatedDir: string + spritePath: string + manifestPath: string + iconCount: number + target: 'vite' | 'webpack' +} +``` + +```ts +console.log(result.name) +console.log(result.iconCount) +console.log(result.spritePath) +console.log(result.manifestPath) +``` + +Функция загружает `svg-sprite.config.ts` из указанного корня, компилирует SVG и безопасно обновляет managed-файлы. + +## `generateNextSprite` + +```ts +import { generateNextSprite } from '@gromlab/svg-sprites' + +const result = await generateNextSprite( + 'src/ui/file-manager/svg-sprite', + { + router: 'app', + bundler: 'turbopack', + }, +) +``` + +Доступные значения: + +```ts +type NextSpriteGenerationOptions = { + router: 'app' | 'pages' + bundler: 'turbopack' | 'webpack' +} +``` + +Результат дополнительно содержит выбранные `router`, `bundler` и полный target вида `next@app/turbopack`. + +## `defineReactSpriteConfig` + +```ts +import { defineReactSpriteConfig } from '@gromlab/svg-sprites' + +export default defineReactSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', + inputFolder: './icons', + inputFiles: [ + '../../shared/icons/check.svg', + ], + transform: { + removeSize: true, + replaceColors: true, + addTransition: true, + }, + generatedNotice: true, +}) +``` + +`inputFolder` и `inputFiles` объединяются. Хелпер возвращает конфиг без runtime-преобразований и предоставляет TypeScript autocomplete. + +## `defineNextSpriteConfig` + +```ts +import { defineNextSpriteConfig } from '@gromlab/svg-sprites' + +export default defineNextSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', + inputFolder: './icons', +}) +``` + +Next.js использует тот же контракт конфигурации, что и React presets. + +## `generateLegacy` + +```ts +import { generateLegacy } from '@gromlab/svg-sprites' + +const results = await generateLegacy({ + output: 'public/sprites', + preview: false, + sprites: [ + { + name: 'icons', + input: 'src/assets/icons', + format: 'symbol', + }, + ], +}) +``` + +Возвращается массив: + +```ts +type SpriteResult = { + name: string + format: 'symbol' | 'stack' + spritePath: string + iconCount: number +} +``` + +Подробнее: [Legacy mode](legacy.md). + +## Низкоуровневые функции + +Основная точка входа также экспортирует: + +```ts +import { + compileSprite, + compileSpriteContent, + createShapeTransform, + generatePreview, + loadLegacyConfig, + loadReactSpriteConfig, + resolveSpriteEntry, + resolveSprites, +} from '@gromlab/svg-sprites' +``` + +Эти функции предназначены для собственного orchestration поверх существующего compiler и writer. Для стандартного использования предпочтительны `generateReactSprite` и `generateLegacy`. + +## React runtime entry + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +``` + +Типы: + +```ts +import type { + SpriteManifest, + SpriteManifestColor, + SpriteManifestIcon, + SpriteManifestLoader, + SpriteManifestModule, + SpriteViewerColorTheme, + SpriteViewerProps, + SpriteViewerSource, + SpriteViewerSources, +} from '@gromlab/svg-sprites/react' +``` + +React entry содержит `'use client'` и предназначен для debug-инструментов. Generated production-компоненты импортируются из локальных sprite-модулей приложения, а не из React entry пакета. + +`SpriteViewerProps.colorTheme` принимает `auto | light | dark`. Значение `auto` используется по умолчанию и следует `prefers-color-scheme`; для синхронизации с темой приложения передавайте вычисленное `light` или `dark`. + +## Связанные руководства + +- [React + Vite](react-vite.md) +- [React + Webpack 5](react-webpack.md) diff --git a/skills/artifacts/svg-sprites/references/docs/ru/react-vite.md b/skills/artifacts/svg-sprites/references/docs/ru/react-vite.md new file mode 100644 index 0000000..1aa146e --- /dev/null +++ b/skills/artifacts/svg-sprites/references/docs/ru/react-vite.md @@ -0,0 +1,116 @@ +# React + Vite + +[← Главная](../../README.md) + +Краткая инструкция по установке и использованию SVG-спрайтов в проекте на React и Vite. + +В результате вы получите типизированный React-компонент и отдельный кешируемый SVG asset. + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Создайте папку спрайта + +```text +src/ui/file-manager/svg-sprite/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Поместите исходные SVG-файлы в `icons/`. + +## 3. Добавьте конфиг + +```ts +// src/ui/file-manager/svg-sprite/svg-sprite.config.ts +import { defineReactSpriteConfig } from '@gromlab/svg-sprites' + +export default defineReactSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', +}) +``` + +По умолчанию SVG берутся из `./icons`. Общие иконки из других папок можно добавить через `inputFiles`: папка и список объединяются в один спрайт. + +Полный список опций находится в разделе [Конфигурация → React](../../README.md#react). + +## 4. Добавьте генерацию в package.json + +```json +{ + "scripts": { + "sprite:file-manager": "svg-sprites --mode react@vite src/ui/file-manager/svg-sprite", + "predev": "npm run sprite:file-manager", + "prebuild": "npm run sprite:file-manager", + "pretypecheck": "npm run sprite:file-manager" + } +} +``` + +Generated-файлы исключаются из Git, поэтому генерация должна выполняться перед `dev`, `build` и `typecheck`. + +Первый запуск: + +```bash +npm run sprite:file-manager +``` + +## 5. Используйте компонент + +Имя `file-manager` преобразуется в `FileManagerIcon`: + +```tsx +import { FileManagerIcon } from './svg-sprite' + +export const OpenFolderButton = () => ( + +) +``` + +Значение `icon` проверяется TypeScript по именам файлов: + +```tsx + // допустимо + // ошибка TypeScript +``` + +Типы, способы отображения и управление цветами описаны в [основной документации](../../README.md#способы-отображения). + +Vite выпустит спрайт отдельным файлом вида `assets/sprite-.svg`. SVG path-данные не попадут в JavaScript. + +## 6. Добавьте debug-страницу + +После подключения иконок можно вывести все React-спрайты через `SpriteViewer`: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' + +const sources = import.meta.glob( + '/src/**/svg-sprite/manifest.ts', +) + +export const IconsDebugPage = () => ( + +) +``` + +Vite автоматически найдёт generated `manifest.ts` каждого React-спрайта. Шаблон `import.meta.glob` должен быть строковым литералом, а генерация должна выполниться до запуска Vite. + +Размещайте Viewer только на debug-маршруте или во внутреннем инструменте. + +## Если что-то не работает + +- Нет `index.ts`: запустите `npm run sprite:file-manager`. +- Viewer не видит спрайт: проверьте путь glob и наличие `manifest.ts`. +- Ошибка `Refusing to overwrite a user file`: в generated-пути находится пользовательский файл. +- Иконка не меняет цвет: используйте `color` или `--icon-color-N`. diff --git a/skills/artifacts/svg-sprites/references/docs/ru/react-webpack.md b/skills/artifacts/svg-sprites/references/docs/ru/react-webpack.md new file mode 100644 index 0000000..bd4ccd9 --- /dev/null +++ b/skills/artifacts/svg-sprites/references/docs/ru/react-webpack.md @@ -0,0 +1,118 @@ +# React + Webpack 5 + +[← Главная](../../README.md) + +Краткая инструкция по установке и использованию SVG-спрайтов в проекте на React и Webpack 5. + +В результате вы получите типизированный React-компонент и отдельный SVG asset через Webpack Asset Modules. + +## 1. Установите пакет + +```bash +npm install @gromlab/svg-sprites +``` + +## 2. Создайте папку спрайта + +```text +src/ui/file-manager/svg-sprite/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Поместите исходные SVG-файлы в `icons/`. + +## 3. Добавьте конфиг + +```ts +// src/ui/file-manager/svg-sprite/svg-sprite.config.ts +import { defineReactSpriteConfig } from '@gromlab/svg-sprites' + +export default defineReactSpriteConfig({ + name: 'file-manager', + description: 'Иконки файлового менеджера', +}) +``` + +По умолчанию SVG берутся из `./icons`. Общие иконки из других папок можно добавить через `inputFiles`: папка и список объединяются в один спрайт. + +Полный список опций находится в разделе [Конфигурация → React](../../README.md#react). + +## 4. Добавьте генерацию в package.json + +```json +{ + "scripts": { + "sprite:file-manager": "svg-sprites --mode react@webpack src/ui/file-manager/svg-sprite", + "predev": "npm run sprite:file-manager", + "prebuild": "npm run sprite:file-manager", + "pretypecheck": "npm run sprite:file-manager" + } +} +``` + +Generated-файлы исключаются из Git, поэтому генерация должна выполняться перед `dev`, `build` и `typecheck`. + +Первый запуск: + +```bash +npm run sprite:file-manager +``` + +## 5. Используйте компонент + +```tsx +import { FileManagerIcon } from './svg-sprite' + +export const OpenFolderButton = () => ( + +) +``` + +Значение `icon` проверяется TypeScript по именам файлов: + +```tsx + // допустимо + // ошибка TypeScript +``` + +Типы, способы отображения и управление цветами описаны в [основной документации](../../README.md#способы-отображения). + +Webpack обработает generated `new URL('./sprite.svg', import.meta.url)` через Asset Modules и выпустит отдельный SVG asset. + +Если проект уже использует собственный SVG loader, убедитесь, что он не перехватывает generated `sprite.svg` вместо Asset Modules. + +## 6. Добавьте debug-страницу + +Webpack не поддерживает Vite API `import.meta.glob`, поэтому передайте статические loaders: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('./ui/file-manager/svg-sprite/manifest'), + () => import('./ui/navigation/svg-sprite/manifest'), +] + +export const IconsDebugPage = () => ( + +) +``` + +Пути в `import()` должны быть строковыми литералами. Webpack создаст chunks для манифестов и свяжет их с SVG assets. + +Размещайте Viewer только на debug-маршруте или во внутреннем инструменте. + +## Если что-то не работает + +- Нет `index.ts`: запустите `npm run sprite:file-manager`. +- Viewer не загружает спрайт: проверьте путь в `import()` и наличие `manifest.ts`. +- Неверный URL asset: проверьте `output.publicPath`. +- SVG перехватывает другой loader: исключите generated sprite из несовместимого правила. + +Для Next.js используйте отдельные mode key из руководств [App Router](next-app.md) и [Pages Router](next-pages.md). diff --git a/skills/svg-sprites/build.mjs b/skills/svg-sprites/build.mjs new file mode 100644 index 0000000..38d05c2 --- /dev/null +++ b/skills/svg-sprites/build.mjs @@ -0,0 +1,194 @@ +import { + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import config from './skill.config.mjs' + +const skillDir = path.dirname(fileURLToPath(import.meta.url)) +const outputDir = path.resolve(skillDir, config.output) +const artifactsDir = path.resolve(skillDir, '../artifacts') +const isCheck = process.argv.slice(2).includes('--check') + +function assertSafeRelativePath(relativePath) { + if ( + typeof relativePath !== 'string' + || relativePath.length === 0 + || path.isAbsolute(relativePath) + || relativePath.split(/[\\/]/).includes('..') + ) { + throw new Error(`Unsafe skill path: ${relativePath}`) + } +} + +function assertInside(parentDir, childPath) { + const relativePath = path.relative(parentDir, childPath) + if (relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))) return + throw new Error(`Path is outside ${parentDir}: ${childPath}`) +} + +function validateConfig() { + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.name)) { + throw new Error(`Invalid skill name: ${config.name}`) + } + if (typeof config.description !== 'string' || config.description.trim() === '') { + throw new Error('Skill description must be a non-empty string') + } + if (!Array.isArray(config.references) || config.references.length === 0) { + throw new Error('Skill references must be a non-empty array') + } + + assertInside(artifactsDir, outputDir) + assertSafeRelativePath(config.source) + + const targets = new Set() + for (const reference of config.references) { + assertSafeRelativePath(reference.to) + if (targets.has(reference.to)) throw new Error(`Duplicate reference target: ${reference.to}`) + targets.add(reference.to) + } +} + +function readRegularFile(filePath) { + if (!existsSync(filePath)) throw new Error(`Source file not found: ${filePath}`) + const stats = lstatSync(filePath) + if (stats.isSymbolicLink() || !stats.isFile()) { + throw new Error(`Source must be a regular file: ${filePath}`) + } + return readFileSync(filePath, 'utf8') +} + +function renderSkill() { + const sourcePath = path.resolve(skillDir, config.source) + assertInside(skillDir, sourcePath) + const body = readRegularFile(sourcePath).trim() + if (body.startsWith('---')) throw new Error('Source SKILL.md must not contain frontmatter') + + return [ + '---', + `name: ${config.name}`, + `description: ${JSON.stringify(config.description)}`, + '---', + '', + '', + '', + body, + '', + ].join('\n') +} + +function buildSkill(targetDir) { + rmSync(targetDir, { recursive: true, force: true }) + mkdirSync(targetDir, { recursive: true }) + writeFileSync(path.join(targetDir, 'SKILL.md'), renderSkill()) + + for (const reference of config.references) { + const sourcePath = path.resolve(skillDir, reference.from) + const targetPath = path.resolve(targetDir, reference.to) + assertInside(targetDir, targetPath) + readRegularFile(sourcePath) + mkdirSync(path.dirname(targetPath), { recursive: true }) + copyFileSync(sourcePath, targetPath) + } +} + +function listFiles(directory, prefix = '') { + const files = [] + for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const relativePath = path.posix.join(prefix, entry.name) + const filePath = path.join(directory, entry.name) + if (entry.isSymbolicLink()) throw new Error(`Skill artifact must not contain symlinks: ${filePath}`) + if (entry.isDirectory()) files.push(...listFiles(filePath, relativePath)) + else if (entry.isFile()) files.push(relativePath) + else throw new Error(`Unsupported skill artifact entry: ${filePath}`) + } + return files +} + +function validateMarkdown(skillRoot, relativePath) { + const filePath = path.join(skillRoot, relativePath) + const content = readFileSync(filePath, 'utf8') + const fences = content.match(/^```/gm)?.length ?? 0 + if (fences % 2 !== 0) throw new Error(`Unbalanced code fences: ${relativePath}`) + + for (const match of content.matchAll(/\]\(([^)]+)\)/g)) { + const target = match[1].trim().split(/\s+['"]/)[0] + if (!target || target.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(target)) continue + const targetPath = decodeURIComponent(target.split('#')[0].split('?')[0]) + const resolvedPath = path.resolve(path.dirname(filePath), targetPath) + assertInside(skillRoot, resolvedPath) + if (!existsSync(resolvedPath)) { + throw new Error(`Broken local link in ${relativePath}: ${target}`) + } + } +} + +function validateArtifact(skillRoot) { + const expectedFiles = [ + 'SKILL.md', + ...config.references.map((reference) => reference.to), + ].sort() + const actualFiles = listFiles(skillRoot).sort() + + if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) { + throw new Error(`Unexpected skill files:\n${actualFiles.join('\n')}`) + } + + const skill = readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8') + if (!skill.startsWith(`---\nname: ${config.name}\ndescription: `)) { + throw new Error('Generated SKILL.md has invalid frontmatter') + } + if (/\bTODO\b/.test(skill)) throw new Error('Generated SKILL.md contains TODO') + + for (const relativePath of actualFiles.filter((filePath) => filePath.endsWith('.md'))) { + validateMarkdown(skillRoot, relativePath) + } +} + +function compareArtifacts(expectedDir, actualDir) { + if (!existsSync(actualDir)) throw new Error(`Skill artifact is missing: ${actualDir}`) + + const expectedFiles = listFiles(expectedDir) + const actualFiles = listFiles(actualDir) + if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) { + throw new Error('Committed skill file list is out of date. Run npm run build:skill') + } + + const changedFiles = expectedFiles.filter((relativePath) => { + const expected = readFileSync(path.join(expectedDir, relativePath)) + const actual = readFileSync(path.join(actualDir, relativePath)) + return !expected.equals(actual) + }) + if (changedFiles.length > 0) { + throw new Error(`Committed skill is out of date:\n${changedFiles.join('\n')}`) + } +} + +validateConfig() + +if (isCheck) { + const temporaryRoot = mkdtempSync(path.join(tmpdir(), 'svg-sprites-skill-')) + try { + const expectedDir = path.join(temporaryRoot, config.name) + buildSkill(expectedDir) + validateArtifact(expectedDir) + compareArtifacts(expectedDir, outputDir) + console.log(`Skill is up to date: ${path.relative(process.cwd(), outputDir)}`) + } finally { + rmSync(temporaryRoot, { recursive: true, force: true }) + } +} else { + buildSkill(outputDir) + validateArtifact(outputDir) + console.log(`Built skill: ${path.relative(process.cwd(), outputDir)}`) +} diff --git a/skills/svg-sprites/skill.config.mjs b/skills/svg-sprites/skill.config.mjs new file mode 100644 index 0000000..9bc7577 --- /dev/null +++ b/skills/svg-sprites/skill.config.mjs @@ -0,0 +1,16 @@ +export default { + name: 'svg-sprites', + description: 'Используй при настройке, генерации, миграции или диагностике SVG-спрайтов через @gromlab/svg-sprites. Триггеры: SVG sprite, SVG-спрайт, svg-sprites, svg-sprite.config.ts, svg-sprites.config.ts, defineReactSpriteConfig, defineNextSpriteConfig, react@vite, react@webpack, next@app, next@pages, inputFiles, SpriteViewer, icon="...", --icon-color-N, generated-компонент или иконка не появилась в превью и автодополнении. НЕ используй для favicon, растровых изображений, icon fonts, выбора набора иконок или inline SVG без спрайтов.', + source: 'src/SKILL.md', + output: '../artifacts/svg-sprites', + references: [ + { from: '../../README.md', to: 'references/README.md' }, + { from: '../../docs/ru/react-vite.md', to: 'references/docs/ru/react-vite.md' }, + { from: '../../docs/ru/react-webpack.md', to: 'references/docs/ru/react-webpack.md' }, + { from: '../../docs/ru/next-app.md', to: 'references/docs/ru/next-app.md' }, + { from: '../../docs/ru/next-pages.md', to: 'references/docs/ru/next-pages.md' }, + { from: '../../docs/ru/legacy.md', to: 'references/docs/ru/legacy.md' }, + { from: '../../docs/ru/migration-1.md', to: 'references/docs/ru/migration-1.md' }, + { from: '../../docs/ru/programmatic-api.md', to: 'references/docs/ru/programmatic-api.md' }, + ], +} diff --git a/skills/svg-sprites/src/SKILL.md b/skills/svg-sprites/src/SKILL.md new file mode 100644 index 0000000..31e0f56 --- /dev/null +++ b/skills/svg-sprites/src/SKILL.md @@ -0,0 +1,57 @@ +# SVG Sprites + +## Назначение + +Используй этот скил для работы с `@gromlab/svg-sprites`: первичной настройки, добавления и переиспользования иконок, генерации React-компонентов, подключения `SpriteViewer`, миграции legacy-конфигурации и диагностики ошибок. + +Не навязывай проекту конкретную архитектуру каталогов. Сначала изучи существующие `package.json`, конфигурацию спрайта, используемый фреймворк, роутер и сборщик. + +## Рабочий алгоритм + +1. Определи существующий режим и не смешивай его API с другим режимом. +2. Для React выбери `react@vite` или `react@webpack` и открой соответствующий reference. +3. Для Next.js определи App Router или Pages Router, затем Turbopack или Webpack, и открой соответствующий reference. +4. Для существующего `svg-sprites.config.ts` с несколькими спрайтами используй legacy-документацию. Не мигрируй такой проект без явного запроса. +5. Изучи локальные scripts и добавляй генерацию перед `dev`, `build` и `typecheck`, если generated-файлы не хранятся в Git. +6. После изменения конфигурации или SVG запусти генерацию, затем доступную проверку типов или сборку проекта. + +## Правила React и Next.js + +- Используй локальный `svg-sprite.config.ts` и подходящий config helper: `defineReactSpriteConfig` или `defineNextSpriteConfig`. +- Не редактируй `generated/`, `index.ts`, `manifest.ts` и созданный генератором `.gitignore` вручную. +- Имена исходных SVG становятся допустимыми значениями prop `icon`; используй generated-компонент и его публичные типы вместо deep imports. +- Объединяй локальную папку и `inputFiles`, когда общая иконка нужна нескольким спрайтам. Не создавай копии одного SVG без необходимости. +- В Next.js generated-компонент можно использовать в Server Components, SSR и SSG. Не добавляй `'use client'` только ради иконки. +- Спрайт должен оставаться внешним asset сборщика: не переносить SVG path-данные в JavaScript и не класть generated-файл вручную в `public`. + +## Цвета и трансформации + +- По умолчанию генератор удаляет `width` и `height`, заменяет поддерживаемые `fill` и `stroke` на CSS-переменные и добавляет transitions. +- Для монохромной иконки сначала управляй `color`; для многоцветной используй `--icon-color-N`. +- Не обещай автоматическую замену цветов внутри внешних stylesheets, gradients, patterns, filters и значений `url(#...)` без проверки результата. +- CSS-переменные страницы работают при ``, но не проникают внутрь `` и `background-image`. + +## Превью + +Для React и Next.js подключай `` отдельной debug-страницей приложения. Передай ему manifests или lazy loaders спрайтов. Viewer поддерживает поиск, светлую и тёмную темы, настройку цветов и примеры React, SVG, IMG и CSS. + +`SpriteViewer` является клиентским debug-инструментом и импортируется из `@gromlab/svg-sprites/react`; production-компоненты иконок от него не зависят. + +## Диагностика + +- Если имя иконки отсутствует в автодополнении, проверь входную папку и `inputFiles`, затем перезапусти генерацию. +- Если два файла имеют одинаковое имя иконки, устрани конфликт вместо выбора одного файла неявно. +- Если генератор отказывается перезаписывать файл, не удаляй защитный marker и не обходи writer: перенеси пользовательский файл или выбери другой каталог спрайта. +- Если asset не загружается, сначала проверь соответствие CLI mode реальному сборщику проекта и обработку generated SVG его asset pipeline. +- Если проект использует старый API, сверь установленную версию пакета и legacy reference перед изменениями. + +## References + +- [Основная документация и API](./references/README.md) +- [React + Vite](./references/docs/ru/react-vite.md) +- [React + Webpack 5](./references/docs/ru/react-webpack.md) +- [Next.js App Router](./references/docs/ru/next-app.md) +- [Next.js Pages Router](./references/docs/ru/next-pages.md) +- [Legacy mode](./references/docs/ru/legacy.md) +- [Миграция с 0.1.x](./references/docs/ru/migration-1.md) +- [Программный API](./references/docs/ru/programmatic-api.md) diff --git a/src/cli.ts b/src/cli.ts index bf894b6..2925ed6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,12 +1,44 @@ #!/usr/bin/env node -import { loadConfig } from './config.js' -import { generate } from './generate.js' +import path from 'node:path' +import { CLI_USAGE, parseCliArgs } from './cli/parse-args.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 { - const config = await loadConfig() - await generate(config) + const args = parseCliArgs(process.argv.slice(2)) + + if ('help' in args) { + console.log(CLI_USAGE) + 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 + } + } } 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 new file mode 100644 index 0000000..d88a4e0 --- /dev/null +++ b/src/cli/parse-args.ts @@ -0,0 +1,119 @@ +import type { NextAssetTarget, ReactAssetTarget } from '../targets/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 ', + '', + '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', +].join('\n') + +export function parseCliArgs(argv: string[]): CliArgs | { help: true } { + if (argv.includes('--help') || argv.includes('-h')) { + return { help: true } + } + + let modeValue: string | undefined + const positional: string[] = [] + + for (let index = 0; index < argv.length; index++) { + const argument = argv[index] + + if (argument === '--mode') { + modeValue = argv[index + 1] + index++ + continue + } + + if (argument.startsWith('--mode=')) { + modeValue = argument.slice('--mode='.length) + 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}`) + } + + if (positional.length > 1) { + throw new Error(`Expected one sprite path, received: ${positional.join(', ')}`) + } + + if (modeValue === 'legacy') { + return { + mode: 'legacy', + path: positional[0], + } + } + + 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 new file mode 100644 index 0000000..0b29623 --- /dev/null +++ b/src/cli/types.ts @@ -0,0 +1,26 @@ +import type { NextAssetTarget, ReactAssetTarget } from '../targets/types.js' + +/** Корневой режим генерации, определяющий структуру создаваемых файлов. */ +export type GenerationMode = 'legacy' | 'next' | 'react' + +/** Аргументы legacy pipeline. */ +export type LegacyCliArgs = { + mode: 'legacy' + path: string +} + +/** Аргументы 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/codegen-react.ts b/src/codegen-react.ts deleted file mode 100644 index b2366a0..0000000 --- a/src/codegen-react.ts +++ /dev/null @@ -1,195 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import type { SpriteFolder, SpriteResult } from './types.js' - -/** Преобразует kebab-case строку в PascalCase. */ -function toPascalCase(str: string): string { - return str.replace(/(^|[-_])([a-z])/g, (_, __, c: string) => c.toUpperCase()) -} - -/** - * Собирает имена иконок из SpriteFolder. - */ -function getIconNames(folder: SpriteFolder): string[] { - return folder.files - .map((filePath) => path.basename(filePath, '.svg')) - .sort() -} - -/** - * Генерирует index.ts, [name].tsx и [name].module.css — React-компонент с типами. - * - * Имена файлов берутся из basename папки outputDir. - * Например: outputDir = 'src/ui/svg-sprite' → index.ts + svg-sprite.tsx + svg-sprite.module.css. - * - * Содержит: - * - union-типы имён иконок для каждого спрайта (IconsIconName, LogosIconName, ...) - * - SpriteMap, SpriteName, IconName - * - компонент SvgSprite с зашитым publicPath - */ -export function generateReactModule( - results: SpriteResult[], - folders: SpriteFolder[], - outputDir: string, - publicPath: string, -): string { - const typeBlocks: string[] = [] - const mapEntries: string[] = [] - const spriteFileEntries: string[] = [] - - for (const result of results) { - const folder = folders.find((f) => f.name === result.name) - if (!folder) continue - - const typeName = `${toPascalCase(result.name)}IconName` - const names = getIconNames(folder) - - typeBlocks.push( - `/** Имена иконок спрайта «${result.name}». */`, - `export type ${typeName} =`, - names.map((n) => ` | '${n}'`).join('\n'), - '', - ) - - mapEntries.push(` ${result.name}: ${typeName}`) - spriteFileEntries.push(` ${result.name}: '${result.name}.sprite.svg',`) - } - - const baseName = path.basename(outputDir) - const defaultSprite = results[0].name - - const lines = [ - '/**', - ' * SVG-спрайты: типы и React-компонент.', - ' * @generated — this file is auto-generated, do not edit manually.', - ' */', - "import type { SVGAttributes, HTMLAttributes } from 'react'", - `import styles from './${baseName}.module.css'`, - '', - ...typeBlocks, - '/** Маппинг имени спрайта на тип его иконок. */', - 'export type SpriteMap = {', - ...mapEntries, - '}', - '', - '/** Имя спрайта. */', - 'export type SpriteName = keyof SpriteMap', - '', - '/** Спрайт по умолчанию. */', - `export type DefaultSprite = '${defaultSprite}'`, - '', - '/** Имя иконки для конкретного спрайта. */', - 'export type IconName = SpriteMap[S]', - '', - `const PUBLIC_PATH = '${publicPath}'`, - `const DEFAULT_SPRITE: SpriteName = '${defaultSprite}'`, - '', - 'const SPRITE_FILES: Record = {', - ...spriteFileEntries, - '}', - '', - 'type IconBaseProps = {', - ' /** Имя иконки. */', - ' icon: IconName', - ' /** Имя спрайта. По умолчанию: первый из конфига. */', - ' sprite?: S', - '}', - '', - 'type IconSvgProps = IconBaseProps & {', - ' wrapped?: false', - '} & SVGAttributes', - '', - 'type IconWrappedProps = IconBaseProps & {', - ' wrapped: true', - '} & HTMLAttributes', - '', - 'export type SvgSpriteProps =', - ' | IconSvgProps', - ' | IconWrappedProps', - '', - '/**', - ' * Иконка из SVG-спрайта.', - ' *', - ' * Используется для:', - ' * - отображения иконки через ``', - ' * - обёртки в `` через проп `wrapped`', - ' *', - ` * Спрайт по умолчанию: «${defaultSprite}».`, - ' */', - 'export const SvgSprite = (props: SvgSpriteProps) => {', - ' const { icon, sprite = DEFAULT_SPRITE as S, wrapped, className, ...rest } = props', - // eslint-disable-next-line no-template-curly-in-string - ' const href = `${PUBLIC_PATH}/${SPRITE_FILES[sprite]}#${icon}`', - '', - ' if (wrapped) {', - ' const { ...htmlAttr } = rest as HTMLAttributes', - ' return (', - ' ', - ' ', - ' ', - ' ', - ' ', - ' )', - ' }', - '', - ' const { ...svgAttr } = rest as SVGAttributes', - ' return (', - ' ', - ' ', - ' ', - ' )', - '}', - '', - ] - - const content = lines.join('\n') - const outputPath = path.join(outputDir, `${baseName}.tsx`) - - fs.mkdirSync(outputDir, { recursive: true }) - fs.writeFileSync(outputPath, content) - - // Генерируем CSS Module - const css = [ - '/* @generated — this file is auto-generated, do not edit manually. */', - '', - '.root {', - ' transition-property: fill, stroke, color;', - ' transition-duration: 0.3s;', - ' transition-timing-function: ease;', - '}', - '', - '.wrap {', - ' display: inline-flex;', - '}', - '', - '.wrap svg {', - ' width: 100%;', - ' height: 100%;', - ' transition-property: fill, stroke, color;', - ' transition-duration: 0.3s;', - ' transition-timing-function: ease;', - '}', - '', - ].join('\n') - - const cssPath = path.join(outputDir, `${baseName}.module.css`) - fs.writeFileSync(cssPath, css) - - const index = [ - '/** @generated — this file is auto-generated, do not edit manually. */', - `export { SvgSprite } from './${baseName}'`, - 'export type {', - ' SvgSpriteProps,', - ' SpriteName,', - ' SpriteMap,', - ' IconName,', - ' DefaultSprite,', - `} from './${baseName}'`, - '', - ].join('\n') - - const indexPath = path.join(outputDir, 'index.ts') - fs.writeFileSync(indexPath, index) - - return outputPath -} diff --git a/src/compiler.ts b/src/compiler.ts index 8784c0a..6af838d 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,16 +1,27 @@ import fs from 'node:fs' import path from 'node:path' import SVGSpriter from 'svg-sprite' +import { getSpriteShapeId } from './shape-id.js' import { createShapeTransform } from './transforms.js' -import type { SpriteFolder, SpriteMode, TransformOptions } from './types.js' +import type { SpriteFolder, SpriteFormat, TransformOptions } from './types.js' + +export type CompileSpriteOptions = { + /** Добавлять вычисленный viewBox корневому stack-спрайту. */ + rootViewBox?: boolean +} /** Конфигурация режима для svg-sprite. */ -function getModeConfig(mode: SpriteMode, destDir: string, name: string) { +function getModeConfig( + format: SpriteFormat, + destDir: string, + name: string, + options: CompileSpriteOptions, +) { return { dest: destDir, sprite: `${name}.sprite.svg`, example: false, - rootviewbox: false, + rootviewbox: options.rootViewBox ?? false, } } @@ -47,13 +58,35 @@ export async function compileSprite( folder: SpriteFolder, outputDir: string, transform: TransformOptions = {}, + options: CompileSpriteOptions = {}, ): Promise { + const contents = await compileSpriteContent(folder, transform, options) + const spritePath = path.join(outputDir, `${folder.name}.sprite.svg`) + + fs.mkdirSync(outputDir, { recursive: true }) + fs.writeFileSync(spritePath, contents) + + return spritePath +} + +/** Компилирует SVG-спрайт в памяти, не изменяя файловую систему. */ +export async function compileSpriteContent( + folder: SpriteFolder, + transform: TransformOptions = {}, + options: CompileSpriteOptions = {}, +): Promise { const config = { shape: { + id: { + generator: (filePath: string) => getSpriteShapeId(filePath), + }, + dimension: { + attributes: transform.removeSize === false, + }, transform: buildShapeTransforms(transform), }, mode: { - [folder.mode]: getModeConfig(folder.mode, outputDir, folder.name), + [folder.format]: getModeConfig(folder.format, '.', folder.name, options), }, } @@ -70,21 +103,24 @@ export async function compileSprite( return } - let spritePath = '' + let spriteContents: Uint8Array | undefined for (const modeResult of Object.values(result)) { for (const resource of Object.values( - modeResult as Record, + modeResult as Record, )) { - fs.mkdirSync(path.dirname(resource.path), { recursive: true }) - fs.writeFileSync(resource.path, resource.contents) if (resource.path.endsWith('.svg')) { - spritePath = resource.path + spriteContents = resource.contents } } } - resolve(spritePath) + if (!spriteContents) { + reject(new Error(`Failed to compile sprite "${folder.name}".`)) + return + } + + resolve(spriteContents) }) }) } diff --git a/src/generate.ts b/src/generate.ts deleted file mode 100644 index 6bb1c14..0000000 --- a/src/generate.ts +++ /dev/null @@ -1,67 +0,0 @@ -import path from 'node:path' -import { resolveSprites } from './scanner.js' -import { compileSprite } from './compiler.js' -import { generateReactModule } from './codegen-react.js' -import { generatePreview } from './preview.js' -import { log } from './logger.js' -import type { SvgSpritesConfig, SpriteResult } from './types.js' - -/** - * Генерирует SVG-спрайты из конфига. - * - * Основная точка входа — используется и из CLI, и из программного API. - */ -export async function generate(config: SvgSpritesConfig): Promise { - const { - output, - publicPath, - preview = true, - react, - transform = {}, - sprites, - } = config - - const outputDir = path.resolve(output) - - log.title('Resolving 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.mode}] ${folder.name} → ${path.relative(process.cwd(), spritePath)} (${folder.files.length} icons)`) - - results.push({ - name: folder.name, - mode: folder.mode, - spritePath, - iconCount: folder.files.length, - }) - } - - if (react) { - const reactDir = path.resolve(react) - const resolvedPublicPath = publicPath ?? `/${output}` - const iconPath = generateReactModule(results, folders, reactDir, resolvedPublicPath) - log.success(` [react] → ${path.relative(process.cwd(), iconPath)}`) - } - - 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/index.ts b/src/index.ts index f0e4c50..08109f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,25 +1,59 @@ import type { SvgSpritesConfig } from './types.js' +import type { NextSpriteConfig } from './modes/next/types.js' +import type { ReactSpriteConfig } from './modes/react/types.js' -export { generate } from './generate.js' +export { generateLegacy } from './modes/legacy/generate.js' export { resolveSprites, resolveSpriteEntry } from './scanner.js' -export { compileSprite } from './compiler.js' +export { compileSprite, compileSpriteContent } from './compiler.js' +export type { CompileSpriteOptions } from './compiler.js' export { createShapeTransform } from './transforms.js' export { generatePreview } from './preview.js' -export { generateReactModule } from './codegen-react.js' -export { loadConfig } from './config.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, + SpriteAssetTarget, + ViteAssetTarget, + WebpackAssetTarget, +} from './targets/types.js' export type { SvgSpritesConfig, SpriteEntry, SpriteResult, SpriteFolder, - SpriteMode, + SpriteFormat, TransformOptions, } from './types.js' +export type { + NextSpriteConfig, + NextSpriteGenerationOptions, + NextSpriteGenerationResult, +} from './modes/next/types.js' +export type { + ReactSpriteConfig, + ReactSpriteGenerationResult, + ResolvedReactSpriteConfig, +} from './modes/react/types.js' -/** - * Хелпер для типизации конфига с автодополнением. - */ -export function defineConfig(config: SvgSpritesConfig): SvgSpritesConfig { +/** Хелпер для типизации legacy-конфига. */ +export function defineLegacyConfig(config: SvgSpritesConfig): SvgSpritesConfig { + return config +} + +/** Хелпер для типизации локального React-конфига. */ +export function defineReactSpriteConfig(config: ReactSpriteConfig): ReactSpriteConfig { + return config +} + +/** Хелпер для типизации локального Next.js-конфига. */ +export function defineNextSpriteConfig(config: NextSpriteConfig): NextSpriteConfig { return config } diff --git a/src/logger.ts b/src/logger.ts index a59e248..d9c5693 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,4 @@ -import { green, red, yellow, cyan, bold } from 'colorette' +import { green, red, yellow, cyan, bold, dim } from 'colorette' export const log = { success: (msg: string) => console.log(green(msg)), @@ -6,4 +6,5 @@ export const log = { warn: (msg: string) => console.warn(yellow(msg)), info: (msg: string) => console.log(cyan(msg)), title: (msg: string) => console.log(bold(cyan(msg))), + detail: (msg: string) => console.log(dim(msg)), } diff --git a/src/config.ts b/src/modes/legacy/config.ts similarity index 50% rename from src/config.ts rename to src/modes/legacy/config.ts index 9e2d639..4b5a602 100644 --- a/src/config.ts +++ b/src/modes/legacy/config.ts @@ -1,16 +1,12 @@ import fs from 'node:fs' import path from 'node:path' import { createJiti } from 'jiti' -import type { SvgSpritesConfig } from './types.js' +import type { SvgSpritesConfig } from '../../types.js' const CONFIG_NAME = 'svg-sprites.config' -/** - * Загружает конфиг svg-sprites.config.ts из указанной директории. - * - * Использует jiti для импорта TypeScript-файлов. - */ -export async function loadConfig(cwd: string = process.cwd()): Promise { +/** Загружает legacy-конфиг из указанной директории. */ +export async function loadLegacyConfig(cwd: string = process.cwd()): Promise { const configPath = path.join(cwd, `${CONFIG_NAME}.ts`) if (!fs.existsSync(configPath)) { @@ -22,25 +18,30 @@ export async function loadConfig(cwd: string = process.cwd()): Promise ({ + ...sprite, + input: Array.isArray(sprite.input) + ? sprite.input.map((filePath) => path.resolve(cwd, filePath)) + : path.resolve(cwd, sprite.input), + })), + } } -/** - * Валидирует конфиг на наличие обязательных полей. - */ -function validateConfig(config: SvgSpritesConfig): void { +/** Валидирует legacy-конфиг. */ +export function validateLegacyConfig(config: SvgSpritesConfig): void { if (!config.output) { throw new Error('Config: "output" is required.') } @@ -50,6 +51,12 @@ function validateConfig(config: SvgSpritesConfig): void { } 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".') } @@ -58,9 +65,9 @@ function validateConfig(config: SvgSpritesConfig): void { throw new Error(`Config: sprite "${sprite.name}" must have an "input".`) } - if (sprite.mode && sprite.mode !== 'stack' && sprite.mode !== 'symbol') { + if (sprite.format && sprite.format !== 'stack' && sprite.format !== 'symbol') { throw new Error( - `Config: sprite "${sprite.name}" has invalid mode "${sprite.mode}". Supported: stack, symbol.`, + `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 new file mode 100644 index 0000000..7833fed --- /dev/null +++ b/src/modes/legacy/generate.ts @@ -0,0 +1,52 @@ +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 new file mode 100644 index 0000000..7d5a636 --- /dev/null +++ b/src/modes/legacy/index.ts @@ -0,0 +1,2 @@ +export { loadLegacyConfig, validateLegacyConfig } from './config.js' +export { generateLegacy } from './generate.js' diff --git a/src/modes/next/generate.ts b/src/modes/next/generate.ts new file mode 100644 index 0000000..d1422cb --- /dev/null +++ b/src/modes/next/generate.ts @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..f94e9c9 --- /dev/null +++ b/src/modes/next/index.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..2185a6b --- /dev/null +++ b/src/modes/next/types.ts @@ -0,0 +1,22 @@ +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/codegen.ts b/src/modes/react/codegen.ts new file mode 100644 index 0000000..560ffdb --- /dev/null +++ b/src/modes/react/codegen.ts @@ -0,0 +1,288 @@ +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://gromlab.ru/gromov/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 new file mode 100644 index 0000000..2329b3c --- /dev/null +++ b/src/modes/react/config.ts @@ -0,0 +1,108 @@ +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 new file mode 100644 index 0000000..b7aef82 --- /dev/null +++ b/src/modes/react/generate.ts @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..c4e7530 --- /dev/null +++ b/src/modes/react/index.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..0d84eb3 --- /dev/null +++ b/src/modes/react/manifest.ts @@ -0,0 +1,120 @@ +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 new file mode 100644 index 0000000..af0caf4 --- /dev/null +++ b/src/modes/react/module-generator.ts @@ -0,0 +1,78 @@ +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 new file mode 100644 index 0000000..3de2340 --- /dev/null +++ b/src/modes/react/naming.ts @@ -0,0 +1,25 @@ +/** Преобразует 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 new file mode 100644 index 0000000..21c5f64 --- /dev/null +++ b/src/modes/react/types.ts @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..3f028f1 --- /dev/null +++ b/src/modes/react/writer.ts @@ -0,0 +1,172 @@ +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 index c179b8e..1eeeaa8 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -126,7 +126,7 @@ export function generatePreview( return { id, group: r.name, - mode: r.mode, + mode: r.format, spriteFile, viewBox: extractViewBox(fragment), vars: extractIconVars(fragment).map((v) => ({ @@ -138,7 +138,7 @@ export function generatePreview( } }) - return { name: r.name, mode: r.mode, spriteFile, icons } + return { name: r.name, mode: r.format, spriteFile, icons } }) // Inline SVG спрайтов diff --git a/src/react.ts b/src/react.ts new file mode 100644 index 0000000..6d8b8a4 --- /dev/null +++ b/src/react.ts @@ -0,0 +1,13 @@ +'use client' + +export { SpriteViewer } from './react/sprite-viewer.js' +export type { SpriteViewerColorTheme, SpriteViewerProps } from './react/sprite-viewer.js' +export type { + SpriteManifest, + SpriteManifestColor, + SpriteManifestIcon, + SpriteManifestLoader, + SpriteManifestModule, + SpriteViewerSource, + SpriteViewerSources, +} from './react/types.js' diff --git a/src/react/sprite-viewer-code.ts b/src/react/sprite-viewer-code.ts new file mode 100644 index 0000000..0d7d25f --- /dev/null +++ b/src/react/sprite-viewer-code.ts @@ -0,0 +1,196 @@ +import type { SpriteManifest, SpriteManifestIcon } from './types.js' + +export type SpriteViewerTab = 'react' | 'svg' | 'img' | 'css' +export type SpriteViewerCodeLanguage = 'tsx' | 'html' | 'css' + +export const SPRITE_VIEWER_TABS: ReadonlyArray<{ id: SpriteViewerTab; label: string }> = [ + { id: 'react', label: 'React' }, + { id: 'svg', label: 'SVG' }, + { id: 'img', label: 'IMG' }, + { id: 'css', label: 'CSS' }, +] + +export function tabsForFormat(format: SpriteManifest['format']) { + return format === 'stack' + ? SPRITE_VIEWER_TABS + : SPRITE_VIEWER_TABS.filter((tab) => tab.id === 'react' || tab.id === 'svg') +} + +export function viewBoxSize(viewBox: string | null): string | null { + if (!viewBox) return null + const values = viewBox.trim().split(/[\s,]+/).map(Number) + return values.length === 4 && values.every(Number.isFinite) + ? `${values[2]} × ${values[3]}` + : null +} + +export function normalizeHexColor(value: string, currentColor = '#1a1a1a'): string { + const normalized = value.trim().toLowerCase() + if (normalized === 'currentcolor') return normalizeHexColor(currentColor) + + const shortHex = normalized.match(/^#([\da-f])([\da-f])([\da-f])(?:[\da-f])?$/i) + if (shortHex) return `#${shortHex[1]}${shortHex[1]}${shortHex[2]}${shortHex[2]}${shortHex[3]}${shortHex[3]}` + + const longHex = normalized.match(/^#([\da-f]{6})(?:[\da-f]{2})?$/i) + if (longHex) return `#${longHex[1]}` + + const rgb = normalized.match(/^rgba?\(\s*(\d+)\s*[, ]\s*(\d+)\s*[, ]\s*(\d+)/) + if (rgb) { + return `#${rgb.slice(1, 4).map((part) => Math.min(255, Number(part)).toString(16).padStart(2, '0')).join('')}` + } + + const named: Record = { + black: '#000000', + blue: '#0000ff', + cyan: '#00ffff', + gray: '#808080', + green: '#008000', + grey: '#808080', + magenta: '#ff00ff', + orange: '#ffa500', + pink: '#ffc0cb', + purple: '#800080', + red: '#ff0000', + white: '#ffffff', + yellow: '#ffff00', + } + return named[normalized] ?? normalizeHexColor(currentColor === value ? '#1a1a1a' : currentColor) +} + +function styleLines(overrides: Readonly>): string[] { + return Object.entries(overrides).map(([variable, color]) => ` ${JSON.stringify(variable)}: ${JSON.stringify(color)},`) +} + +function htmlAttribute(value: string): string { + const entities: Record = { + '&': '&', + '"': '"', + '<': '<', + '>': '>', + } + return value.replace(/[&"<>]/g, (character) => entities[character]) +} + +function cssUrl(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") +} + +export function generateViewerCode(options: { + manifest: SpriteManifest + icon: SpriteManifestIcon + tab: SpriteViewerTab + colorOverrides: Readonly> + cssColor: string +}): { code: string; language: SpriteViewerCodeLanguage } { + const { manifest, icon, tab, colorOverrides, cssColor } = options + const href = `${manifest.spriteUrl}#${icon.id}` + const overrides = styleLines(colorOverrides) + + if (tab === 'react') { + if (overrides.length === 0) { + return { + code: `<${manifest.componentName} icon=${JSON.stringify(icon.name)} />`, + language: 'tsx', + } + } + return { + code: [ + `<${manifest.componentName}`, + ` icon=${JSON.stringify(icon.name)}`, + ' style={{', + ...overrides, + ' }}', + '/>', + ].join('\n'), + language: 'tsx', + } + } + + if (tab === 'svg') { + const style = Object.entries(colorOverrides) + .map(([variable, color]) => `${variable}: ${color}`) + .join('; ') + return { + code: `\n \n`, + language: 'html', + } + } + + if (tab === 'img') { + return { + code: `${htmlAttribute(icon.name)}`, + language: 'html', + } + } + + const className = `icon-${icon.name.replace(/[^a-zA-Z0-9_-]+/g, '-') || 'sprite'}` + const escapedHref = cssUrl(href) + return { + code: [ + `.${className} {`, + ' width: 24px;', + ' height: 24px;', + ` mask: url('${escapedHref}') no-repeat center / contain;`, + ` -webkit-mask: url('${escapedHref}') no-repeat center / contain;`, + ` background-color: ${cssColor};`, + '}', + ].join('\n'), + language: 'css', + } +} + +const ESCAPE_RE = /[&<>"]/g +const ESCAPE_MAP: Record = { '&': '&', '<': '<', '>': '>', '"': '"' } +type HighlightRule = [RegExp, string] + +const MARKUP_RULES: HighlightRule[] = [ + [//g, 'comment'], + [/<\/?[\w.-]+/g, 'tag'], + [/[\w-]+(?=\s*=)/g, 'attr'], + [/"[^"]*"|'[^']*'/g, 'string'], +] + +const CSS_RULES: HighlightRule[] = [ + [/\/\*[\s\S]*?\*\//g, 'comment'], + [/\.[\w-]+/g, 'selector'], + [/[\w-]+(?=\s*:)/g, 'property'], + [/'[^']*'|"[^"]*"/g, 'string'], + [/#[\da-fA-F]{3,8}\b/g, 'color'], + [/\d+(?:\.\d+)?(?:px|em|rem|%)?/g, 'number'], +] + +export function highlightViewerCode(code: string, language: SpriteViewerCodeLanguage): string { + const rules = language === 'css' ? CSS_RULES : MARKUP_RULES + const tokens: Array<{ kind: string; value: string }> = [] + let position = 0 + + while (position < code.length) { + let match: RegExpExecArray | null = null + let kind = '' + + for (const [rule, ruleKind] of rules) { + rule.lastIndex = position + const candidate = rule.exec(code) + if (candidate?.index === position) { + match = candidate + kind = ruleKind + break + } + } + + if (match) { + tokens.push({ kind, value: match[0] }) + position += match[0].length + } else { + const previous = tokens.at(-1) + if (previous?.kind === '') previous.value += code[position] + else tokens.push({ kind: '', value: code[position] }) + position++ + } + } + + return tokens.map(({ kind, value }) => { + const escaped = value.replace(ESCAPE_RE, (character) => ESCAPE_MAP[character]) + return kind ? `${escaped}` : escaped + }).join('') +} diff --git a/src/react/sprite-viewer-dialog.tsx b/src/react/sprite-viewer-dialog.tsx new file mode 100644 index 0000000..62c6a82 --- /dev/null +++ b/src/react/sprite-viewer-dialog.tsx @@ -0,0 +1,317 @@ +import { HexColorInput, HexColorPicker } from 'react-colorful' +import { useEffect, useId, useRef, useState } from 'react' +import type { CSSProperties } from 'react' +import { + generateViewerCode, + highlightViewerCode, + normalizeHexColor, + tabsForFormat, + viewBoxSize, +} from './sprite-viewer-code.js' +import type { SpriteViewerTab } from './sprite-viewer-code.js' +import type { SpriteManifest, SpriteManifestIcon } from './types.js' + +type SpriteViewerDialogProps = { + manifest: SpriteManifest + icon: SpriteManifestIcon + colorTheme: 'light' | 'dark' + onClose: () => void +} + +type ColorControlProps = { + label: string + value: string + onChange: (value: string) => void +} + +function ColorControl({ label, value, onChange }: ColorControlProps) { + const [open, setOpen] = useState(false) + const swatchRef = useRef(null) + const popoverRef = useRef(null) + const popoverId = useId() + + useEffect(() => { + if (!open) return + + function handlePointerDown(event: PointerEvent) { + const target = event.target as Node + const insideSwatch = swatchRef.current?.contains(target) + const insidePopover = popoverRef.current?.contains(target) + if (!insideSwatch && !insidePopover) setOpen(false) + } + + document.addEventListener('pointerdown', handlePointerDown, true) + return () => document.removeEventListener('pointerdown', handlePointerDown, true) + }, [open]) + + return ( +
+
+ ) +} + +function initialColors(icon: SpriteManifestIcon, currentColor: string) { + return Object.fromEntries(icon.colors.map(({ variable, fallback }) => [ + variable, + normalizeHexColor(fallback, currentColor), + ])) +} + +export function SpriteViewerDialog({ manifest, icon, colorTheme, onClose }: SpriteViewerDialogProps) { + const dialogRef = useRef(null) + const titleId = useId() + const tabsId = useId() + const tabs = tabsForFormat(manifest.format) + const [activeTab, setActiveTab] = useState('react') + const themeColor = colorTheme === 'dark' ? '#e5e5e5' : '#1a1a1a' + const [colors, setColors] = useState>(() => initialColors(icon, themeColor)) + const [colorOverrides, setColorOverrides] = useState>({}) + const [cssColor, setCssColor] = useState(themeColor) + const [cssColorOverridden, setCssColorOverridden] = useState(false) + const [copied, setCopied] = useState(false) + + useEffect(() => { + const dialog = dialogRef.current + if (!dialog) return + + dialog.showModal() + + return () => { + if (dialog.open) dialog.close() + } + }, [icon]) + + useEffect(() => { + setColors({ ...initialColors(icon, themeColor), ...colorOverrides }) + if (!cssColorOverridden) setCssColor(themeColor) + }, [colorTheme, icon]) + + useEffect(() => { + if (!copied) return + const timeout = setTimeout(() => setCopied(false), 1500) + return () => clearTimeout(timeout) + }, [copied]) + + const previewStyle = Object.fromEntries( + Object.entries(colors).map(([variable, color]) => [variable, color]), + ) as CSSProperties + const href = `${manifest.spriteUrl}#${icon.id}` + const dimensions = viewBoxSize(icon.viewBox) + const code = generateViewerCode({ manifest, icon, tab: activeTab, colorOverrides, cssColor }) + + function handleColorChange(variable: string, color: string) { + const normalized = normalizeHexColor(color) + setColors((current) => ({ ...current, [variable]: normalized })) + setColorOverrides((current) => ({ ...current, [variable]: normalized })) + } + + function handleCssColorChange(color: string) { + setCssColor(normalizeHexColor(color)) + setCssColorOverridden(true) + } + + function handleBackdropClick(event: React.MouseEvent) { + if (event.target !== event.currentTarget) return + const bounds = event.currentTarget.getBoundingClientRect() + const outside = event.clientX < bounds.left + || event.clientX > bounds.right + || event.clientY < bounds.top + || event.clientY > bounds.bottom + if (outside) onClose() + } + + function handleTabKeyDown(event: React.KeyboardEvent, tabIndex: number) { + let nextIndex: number | null = null + if (event.key === 'ArrowRight') nextIndex = (tabIndex + 1) % tabs.length + if (event.key === 'ArrowLeft') nextIndex = (tabIndex - 1 + tabs.length) % tabs.length + if (event.key === 'Home') nextIndex = 0 + if (event.key === 'End') nextIndex = tabs.length - 1 + if (nextIndex === null) return + + event.preventDefault() + setActiveTab(tabs[nextIndex].id) + const buttons = event.currentTarget.parentElement?.querySelectorAll('[role="tab"]') + buttons?.[nextIndex]?.focus() + } + + async function copyCode() { + if (!globalThis.navigator?.clipboard) return + try { + await globalThis.navigator.clipboard.writeText(code.code) + setCopied(true) + } catch { + setCopied(false) + } + } + + function renderPreview() { + if (activeTab === 'img') { + return {icon.name} + } + if (activeTab === 'css') { + return ( +
+ ) + } + return ( + + + + ) + } + + return ( + { + event.preventDefault() + onClose() + }} + onClick={handleBackdropClick} + > +
+ + +
+
+ {renderPreview()} +
+
+ +
+

{icon.name}

+ {dimensions && {dimensions}} +
+

+ {manifest.name} · {manifest.format} · {manifest.target} +

+ +
+ {(activeTab === 'react' || activeTab === 'svg') && icon.colors.length > 0 && ( + <> +

+ Цвета применяются к превью через CSS-переменные и попадут в пример кода. +

+

CSS Variables

+ {icon.colors.map(({ variable, fallback }) => ( + handleColorChange(variable, color)} + /> + ))} + + )} + {(activeTab === 'react' || activeTab === 'svg') && icon.colors.length === 0 && ( +

У иконки нет настраиваемых цветовых переменных.

+ )} + {activeTab === 'img' && ( +

+ IMG изолирует SVG: CSS-переменные и currentColor внутрь изображения не передаются. +

+ )} + {activeTab === 'css' && ( + <> +

+ CSS mask отображает иконку одним цветом через background-color. +

+ + + )} +
+ +
+ {tabs.map((tab, tabIndex) => ( + + ))} +
+ +
+
+ +
+
+
+ ) +} diff --git a/src/react/sprite-viewer-styles.ts b/src/react/sprite-viewer-styles.ts new file mode 100644 index 0000000..f51d4af --- /dev/null +++ b/src/react/sprite-viewer-styles.ts @@ -0,0 +1,332 @@ +export const SPRITE_VIEWER_STYLES = ` +.gromlab-sprite-viewer { + --sv-bg: #f0f0f3; + --sv-text: #1a1a1a; + --sv-card: #ffffff; + --sv-card-hover: #eaeaed; + --sv-border: #d8d8d8; + --sv-accent: #3b82f6; + --sv-muted: #777777; + --sv-code: #f5f5f5; + --sv-checker-a: #e9e9e9; + --sv-checker-b: #ffffff; + --sv-danger: #b42332; + box-sizing: border-box; + min-height: 320px; + padding: 24px; + color: var(--sv-text); + color-scheme: light; + background: var(--sv-bg); + border-radius: 12px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} +.gromlab-sprite-viewer[data-theme="dark"] { + --sv-bg: #1a1a1a; + --sv-text: #e5e5e5; + --sv-card: #2a2a2a; + --sv-card-hover: #333333; + --sv-border: #404040; + --sv-muted: #a3a3a3; + --sv-code: #242424; + --sv-checker-a: #333333; + --sv-checker-b: #2a2a2a; + --sv-danger: #ff9ba6; + color-scheme: dark; +} +@media (prefers-color-scheme: dark) { + .gromlab-sprite-viewer:not([data-theme]) { + --sv-bg: #1a1a1a; + --sv-text: #e5e5e5; + --sv-card: #2a2a2a; + --sv-card-hover: #333333; + --sv-border: #404040; + --sv-muted: #a3a3a3; + --sv-code: #242424; + --sv-checker-a: #333333; + --sv-checker-b: #2a2a2a; + --sv-danger: #ff9ba6; + color-scheme: dark; + } +} +.gromlab-sprite-viewer *, +.gromlab-sprite-viewer *::before, +.gromlab-sprite-viewer *::after { box-sizing: border-box; } +.gromlab-sprite-viewer button, +.gromlab-sprite-viewer input { font: inherit; } +.gromlab-sprite-viewer__header { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 24px; +} +.gromlab-sprite-viewer__title { margin: 0; font-size: 24px; line-height: 1.2; font-weight: 700; } +.gromlab-sprite-viewer__summary { color: var(--sv-muted); font-size: 13px; } +.gromlab-sprite-viewer__toolbar { display: flex; align-items: center; gap: 12px; margin-left: auto; } +.gromlab-sprite-viewer__search { + width: 220px; + height: 38px; + padding: 0 12px; + color: var(--sv-text); + background: var(--sv-card); + border: 1px solid var(--sv-border); + border-radius: 8px; + outline: none; + transition: border-color .15s, box-shadow .15s; +} +.gromlab-sprite-viewer__search:focus { border-color: var(--sv-accent); box-shadow: 0 0 0 3px rgba(59, 130, 246, .18); } +.gromlab-sprite-viewer__search::placeholder { color: var(--sv-muted); } +.gromlab-sprite-viewer__theme { + display: grid; + place-items: center; + width: 38px; + height: 38px; + padding: 0; + color: var(--sv-text); + background: var(--sv-card); + border: 1px solid var(--sv-border); + border-radius: 8px; + cursor: pointer; + transition: background .15s, border-color .15s; +} +.gromlab-sprite-viewer__theme:hover { background: var(--sv-card-hover); } +.gromlab-sprite-viewer__theme:focus-visible, +.gromlab-sprite-viewer__card:focus-visible, +.gromlab-sprite-viewer__close:focus-visible, +.gromlab-sprite-viewer__tab:focus-visible, +.gromlab-sprite-viewer__copy:focus-visible, +.gromlab-sprite-viewer__swatch:focus-visible { + outline: 2px solid var(--sv-accent); + outline-offset: 2px; +} +.gromlab-sprite-viewer__errors { + margin: 0 0 24px; + padding: 12px 14px; + color: var(--sv-danger); + background: rgba(180, 35, 50, .08); + border: 1px solid rgba(180, 35, 50, .28); + border-radius: 8px; + font-size: 12px; +} +.gromlab-sprite-viewer__group { margin-bottom: 40px; } +.gromlab-sprite-viewer__group-header { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; } +.gromlab-sprite-viewer__group-title { margin: 0; font-size: 18px; line-height: 1.3; font-weight: 600; } +.gromlab-sprite-viewer__badge { + padding: 2px 8px; + color: #ffffff; + background: var(--sv-accent); + border-radius: 999px; + font-size: 11px; + line-height: 1.5; + font-weight: 600; + letter-spacing: .04em; + text-transform: uppercase; +} +.gromlab-sprite-viewer__group-count, +.gromlab-sprite-viewer__description { color: var(--sv-muted); font-size: 13px; font-weight: 400; } +.gromlab-sprite-viewer__description { flex-basis: 100%; margin: -2px 0 0; } +.gromlab-sprite-viewer__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; } +.gromlab-sprite-viewer__card { + display: flex; + min-width: 0; + padding: 16px 8px; + color: inherit; + text-align: center; + background: var(--sv-card); + border: 0; + border-radius: 8px; + cursor: pointer; + flex-direction: column; + align-items: center; + gap: 8px; + transition: background .15s, transform .15s; +} +.gromlab-sprite-viewer__card:hover { background: var(--sv-card-hover); transform: translateY(-1px); } +.gromlab-sprite-viewer__icon-wrap, +.gromlab-sprite-viewer__dialog-preview-canvas { + display: grid; + place-items: center; + background: conic-gradient( + var(--sv-checker-a) 25%, var(--sv-checker-b) 0 50%, + var(--sv-checker-a) 0 75%, var(--sv-checker-b) 0 + ); + background-size: 8px 8px; + border-radius: 4px; +} +.gromlab-sprite-viewer__icon-wrap { width: 128px; height: 128px; } +.gromlab-sprite-viewer__icon { display: block; width: 128px; height: 128px; color: var(--sv-text); overflow: visible; } +.gromlab-sprite-viewer__icon-name { + max-width: 100%; + overflow: hidden; + color: var(--sv-muted); + font-size: 13px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} +.gromlab-sprite-viewer__status { + padding: 40px 20px; + color: var(--sv-muted); + text-align: center; + background: var(--sv-card); + border: 1px dashed var(--sv-border); + border-radius: 8px; +} +.gromlab-sprite-viewer__footer { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-top: 40px; + padding-top: 16px; + color: var(--sv-muted); + border-top: 1px solid var(--sv-border); + font-size: 13px; +} +.gromlab-sprite-viewer__footer a { color: var(--sv-accent); text-decoration: none; } +.gromlab-sprite-viewer__footer a:hover { text-decoration: underline; } +.gromlab-sprite-viewer__dialog { + width: min(560px, calc(100vw - 32px)); + max-width: none; + max-height: calc(100dvh - 48px); + padding: 0; + color: var(--sv-text); + background: transparent; + border: 0; + overflow: visible; +} +.gromlab-sprite-viewer__dialog::backdrop { background: rgba(0, 0, 0, .55); backdrop-filter: blur(2px); } +.gromlab-sprite-viewer__dialog-shell { + position: relative; + max-height: calc(100dvh - 48px); + padding: 24px; + overflow-y: auto; + color: var(--sv-text); + background: var(--sv-bg); + border: 1px solid var(--sv-border); + border-radius: 12px; + box-shadow: 0 24px 72px rgba(0, 0, 0, .28); +} +.gromlab-sprite-viewer__close { + position: absolute; + z-index: 2; + top: 12px; + right: 12px; + display: grid; + place-items: center; + width: 34px; + height: 34px; + padding: 0; + color: var(--sv-text); + background: var(--sv-bg); + border: 1px solid var(--sv-border); + border-radius: 8px; + cursor: pointer; +} +.gromlab-sprite-viewer__close:hover { background: var(--sv-card-hover); } +.gromlab-sprite-viewer__dialog-preview { display: flex; align-items: center; justify-content: center; padding: 24px; margin-bottom: 16px; background: var(--sv-card); border-radius: 8px; } +.gromlab-sprite-viewer__dialog-preview-canvas { width: 256px; height: 256px; } +.gromlab-sprite-viewer__dialog-icon, +.gromlab-sprite-viewer__dialog-img, +.gromlab-sprite-viewer__dialog-mask { display: block; width: 256px; height: 256px; } +.gromlab-sprite-viewer__dialog-icon { color: var(--sv-text); overflow: visible; } +.gromlab-sprite-viewer__dialog-img { object-fit: contain; } +.gromlab-sprite-viewer__dialog-heading { display: flex; justify-content: center; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; } +.gromlab-sprite-viewer__dialog-title { margin: 0; font: 600 16px/1.3 ui-monospace, "SFMono-Regular", Consolas, monospace; } +.gromlab-sprite-viewer__viewbox { + padding: 2px 8px; + color: var(--sv-muted); + background: var(--sv-card); + border: 1px solid var(--sv-border); + border-radius: 999px; + font-size: 11px; + white-space: nowrap; +} +.gromlab-sprite-viewer__dialog-meta { margin: 0 0 20px; color: var(--sv-muted); text-align: center; font-size: 12px; } +.gromlab-sprite-viewer__colors { margin-bottom: 20px; } +.gromlab-sprite-viewer__hint { + margin: 0 0 12px; + padding: 8px 12px; + color: var(--sv-muted); + background: var(--sv-card); + border-left: 3px solid var(--sv-accent); + border-radius: 4px; + font-size: 12px; + line-height: 1.5; +} +.gromlab-sprite-viewer__colors-title { margin: 0 0 8px; color: var(--sv-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; } +.gromlab-sprite-viewer__color-row { position: relative; display: flex; align-items: center; gap: 8px; min-height: 32px; margin-bottom: 6px; } +.gromlab-sprite-viewer__swatch { width: 26px; height: 26px; flex: 0 0 auto; padding: 0; border: 1px solid var(--sv-border); border-radius: 4px; cursor: pointer; } +.gromlab-sprite-viewer__color-label { min-width: 0; overflow-wrap: anywhere; color: var(--sv-muted); font: 12px/1.4 ui-monospace, "SFMono-Regular", Consolas, monospace; } +.gromlab-sprite-viewer__color-popover { + position: absolute; + z-index: 5; + bottom: calc(100% + 6px); + left: 0; + width: 224px; + max-width: calc(100vw - 64px); + padding: 12px; + background: var(--sv-bg); + border: 1px solid var(--sv-border); + border-radius: 8px; + box-shadow: 0 8px 28px rgba(0, 0, 0, .22); +} +.gromlab-sprite-viewer__color-popover .react-colorful { width: 198px; max-width: 100%; height: 160px; } +.gromlab-sprite-viewer__hex-input { display: block; width: 100%; height: 32px; margin-top: 8px; padding: 0 8px; color: var(--sv-text); text-align: center; background: var(--sv-card); border: 1px solid var(--sv-border); border-radius: 4px; outline: none; font: 12px/1 ui-monospace, "SFMono-Regular", Consolas, monospace; } +.gromlab-sprite-viewer__hex-input:focus { border-color: var(--sv-accent); } +.gromlab-sprite-viewer__tabs { display: flex; overflow-x: auto; margin-bottom: 12px; border-bottom: 1px solid var(--sv-border); } +.gromlab-sprite-viewer__tab { flex: 0 0 auto; padding: 8px 16px; color: var(--sv-muted); background: none; border: 0; border-bottom: 2px solid transparent; cursor: pointer; font-size: 12px; font-weight: 600; } +.gromlab-sprite-viewer__tab[aria-selected="true"] { color: var(--sv-accent); border-bottom-color: var(--sv-accent); } +.gromlab-sprite-viewer__code { position: relative; overflow: hidden; background: var(--sv-code); border-radius: 8px; } +.gromlab-sprite-viewer__code pre { min-height: 72px; margin: 0; padding: 16px 70px 16px 16px; overflow-x: auto; font: 12px/1.6 ui-monospace, "SFMono-Regular", Consolas, monospace; } +.gromlab-sprite-viewer__copy { position: absolute; top: 8px; right: 8px; padding: 4px 8px; color: var(--sv-muted); background: var(--sv-bg); border: 1px solid var(--sv-border); border-radius: 4px; cursor: pointer; font-size: 11px; } +.gromlab-sprite-viewer__copy:hover { color: var(--sv-text); } +.gromlab-sprite-viewer__code .hl-tag { color: #116329; } +.gromlab-sprite-viewer__code .hl-attr, +.gromlab-sprite-viewer__code .hl-number, +.gromlab-sprite-viewer__code .hl-property { color: #0550ae; } +.gromlab-sprite-viewer__code .hl-string, +.gromlab-sprite-viewer__code .hl-color { color: #0a3069; } +.gromlab-sprite-viewer__code .hl-comment { color: #8b949e; } +.gromlab-sprite-viewer__code .hl-punctuation, +.gromlab-sprite-viewer__code .hl-selector { color: #6639ba; } +.gromlab-sprite-viewer[data-theme="dark"] .gromlab-sprite-viewer__code .hl-tag { color: #7ee787; } +.gromlab-sprite-viewer[data-theme="dark"] .gromlab-sprite-viewer__code .hl-attr, +.gromlab-sprite-viewer[data-theme="dark"] .gromlab-sprite-viewer__code .hl-number, +.gromlab-sprite-viewer[data-theme="dark"] .gromlab-sprite-viewer__code .hl-property { color: #79c0ff; } +.gromlab-sprite-viewer[data-theme="dark"] .gromlab-sprite-viewer__code .hl-string, +.gromlab-sprite-viewer[data-theme="dark"] .gromlab-sprite-viewer__code .hl-color { color: #a5d6ff; } +.gromlab-sprite-viewer[data-theme="dark"] .gromlab-sprite-viewer__code .hl-punctuation, +.gromlab-sprite-viewer[data-theme="dark"] .gromlab-sprite-viewer__code .hl-selector { color: #d2a8ff; } +@media (prefers-color-scheme: dark) { + .gromlab-sprite-viewer:not([data-theme]) .gromlab-sprite-viewer__code .hl-tag { color: #7ee787; } + .gromlab-sprite-viewer:not([data-theme]) .gromlab-sprite-viewer__code .hl-attr, + .gromlab-sprite-viewer:not([data-theme]) .gromlab-sprite-viewer__code .hl-number, + .gromlab-sprite-viewer:not([data-theme]) .gromlab-sprite-viewer__code .hl-property { color: #79c0ff; } + .gromlab-sprite-viewer:not([data-theme]) .gromlab-sprite-viewer__code .hl-string, + .gromlab-sprite-viewer:not([data-theme]) .gromlab-sprite-viewer__code .hl-color { color: #a5d6ff; } + .gromlab-sprite-viewer:not([data-theme]) .gromlab-sprite-viewer__code .hl-punctuation, + .gromlab-sprite-viewer:not([data-theme]) .gromlab-sprite-viewer__code .hl-selector { color: #d2a8ff; } +} +@media (max-width: 640px) { + .gromlab-sprite-viewer { padding: 16px; border-radius: 8px; } + .gromlab-sprite-viewer__header { align-items: stretch; } + .gromlab-sprite-viewer__toolbar { width: 100%; margin-left: 0; } + .gromlab-sprite-viewer__search { width: auto; flex: 1; min-width: 0; } + .gromlab-sprite-viewer__grid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 8px; } + .gromlab-sprite-viewer__icon-wrap, + .gromlab-sprite-viewer__icon { width: 104px; height: 104px; } + .gromlab-sprite-viewer__dialog { width: calc(100vw - 20px); max-height: calc(100dvh - 20px); } + .gromlab-sprite-viewer__dialog-shell { max-height: calc(100dvh - 20px); padding: 16px; } + .gromlab-sprite-viewer__dialog-preview { padding: 16px; } + .gromlab-sprite-viewer__dialog-preview-canvas, + .gromlab-sprite-viewer__dialog-icon, + .gromlab-sprite-viewer__dialog-img, + .gromlab-sprite-viewer__dialog-mask { width: min(256px, calc(100vw - 86px)); height: min(256px, calc(100vw - 86px)); } +} +@media (prefers-reduced-motion: reduce) { + .gromlab-sprite-viewer *, + .gromlab-sprite-viewer *::before, + .gromlab-sprite-viewer *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; } +} +` diff --git a/src/react/sprite-viewer.tsx b/src/react/sprite-viewer.tsx new file mode 100644 index 0000000..9518636 --- /dev/null +++ b/src/react/sprite-viewer.tsx @@ -0,0 +1,301 @@ +'use client' + +import { useDeferredValue, useEffect, useState } from 'react' +import type { CSSProperties, ReactElement } from 'react' +import { SpriteViewerDialog } from './sprite-viewer-dialog.js' +import { SPRITE_VIEWER_STYLES } from './sprite-viewer-styles.js' +import type { + SpriteManifest, + SpriteManifestIcon, + SpriteManifestLoader, + SpriteManifestModule, + SpriteViewerSource, + SpriteViewerSources, +} from './types.js' + +export type SpriteViewerColorTheme = 'auto' | 'light' | 'dark' + +export type SpriteViewerProps = { + sources: SpriteViewerSources + title?: string + /** Тема Viewer. По умолчанию следует prefers-color-scheme. */ + colorTheme?: SpriteViewerColorTheme + /** Вызывается встроенным переключателем в controlled-режиме. */ + onColorThemeChange?: (theme: SpriteViewerColorTheme) => void + className?: string + style?: CSSProperties +} + +type SelectedIcon = { + key: string + manifest: SpriteManifest + icon: SpriteManifestIcon +} + +const manifestLoaderCache = new WeakMap>() + +function compareManifests(left: SpriteManifest, right: SpriteManifest): number { + return left.name < right.name ? -1 : left.name > right.name ? 1 : 0 +} + +function sourceArray(sources: SpriteViewerSources): readonly SpriteViewerSource[] { + return Array.isArray(sources) + ? sources + : Object.values(sources as Readonly>) +} + +function isSpriteManifest(value: unknown): value is SpriteManifest { + if (!value || typeof value !== 'object') return false + const manifest = value as Partial + return manifest.schemaVersion === 1 + && manifest.generator === '@gromlab/svg-sprites' + && typeof manifest.name === 'string' + && typeof manifest.componentName === 'string' + && typeof manifest.spriteUrl === 'string' + && Array.isArray(manifest.icons) +} + +function manifestFromModule(value: SpriteManifest | SpriteManifestModule): SpriteManifest { + if (isSpriteManifest(value)) return value + if (isSpriteManifest(value.default)) return value.default + if (isSpriteManifest(value.spriteManifest)) return value.spriteManifest + throw new Error('The loaded module does not export a valid SVG sprite manifest.') +} + +async function resolveSource(source: SpriteViewerSource): Promise { + if (typeof source !== 'function') return source + + const cached = manifestLoaderCache.get(source) + if (cached) return cached + + const pending = Promise.resolve().then(source).then(manifestFromModule) + manifestLoaderCache.set(source, pending) + void pending.catch(() => { + if (manifestLoaderCache.get(source) === pending) manifestLoaderCache.delete(source) + }) + return pending +} + +function directManifests(sources: SpriteViewerSources): SpriteManifest[] { + return sourceArray(sources) + .filter((source): source is SpriteManifest => typeof source !== 'function') + .sort(compareManifests) +} + +function countLabel(value: number, forms: readonly [string, string, string]): string { + const modulo100 = value % 100 + const modulo10 = value % 10 + const form = modulo100 >= 11 && modulo100 <= 19 + ? forms[2] + : modulo10 === 1 + ? forms[0] + : modulo10 >= 2 && modulo10 <= 4 + ? forms[1] + : forms[2] + return `${value} ${form}` +} + +function currentSystemTheme(): 'light' | 'dark' { + return globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +function useSystemTheme(): 'light' | 'dark' { + const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(currentSystemTheme) + + useEffect(() => { + const media = globalThis.matchMedia?.('(prefers-color-scheme: dark)') + if (!media) return + + const update = () => setSystemTheme(media.matches ? 'dark' : 'light') + update() + media.addEventListener('change', update) + return () => media.removeEventListener('change', update) + }, []) + + return systemTheme +} + +/** Интерактивный каталог локальных React- и Next.js-спрайтов. */ +export function SpriteViewer({ + sources, + title = 'SVG Sprites', + colorTheme, + onColorThemeChange, + className, + style, +}: SpriteViewerProps): ReactElement { + const [manifests, setManifests] = useState(() => directManifests(sources)) + const [errors, setErrors] = useState([]) + const [loading, setLoading] = useState(() => sourceArray(sources).some((source) => typeof source === 'function')) + const [query, setQuery] = useState('') + const [localColorTheme, setLocalColorTheme] = useState('auto') + const [selected, setSelected] = useState(null) + const systemTheme = useSystemTheme() + const activeColorTheme = colorTheme ?? localColorTheme + const resolvedColorTheme = activeColorTheme === 'auto' ? systemTheme : activeColorTheme + const canToggleTheme = colorTheme === undefined || onColorThemeChange !== undefined + const deferredQuery = useDeferredValue(query.trim().toLowerCase()) + + useEffect(() => { + let active = true + const allSources = sourceArray(sources) + const direct = directManifests(sources) + const loaders = allSources.filter((source) => typeof source === 'function') + + setManifests(direct) + setErrors([]) + setLoading(loaders.length > 0) + setSelected(null) + + if (loaders.length === 0) return () => { active = false } + + Promise.allSettled(loaders.map(resolveSource)).then((results) => { + if (!active) return + + const loaded: SpriteManifest[] = [] + const failures: string[] = [] + + for (const result of results) { + if (result.status === 'fulfilled') loaded.push(result.value) + else failures.push(result.reason instanceof Error ? result.reason.message : String(result.reason)) + } + + setManifests([...direct, ...loaded].sort(compareManifests)) + setErrors(failures) + setLoading(false) + }) + + return () => { active = false } + }, [sources]) + + const visibleGroups = manifests + .map((manifest) => ({ + manifest, + icons: manifest.icons.filter((icon) => ( + deferredQuery === '' + || icon.name.toLowerCase().includes(deferredQuery) + || manifest.name.toLowerCase().includes(deferredQuery) + )), + })) + .filter((group) => group.icons.length > 0) + + const totalIcons = manifests.reduce((total, manifest) => total + manifest.iconCount, 0) + const visibleIcons = visibleGroups.reduce((total, group) => total + group.icons.length, 0) + const rootClassName = ['gromlab-sprite-viewer', className].filter(Boolean).join(' ') + + function toggleTheme() { + const nextTheme = resolvedColorTheme === 'dark' ? 'light' : 'dark' + if (colorTheme === undefined) setLocalColorTheme(nextTheme) + onColorThemeChange?.(nextTheme) + } + + return ( +
+ +
+

{title}

+ + {countLabel(manifests.length, ['спрайт', 'спрайта', 'спрайтов'])} + {' · '} + {countLabel(totalIcons, ['иконка', 'иконки', 'иконок'])} + {deferredQuery && ` · найдено ${visibleIcons}`} + +
+ setQuery(event.currentTarget.value)} + placeholder="Найти иконку" + aria-label="Поиск иконок" + /> + {canToggleTheme && ( + + )} +
+
+ + {errors.length > 0 && ( +
+ {errors.map((error, index) =>
{error}
)} +
+ )} + + {visibleGroups.map(({ manifest, icons }) => ( +
+
+

{manifest.name}

+ {manifest.format} + {icons.length} + {manifest.description && ( +

{manifest.description}

+ )} +
+
+ {icons.map((icon) => { + const key = `${manifest.name}:${manifest.spriteUrl}:${icon.id}` + return ( + + ) + })} +
+
+ ))} + + {visibleGroups.length === 0 && (!loading || manifests.length > 0) && ( +
+ {manifests.length === 0 ? 'Спрайты не подключены' : 'Иконки не найдены'} +
+ )} + {loading && manifests.length === 0 && ( +
Загрузка спрайтов...
+ )} + + + + {selected && ( + setSelected(null)} + /> + )} +
+ ) +} diff --git a/src/react/types.ts b/src/react/types.ts new file mode 100644 index 0000000..23afcdb --- /dev/null +++ b/src/react/types.ts @@ -0,0 +1,40 @@ +import type { SpriteAssetTarget } from '../targets/types.js' + +export type SpriteManifestColor = { + variable: `--icon-color-${number}` + fallback: string +} + +export type SpriteManifestIcon = { + name: string + id: string + viewBox: string | null + colors: readonly SpriteManifestColor[] +} + +/** Публичные данные одного сгенерированного React-спрайта. */ +export type SpriteManifest = { + schemaVersion: 1 + generator: '@gromlab/svg-sprites' + name: string + description?: string + componentName: string + target: SpriteAssetTarget + format: 'stack' | 'symbol' + iconCount: number + spriteUrl: string + icons: readonly SpriteManifestIcon[] +} + +export type SpriteManifestModule = { + default?: SpriteManifest + spriteManifest?: SpriteManifest +} + +export type SpriteManifestLoader = () => Promise +export type SpriteViewerSource = SpriteManifest | SpriteManifestLoader + +/** Массив источников либо результат import.meta.glob. */ +export type SpriteViewerSources = + | readonly SpriteViewerSource[] + | Readonly> diff --git a/src/scanner.ts b/src/scanner.ts index 8ee5f7f..bd94908 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -37,11 +37,36 @@ function resolveFiles(files: string[]): string[] { }) } +type ResolveSpriteSourcesOptions = { + name: string + format: SpriteFolder['format'] + inputFolder: string | null + inputFiles: string[] +} + +/** Объединяет SVG из папки и явного списка в один источник спрайта. */ +export function resolveSpriteSources(options: ResolveSpriteSourcesOptions): SpriteFolder { + const { name, format, inputFolder, inputFiles } = options + const folderFiles = inputFolder === null ? [] : scanDirectory(inputFolder) + const files = [...new Set([...folderFiles, ...resolveFiles(inputFiles)])] + + if (files.length === 0) { + throw new Error(`Sprite "${name}" has no SVG files in configured inputs.`) + } + + return { + name, + format, + path: inputFolder, + files, + } +} + /** * Преобразует SpriteEntry из конфига в SpriteFolder для компиляции. */ export function resolveSpriteEntry(entry: SpriteEntry): SpriteFolder { - const mode = entry.mode ?? 'stack' + const format = entry.format ?? 'stack' if (Array.isArray(entry.input)) { const files = resolveFiles(entry.input) @@ -52,7 +77,7 @@ export function resolveSpriteEntry(entry: SpriteEntry): SpriteFolder { return { name: entry.name, - mode, + format, path: null, files, } @@ -67,7 +92,7 @@ export function resolveSpriteEntry(entry: SpriteEntry): SpriteFolder { return { name: entry.name, - mode, + format, path: dirPath, files, } diff --git a/src/shape-id.ts b/src/shape-id.ts new file mode 100644 index 0000000..d9db646 --- /dev/null +++ b/src/shape-id.ts @@ -0,0 +1,14 @@ +import { createHash } from 'node:crypto' +import path from 'node:path' + +/** Возвращает стабильный и безопасный fragment ID для исходного SVG-файла. */ +export function getSpriteShapeId(filePath: string): string { + const iconName = path.basename(filePath, '.svg') + + if (/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(iconName)) { + return iconName + } + + const hash = createHash('sha256').update(iconName).digest('hex').slice(0, 16) + return `icon-${hash}` +} diff --git a/src/targets/index.ts b/src/targets/index.ts new file mode 100644 index 0000000..bafff21 --- /dev/null +++ b/src/targets/index.ts @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..77492a4 --- /dev/null +++ b/src/targets/types.ts @@ -0,0 +1,45 @@ +/** + * Asset target для Vite. + * + * Генерирует статический импорт SVG с query `?no-inline`, чтобы Vite всегда + * выпускал спрайт отдельным кешируемым файлом и не преобразовывал его в data URL. + */ +export type ViteAssetTarget = 'vite' + +/** + * Asset target для Webpack 5. + * + * Генерирует статический `new URL(..., import.meta.url)`. Webpack обрабатывает + * такую ссылку через Asset Modules и выпускает SVG отдельным файлом. + */ +export type WebpackAssetTarget = 'webpack' + +/** Роутер Next.js, для которого генерируется sprite-модуль. */ +export type NextRouter = 'app' | 'pages' + +/** Сборщик Next.js, обрабатывающий SVG asset. */ +export type NextBundler = 'turbopack' | 'webpack' + +/** Полный ключ Next.js target, используемый CLI и manifest. */ +export type NextAssetTarget = `next@${NextRouter}/${NextBundler}` + +/** + * Среда сборки React sprite-модуля. + * + * Target определяет только способ получения публичного URL SVG asset. + * Состав иконок, React API и структура generated-файлов от него не зависят. + */ +export type ReactAssetTarget = ViteAssetTarget | WebpackAssetTarget + +/** Любая среда, для которой может быть сгенерирован React sprite-модуль. */ +export type SpriteAssetTarget = ReactAssetTarget | NextAssetTarget + +/** Фрагменты кода, необходимые компоненту для получения URL SVG asset. */ +export type SpriteAssetUrlCode = { + /** Импорты, которые добавляются в начало generated-компонента. */ + imports: string[] + /** Объявления, которые добавляются после импортов. */ + declarations: string[] + /** Имя переменной, содержащей итоговый публичный URL SVG. */ + variableName: string +} diff --git a/src/targets/vite.ts b/src/targets/vite.ts new file mode 100644 index 0000000..1d8da1f --- /dev/null +++ b/src/targets/vite.ts @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..c06a502 --- /dev/null +++ b/src/targets/webpack.ts @@ -0,0 +1,12 @@ +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 629d49f..335e7db 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ -/** Режим спрайта: stack или symbol. */ -export type SpriteMode = 'stack' | 'symbol' +/** Формат спрайта: stack или symbol. */ +export type SpriteFormat = 'stack' | 'symbol' /** Описание одного спрайта в конфиге. */ export type SpriteEntry = { @@ -12,10 +12,10 @@ export type SpriteEntry = { */ input: string | string[] /** - * Режим спрайта. + * Формат спрайта. * По умолчанию: 'stack'. */ - mode?: SpriteMode + format?: SpriteFormat } /** Параметры трансформации SVG. Все включены по умолчанию. */ @@ -43,22 +43,11 @@ export type TransformOptions = { export type SvgSpritesConfig = { /** Путь к папке для сгенерированных SVG-спрайтов. */ output: string - /** - * Публичный путь к спрайтам для использования в коде (href, src, url()). - * Используется в сгенерированном React-компоненте. - * Пример: '/img/sprites'. - */ - publicPath?: string /** * Генерировать HTML-превью со всеми иконками. * По умолчанию: true. */ preview?: boolean - /** - * Путь для генерации React-компонента. - * Если не задан — компонент и типы не генерируются. - */ - react?: string /** * Настройки трансформации SVG. * По умолчанию: все трансформации включены. @@ -72,8 +61,8 @@ export type SvgSpritesConfig = { export type SpriteFolder = { /** Имя спрайта. */ name: string - /** Режим спрайта. */ - mode: SpriteMode + /** Формат спрайта. */ + format: SpriteFormat /** Абсолютный путь к папке (для input-папки) или null (для input-массива). */ path: string | null /** Абсолютные пути к SVG-файлам. */ @@ -84,8 +73,8 @@ export type SpriteFolder = { export type SpriteResult = { /** Имя спрайта. */ name: string - /** Режим спрайта. */ - mode: SpriteMode + /** Формат спрайта. */ + format: SpriteFormat /** Путь к сгенерированному SVG-спрайту. */ spritePath: string /** Количество иконок в спрайте. */ diff --git a/svg-sprites.config.ts b/svg-sprites.config.ts index 1d45c01..a21098c 100644 --- a/svg-sprites.config.ts +++ b/svg-sprites.config.ts @@ -1,21 +1,19 @@ -import { defineConfig } from './src/index.js' +import { defineLegacyConfig } from './src/index.js' -export default defineConfig({ +export default defineLegacyConfig({ output: 'preview/public', - publicPath: '', preview: true, - react: 'test/ui/svg-sprite', sprites: [ { name: 'icons', input: 'test/assets/icons', - mode: 'stack', + format: 'stack', }, { name: 'logos', input: 'test/assets/logos', - mode: 'stack', + format: 'stack', }, ], }) diff --git a/test/asset-targets.test.mjs b/test/asset-targets.test.mjs new file mode 100644 index 0000000..5ae7ebc --- /dev/null +++ b/test/asset-targets.test.mjs @@ -0,0 +1,117 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import test from 'node:test' +import { build as viteBuild } from 'vite' +import webpack from 'webpack' + +const TINY_SPRITE = [ + '', + '', + '', + '', + '', + '', +].join('') + +function getOutputFiles(directory, extension) { + const files = [] + + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name) + + if (entry.isDirectory()) { + files.push(...getOutputFiles(entryPath, extension).map((filePath) => path.join(entry.name, filePath))) + } else if (entry.name.endsWith(extension)) { + files.push(entry.name) + } + } + + return files +} + +test('Vite target keeps a tiny sprite as a separate asset', async () => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-vite-target-')) + const outputDir = path.join(rootDir, 'dist') + + fs.writeFileSync( + path.join(rootDir, 'index.html'), + '
', + ) + fs.writeFileSync( + path.join(rootDir, 'main.js'), + [ + "import spriteUrl from './sprite.svg?no-inline'", + "document.querySelector('#root').dataset.spriteUrl = spriteUrl", + '', + ].join('\n'), + ) + fs.writeFileSync(path.join(rootDir, 'sprite.svg'), TINY_SPRITE) + + await viteBuild({ + root: rootDir, + logLevel: 'silent', + build: { + outDir: outputDir, + }, + }) + + const svgFiles = getOutputFiles(outputDir, '.svg') + const jsFiles = getOutputFiles(outputDir, '.js') + const javascript = jsFiles + .map((filePath) => fs.readFileSync(path.join(outputDir, filePath), 'utf-8')) + .join('\n') + + assert.equal(svgFiles.length, 1) + assert.match(svgFiles[0], /sprite-[A-Za-z0-9_-]+\.svg$/) + assert.doesNotMatch(javascript, /data:image\/svg\+xml/) +}) + +test('Webpack target emits new URL sprite through Asset Modules', async () => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-webpack-target-')) + const outputDir = path.join(rootDir, 'dist') + + fs.writeFileSync( + path.join(rootDir, 'index.js'), + [ + "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", + 'globalThis.__SPRITE_URL__ = spriteUrl', + '', + ].join('\n'), + ) + fs.writeFileSync(path.join(rootDir, 'sprite.svg'), TINY_SPRITE) + + await new Promise((resolve, reject) => { + webpack({ + context: rootDir, + entry: './index.js', + mode: 'production', + output: { + assetModuleFilename: 'assets/[name]-[contenthash][ext]', + filename: 'bundle.js', + path: outputDir, + }, + }, (error, stats) => { + if (error) { + reject(error) + return + } + + if (!stats || stats.hasErrors()) { + const errors = stats?.toJson({ all: false, errors: true }).errors ?? [] + reject(new Error(errors.map((item) => item.message).join('\n'))) + return + } + + resolve() + }) + }) + + const svgFiles = getOutputFiles(outputDir, '.svg') + const javascript = fs.readFileSync(path.join(outputDir, 'bundle.js'), 'utf-8') + + assert.equal(svgFiles.length, 1) + assert.match(svgFiles[0], /sprite-[a-f0-9]+\.svg$/) + assert.doesNotMatch(javascript, /data:image\/svg\+xml/) +}) diff --git a/test/next-targets.test.mjs b/test/next-targets.test.mjs new file mode 100644 index 0000000..71c47e9 --- /dev/null +++ b/test/next-targets.test.mjs @@ -0,0 +1,204 @@ +import assert from 'node:assert/strict' +import { spawn, spawnSync } from 'node:child_process' +import { once } from 'node:events' +import fs from 'node:fs' +import net from 'node:net' +import path from 'node:path' +import test from 'node:test' +import { setTimeout as delay } from 'node:timers/promises' + +import { generateNextSprite } from '../dist/index.js' + +const NEXT_BIN = path.resolve('node_modules/next/dist/bin/next') + +function writeFile(filePath, content) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + fs.writeFileSync(filePath, content) +} + +function createNextFixture() { + const rootDir = fs.mkdtempSync(path.join(path.resolve('test'), '.next-fixture-')) + + writeFile( + path.join(rootDir, 'package.json'), + JSON.stringify({ name: 'svg-sprites-next-fixture', private: true }, null, 2), + ) + writeFile( + path.join(rootDir, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + jsx: 'preserve', + module: 'esnext', + moduleResolution: 'bundler', + strict: true, + skipLibCheck: true, + }, + }, null, 2), + ) + writeFile( + path.join(rootDir, 'next-env.d.ts'), + '/// \n/// \n', + ) + writeFile( + path.join(rootDir, 'app', 'layout.tsx'), + `export default function Layout({ children }: { children: React.ReactNode }) { + return {children} +} +`, + ) + writeFile( + path.join(rootDir, 'app', 'page.tsx'), + `import { AppIcon } from '../sprites/app' + +export default function Page() { + return
+} +`, + ) + writeFile( + path.join(rootDir, 'pages', 'legacy.tsx'), + `import { PagesIcon } from '../sprites/pages' + +export default function LegacyPage() { + return
+} + +export function getServerSideProps() { + return { props: {} } +} +`, + ) + + for (const name of ['app', 'pages']) { + const spriteRoot = path.join(rootDir, 'sprites', name) + writeFile( + path.join(spriteRoot, 'svg-sprite.config.ts'), + `export default { name: '${name}', generatedNotice: false }\n`, + ) + writeFile( + path.join(spriteRoot, 'icons', 'check.svg'), + '', + ) + } + + return rootDir +} + +function runNext(rootDir, args) { + const result = spawnSync(process.execPath, [NEXT_BIN, ...args], { + cwd: rootDir, + encoding: 'utf-8', + env: { + ...process.env, + NEXT_TELEMETRY_DISABLED: '1', + }, + maxBuffer: 10 * 1024 * 1024, + timeout: 120_000, + }) + + assert.equal( + result.status, + 0, + [result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n'), + ) +} + +async function getFreePort() { + const server = net.createServer() + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + assert.ok(address && typeof address === 'object') + const { port } = address + server.close() + await once(server, 'close') + return port +} + +async function startNext(rootDir) { + const port = await getFreePort() + const child = spawn(process.execPath, [NEXT_BIN, 'start', '--hostname', '127.0.0.1', '--port', String(port)], { + cwd: rootDir, + env: { + ...process.env, + NEXT_TELEMETRY_DISABLED: '1', + }, + stdio: ['ignore', 'pipe', 'pipe'], + }) + let output = '' + child.stdout.on('data', (chunk) => { output += chunk }) + child.stderr.on('data', (chunk) => { output += chunk }) + const origin = `http://127.0.0.1:${port}` + + for (let attempt = 0; attempt < 100; attempt++) { + if (child.exitCode !== null) { + throw new Error(`Next.js exited before startup.\n${output}`) + } + + try { + const response = await fetch(origin) + if (response.ok) return { child, origin } + } catch { + // Server is still starting. + } + + await delay(100) + } + + child.kill('SIGKILL') + throw new Error(`Next.js did not start in time.\n${output}`) +} + +async function stopNext(child) { + if (child.exitCode !== null) return + child.kill('SIGTERM') + + await Promise.race([ + once(child, 'exit'), + delay(5_000).then(() => child.kill('SIGKILL')), + ]) +} + +async function assertRenderedSprite(origin, route) { + const response = await fetch(`${origin}${route}`) + assert.equal(response.status, 200) + const html = await response.text() + const href = html.match(/href="([^"]+\.svg)#check"/)?.[1] + + assert.ok(href, `Sprite href not found in ${route}: ${html}`) + assert.doesNotMatch(href, /^(?:data|file|blob):/) + + const spriteResponse = await fetch(new URL(href, origin)) + assert.equal(spriteResponse.status, 200) + assert.match(spriteResponse.headers.get('content-type') ?? '', /image\/svg\+xml/) + assert.match(await spriteResponse.text(), /id="check"/) +} + +test('Next 16.2 renders App and Pages sprites with Turbopack and Webpack', { timeout: 300_000 }, async () => { + const rootDir = createNextFixture() + + try { + for (const bundler of ['turbopack', 'webpack']) { + await generateNextSprite(path.join(rootDir, 'sprites', 'app'), { + router: 'app', + bundler, + }) + await generateNextSprite(path.join(rootDir, 'sprites', 'pages'), { + router: 'pages', + bundler, + }) + + runNext(rootDir, ['build', `--${bundler}`]) + const { child, origin } = await startNext(rootDir) + + try { + await assertRenderedSprite(origin, '/') + await assertRenderedSprite(origin, '/legacy') + } finally { + await stopNext(child) + } + } + } finally { + fs.rmSync(rootDir, { recursive: true, force: true }) + } +}) diff --git a/test/public-api.test.mjs b/test/public-api.test.mjs new file mode 100644 index 0000000..9f891cf --- /dev/null +++ b/test/public-api.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + generateNextSprite, + generateReactSprite, +} from '../dist/index.js' + +test('generateReactSprite rejects unsupported targets', () => { + assert.throws( + () => generateReactSprite('.', 'unknown'), + /Unsupported React asset target: unknown/, + ) +}) + +test('generateNextSprite rejects unsupported routers', async () => { + await assert.rejects( + generateNextSprite('.', { router: 'unknown', bundler: 'webpack' }), + /Unsupported Next\.js router: unknown/, + ) +}) + +test('generateNextSprite rejects unsupported bundlers', async () => { + await assert.rejects( + generateNextSprite('.', { router: 'app', bundler: 'unknown' }), + /Unsupported Next\.js bundler: unknown/, + ) +}) diff --git a/test/react-entry.test.mjs b/test/react-entry.test.mjs deleted file mode 100644 index 251cd89..0000000 --- a/test/react-entry.test.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import assert from 'node:assert/strict' -import fs from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import test from 'node:test' - -import { generate } from '../dist/index.js' - -test('generates a React directory entry-point', async () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-entry-')) - const inputDir = path.join(root, 'icons') - const outputDir = path.join(root, 'public', 'sprites') - const reactDir = path.join(root, 'src', 'shared', 'ui', 'svg-sprite') - - fs.mkdirSync(inputDir, { recursive: true }) - fs.writeFileSync( - path.join(inputDir, 'check.svg'), - '', - ) - - await generate({ - output: outputDir, - publicPath: '/sprites', - preview: false, - react: reactDir, - sprites: [ - { - name: 'icons', - input: inputDir, - }, - ], - }) - - const indexPath = path.join(reactDir, 'index.ts') - const index = fs.readFileSync(indexPath, 'utf-8') - - assert.ok(fs.existsSync(indexPath)) - assert.match(index, /export \{ SvgSprite \} from '\.\/svg-sprite'/) - assert.match(index, /SvgSpriteProps/) -}) diff --git a/test/react-mode.test.mjs b/test/react-mode.test.mjs new file mode 100644 index 0000000..e83a3d9 --- /dev/null +++ b/test/react-mode.test.mjs @@ -0,0 +1,590 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import test from 'node:test' +import ts from 'typescript' + +import { generateNextSprite, generateReactSprite } from '../dist/index.js' + +const GENERATED_FILES = [ + '.svg-sprites.manifest.json', + 'react-component.tsx', + 'sprite.svg', + 'styles.module.css', + 'types.ts', +] + +function createReactFixture() { + const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-')) + const rootDir = path.join(temporaryDir, 'src', 'widgets', 'file-manager', 'svg-sprite') + const iconsDir = path.join(rootDir, 'icons') + + fs.mkdirSync(iconsDir, { recursive: true }) + fs.writeFileSync( + path.join(rootDir, 'svg-sprite.config.ts'), + `export default { description: 'File manager icons' }\n`, + ) + fs.writeFileSync( + path.join(iconsDir, 'check.svg'), + '', + ) + fs.writeFileSync( + path.join(iconsDir, 'folder.svg'), + '', + ) + fs.writeFileSync( + path.join(iconsDir, 'folder open.svg'), + '', + ) + + return { rootDir, iconsDir } +} + +test('generates an isolated React sprite module', async () => { + const { rootDir } = createReactFixture() + const result = await generateReactSprite(rootDir, 'vite') + const generatedDir = path.join(rootDir, 'generated') + + assert.equal(result.name, 'file-manager') + assert.equal(result.iconCount, 3) + assert.equal(result.target, 'vite') + assert.equal(result.manifestPath, path.join(rootDir, 'manifest.ts')) + assert.deepEqual(fs.readdirSync(generatedDir).sort(), GENERATED_FILES) + + const gitignore = fs.readFileSync(path.join(rootDir, '.gitignore'), 'utf-8') + assert.match(gitignore, /@generated by @gromlab\/svg-sprites/) + assert.match(gitignore, /^\/generated\/$/m) + assert.match(gitignore, /^\/index\.ts$/m) + assert.match(gitignore, /^\/manifest\.ts$/m) + + const sprite = fs.readFileSync(path.join(generatedDir, 'sprite.svg'), 'utf-8') + assert.match(sprite, /АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ/) + assert.match(sprite, /Генератор: @gromlab\/svg-sprites/) + assert.match(sprite, /]+id="check"/) + assert.match(sprite, /]+id="folder"/) + assert.match(sprite, /]*>\n