feat(skills): добавить установку через npx skills add

This commit is contained in:
Gromov Sergei
2026-08-17 09:43:40 +03:00
parent d1f122c23e
commit 19d9cf01e7
87 changed files with 9864 additions and 38 deletions
+34
View File
@@ -0,0 +1,34 @@
# AI skills
Исходники обязательного контекста английского и русского skills находятся в `src/skills/svg-sprites/src/{en,ru}/`. Готовые переносимые артефакты генерируются в корневой каталог `skills/`, версионируются для установки через `npx skills add` и упаковываются в ZIP во время release workflow.
Обе языковые версии имеют симметричную single-file структуру:
```text
src/{en,ru}/
├── SKILL.md
└── references/
└── complex-svg.md
```
Каждый `SKILL.md` содержит обязательные знания о пакете, рабочий процесс агента и operational map canonical-документации. Exact-mode настройка берётся из canonical guides, а не дублируется отдельными source-фрагментами. Agent-specific `complex-svg.md` остаётся отдельным reference.
Английский artifact дополнительно получает без изменений `README.md` и содержательную пользовательскую документацию из `docs/en/`; русский — `README_RU.md` и `docs/ru/`. Локальный редакторский `guides/AGENTS.md`, а также навигационные `guides/README.md` и `reference/README.md` не копируются. Canonical-файлы находятся в `references/README.md` и `references/docs/en/` либо в `references/README_RU.md` и `references/docs/ru/`.
## Композиция Markdown
Сборщик сохраняет поддержку Markdown includes для будущих документов:
```md
<!-- include: ./fragments/mode-selection.md -->
```
Include раскрываются рекурсивно, путь считается относительно включающего файла. Циклы, отсутствующие файлы, выход за `src/skills/svg-sprites/`, frontmatter во фрагментах и нераскрытые include завершают сборку ошибкой. Заголовки не сдвигаются автоматически: entry содержит единственный `# H1`, inline-фрагменты начинаются с `##`.
## Локальная сборка
```bash
npm run build:skill
```
Команда собирает и валидирует обе языковые версии, затем атомарно заменяет корневой каталог `skills/`. Сборщик проверяет точный список файлов, безопасные пути, symlink, Markdown fences, локальные ссылки и anchors, единственный H1, frontmatter, размер основного документа и отсутствие `TODO`. `npm run check:skill` дополнительно проверяет, что версионируемые артефакты совпадают с результатом сборки.
+410
View File
@@ -0,0 +1,410 @@
import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
renameSync,
rmSync,
writeFileSync,
} from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import configs from './skill.config.mjs'
const skillDir = path.dirname(fileURLToPath(import.meta.url))
const repositoryRoot = path.resolve(skillDir, '../../..')
const outputRoot = path.join(repositoryRoot, 'skills')
const temporaryParent = path.join(repositoryRoot, '.tmp')
const includePattern = /<!--\s*include:\s*(.*?)\s*-->/g
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, { allowSame = false } = {}) {
const relativePath = path.relative(parentDir, childPath)
if ((allowSame && relativePath === '') || (relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath))) {
return
}
throw new Error(`Path is outside ${parentDir}: ${childPath}`)
}
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 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 listDirectoryFiles(directory, extensions, prefix = '') {
if (!existsSync(directory)) throw new Error(`Source directory not found: ${directory}`)
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(`Source directory must not contain symlinks: ${filePath}`)
if (entry.isDirectory()) files.push(...listDirectoryFiles(filePath, extensions, relativePath))
else if (entry.isFile() && (extensions.length === 0 || extensions.includes(path.extname(entry.name)))) files.push(relativePath)
else if (!entry.isFile()) throw new Error(`Unsupported source entry: ${filePath}`)
}
return files
}
function resolveIncludes(filePath, stack = []) {
assertInside(skillDir, filePath)
if (stack.includes(filePath)) {
const cycle = [...stack, filePath].map((entry) => path.relative(skillDir, entry)).join(' -> ')
throw new Error(`Circular Markdown include: ${cycle}`)
}
const content = readRegularFile(filePath)
if (content.startsWith('---')) {
throw new Error(`Source Markdown must not contain frontmatter: ${path.relative(skillDir, filePath)}`)
}
return content.replace(includePattern, (match, includePath) => {
const trimmedPath = includePath.trim()
assertSafeRelativePath(trimmedPath)
if (path.extname(trimmedPath) !== '.md') {
throw new Error(`Included source must be Markdown: ${trimmedPath}`)
}
const includedFile = path.resolve(path.dirname(filePath), trimmedPath)
assertInside(skillDir, includedFile)
return `${resolveIncludes(includedFile, [...stack, filePath]).trim()}\n`
})
}
function renderSkill(config, document) {
const entryPath = path.resolve(skillDir, document.entry)
const body = resolveIncludes(entryPath).trim()
if (includePattern.test(body)) throw new Error(`Unresolved Markdown include: ${document.entry}`)
includePattern.lastIndex = 0
const frontmatter = [
'---',
`name: ${config.name}`,
`description: ${JSON.stringify(config.description)}`,
]
frontmatter.push('---')
return [
...frontmatter,
'',
`<!-- Generated from src/skills/svg-sprites/${document.entry}. Do not edit manually. -->`,
'',
body,
'',
].join('\n')
}
function expandCopies(config) {
const copies = []
for (const entry of config.copy ?? []) {
if (entry.from && entry.to) {
assertSafeRelativePath(entry.to)
copies.push({
from: path.resolve(skillDir, entry.from),
to: entry.to,
})
continue
}
if (entry.fromDirectory && entry.toDirectory) {
assertSafeRelativePath(entry.toDirectory)
const sourceDirectory = path.resolve(skillDir, entry.fromDirectory)
const extensions = entry.extensions ?? []
const sourceFiles = listDirectoryFiles(sourceDirectory, extensions)
const excluded = new Set((entry.exclude ?? []).map((relativePath) => {
assertSafeRelativePath(relativePath)
return relativePath.replaceAll('\\', '/')
}))
for (const relativePath of sourceFiles) {
if (excluded.has(relativePath)) continue
copies.push({
from: path.join(sourceDirectory, relativePath),
to: path.posix.join(entry.toDirectory, relativePath),
})
}
continue
}
throw new Error(`Invalid copy entry in ${config.name}`)
}
return copies
}
function prepareConfig(config) {
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.documents) || config.documents.length === 0) {
throw new Error(`Skill documents must be a non-empty array: ${config.name}`)
}
assertSafeRelativePath(config.output)
const outputDir = path.resolve(outputRoot, config.output)
assertInside(outputRoot, outputDir)
const documents = config.documents.map((document) => {
assertSafeRelativePath(document.entry)
assertSafeRelativePath(document.to)
const entryPath = path.resolve(skillDir, document.entry)
assertInside(skillDir, entryPath)
return { ...document, entryPath }
})
const skillDocuments = documents.filter((document) => document.skill === true)
if (skillDocuments.length !== 1 || skillDocuments[0].to !== 'SKILL.md') {
throw new Error(`Exactly one skill document targeting SKILL.md is required: ${config.name}`)
}
const copies = expandCopies(config)
const targets = new Set()
for (const entry of [...documents, ...copies]) {
if (targets.has(entry.to)) throw new Error(`Duplicate artifact target in ${config.name}: ${entry.to}`)
targets.add(entry.to)
}
return {
config,
outputDir,
outputPath: path.relative(outputRoot, outputDir),
documents,
copies,
expectedFiles: [...targets].sort(),
}
}
function writeArtifactFile(targetDir, relativePath, content) {
const targetPath = path.resolve(targetDir, relativePath)
assertInside(targetDir, targetPath)
mkdirSync(path.dirname(targetPath), { recursive: true })
writeFileSync(targetPath, content)
}
function buildSkill(prepared, targetDir) {
mkdirSync(targetDir, { recursive: true })
for (const document of prepared.documents) {
const content = document.skill
? renderSkill(prepared.config, document)
: `${resolveIncludes(document.entryPath).trim()}\n`
writeArtifactFile(targetDir, document.to, content)
}
for (const copy of prepared.copies) {
writeArtifactFile(targetDir, copy.to, readRegularFile(copy.from))
}
}
function withoutCodeFences(content, relativePath) {
const visibleLines = []
let fence = null
for (const line of content.split('\n')) {
const match = line.match(/^\s*(`{3,}|~{3,})/)
if (match) {
if (!fence) fence = match[1]
else if (match[1][0] === fence[0] && match[1].length >= fence.length) fence = null
continue
}
if (!fence) visibleLines.push(line)
}
if (fence) throw new Error(`Unbalanced code fence: ${relativePath}`)
return visibleLines.join('\n')
}
function markdownAnchors(content) {
const anchors = new Set()
const occurrences = new Map()
for (const match of withoutCodeFences(content, 'Markdown').matchAll(/^#{1,6}\s+(.+?)\s*#*$/gm)) {
const base = match[1]
.toLowerCase()
.replace(/<[^>]+>/g, '')
.replace(/[^\p{L}\p{N} _-]/gu, '')
.trim()
.replace(/\s+/g, '-')
const occurrence = occurrences.get(base) ?? 0
occurrences.set(base, occurrence + 1)
anchors.add(occurrence === 0 ? base : `${base}-${occurrence}`)
}
return anchors
}
function validateMarkdown(skillRoot, relativePath) {
const filePath = path.join(skillRoot, relativePath)
const content = readFileSync(filePath, 'utf8')
const visibleContent = withoutCodeFences(content, relativePath)
if (includePattern.test(visibleContent)) throw new Error(`Unresolved Markdown include: ${relativePath}`)
includePattern.lastIndex = 0
for (const match of visibleContent.matchAll(/\]\(([^)]+)\)/g)) {
const target = match[1].trim().split(/\s+['"]/)[0]
if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target)) continue
const [rawTargetPath, rawAnchor] = target.split('#', 2)
let targetPath
let anchor
try {
targetPath = decodeURIComponent(rawTargetPath.split('?')[0])
anchor = rawAnchor ? decodeURIComponent(rawAnchor).toLowerCase() : ''
} catch {
throw new Error(`Invalid encoded link in ${relativePath}: ${target}`)
}
const resolvedPath = targetPath
? path.resolve(path.dirname(filePath), targetPath)
: filePath
assertInside(skillRoot, resolvedPath)
if (!existsSync(resolvedPath) || !lstatSync(resolvedPath).isFile()) {
throw new Error(`Broken local link in ${relativePath}: ${target}`)
}
if (anchor) {
const anchors = markdownAnchors(readFileSync(resolvedPath, 'utf8'))
if (!anchors.has(anchor)) throw new Error(`Broken local anchor in ${relativePath}: ${target}`)
}
}
}
function validateArtifact(prepared, skillRoot) {
const actualFiles = listFiles(skillRoot).sort()
if (JSON.stringify(actualFiles) !== JSON.stringify(prepared.expectedFiles)) {
throw new Error(`Unexpected skill files in ${prepared.config.name}:\n${actualFiles.join('\n')}`)
}
const skill = readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8')
if (!skill.startsWith(`---\nname: ${prepared.config.name}\ndescription: `)) {
throw new Error(`Generated SKILL.md has invalid frontmatter: ${prepared.config.name}`)
}
if (/\bTODO\b/.test(skill)) throw new Error(`Generated SKILL.md contains TODO: ${prepared.config.name}`)
const visibleSkill = withoutCodeFences(skill, 'SKILL.md')
const h1Count = visibleSkill.match(/^#\s+/gm)?.length ?? 0
if (h1Count !== 1) throw new Error(`Generated SKILL.md must contain exactly one H1: ${prepared.config.name}`)
if (prepared.config.maxSkillBytes && Buffer.byteLength(skill) > prepared.config.maxSkillBytes) {
throw new Error(`Generated SKILL.md exceeds ${prepared.config.maxSkillBytes} bytes: ${prepared.config.name}`)
}
for (const relativePath of actualFiles.filter((filePath) => filePath.endsWith('.md'))) {
validateMarkdown(skillRoot, relativePath)
}
}
function assertArtifactDirectory(directory) {
if (!existsSync(directory)) {
throw new Error('Generated skills are missing. Run npm run build:skill.')
}
const stats = lstatSync(directory)
if (stats.isSymbolicLink() || !stats.isDirectory()) {
throw new Error(`Skill output must be a directory without symlinks: ${directory}`)
}
}
function assertArtifactsCurrent(stagedRoot) {
assertArtifactDirectory(outputRoot)
const expectedFiles = listFiles(stagedRoot).sort()
const actualFiles = listFiles(outputRoot).sort()
const expectedSet = new Set(expectedFiles)
const actualSet = new Set(actualFiles)
const missingFiles = expectedFiles.filter((file) => !actualSet.has(file))
const unexpectedFiles = actualFiles.filter((file) => !expectedSet.has(file))
const changedFiles = expectedFiles.filter((file) => (
actualSet.has(file)
&& !readFileSync(path.join(stagedRoot, file)).equals(readFileSync(path.join(outputRoot, file)))
))
if (missingFiles.length || unexpectedFiles.length || changedFiles.length) {
const details = [
...missingFiles.map((file) => `Missing: ${file}`),
...unexpectedFiles.map((file) => `Unexpected: ${file}`),
...changedFiles.map((file) => `Changed: ${file}`),
]
throw new Error(`Generated skills are out of date. Run npm run build:skill.\n${details.join('\n')}`)
}
}
function replaceDirectory(stagedDir, outputDir) {
const backupDir = path.join(path.dirname(stagedDir), `.skills-backup-${process.pid}`)
rmSync(backupDir, { recursive: true, force: true })
if (existsSync(outputDir)) {
assertArtifactDirectory(outputDir)
renameSync(outputDir, backupDir)
}
try {
renameSync(stagedDir, outputDir)
rmSync(backupDir, { recursive: true, force: true })
} catch (error) {
rmSync(outputDir, { recursive: true, force: true })
if (existsSync(backupDir)) renameSync(backupDir, outputDir)
throw error
}
}
if (!Array.isArray(configs) || configs.length === 0) {
throw new Error('Skill configs must be a non-empty array')
}
const preparedConfigs = configs.map(prepareConfig)
const names = new Set()
for (const prepared of preparedConfigs) {
if (names.has(prepared.config.name)) throw new Error(`Duplicate skill name: ${prepared.config.name}`)
names.add(prepared.config.name)
}
for (const [index, prepared] of preparedConfigs.entries()) {
for (const other of preparedConfigs.slice(index + 1)) {
const overlap = path.relative(prepared.outputDir, other.outputDir)
const reverseOverlap = path.relative(other.outputDir, prepared.outputDir)
if (overlap === '' || (!overlap.startsWith('..') && !path.isAbsolute(overlap)) || (!reverseOverlap.startsWith('..') && !path.isAbsolute(reverseOverlap))) {
throw new Error(`Overlapping skill outputs: ${prepared.config.output} and ${other.config.output}`)
}
}
}
mkdirSync(temporaryParent, { recursive: true })
const temporaryRoot = mkdtempSync(path.join(temporaryParent, 'skills-build-'))
try {
for (const prepared of preparedConfigs) {
const stagedDir = path.join(temporaryRoot, prepared.outputPath)
buildSkill(prepared, stagedDir)
validateArtifact(prepared, stagedDir)
}
if (isCheck) {
assertArtifactsCurrent(temporaryRoot)
for (const prepared of preparedConfigs) {
console.log(`Skill artifact is up to date: ${prepared.config.name}`)
}
} else {
replaceDirectory(temporaryRoot, outputRoot)
for (const prepared of preparedConfigs) {
console.log(`Built skill: ${path.relative(process.cwd(), prepared.outputDir)}`)
}
}
} finally {
rmSync(temporaryRoot, { recursive: true, force: true })
}
+65
View File
@@ -0,0 +1,65 @@
const agentReferences = {
en: [
'complex-svg.md',
],
ru: [
'complex-svg.md',
],
}
function documents(language) {
return [
{ entry: `src/${language}/SKILL.md`, to: 'SKILL.md', skill: true },
...agentReferences[language].map((file) => ({
entry: `src/${language}/references/${file}`,
to: `references/${file}`,
})),
]
}
const englishDocumentation = [
{ from: '../../../README.md', to: 'references/README.md' },
{
fromDirectory: '../../../docs/en',
toDirectory: 'references/docs/en',
extensions: ['.md'],
exclude: [
'guides/AGENTS.md',
'guides/README.md',
'reference/README.md',
],
},
]
const russianDocumentation = [
{ from: '../../../README_RU.md', to: 'references/README_RU.md' },
{
fromDirectory: '../../../docs/ru',
toDirectory: 'references/docs/ru',
extensions: ['.md'],
exclude: [
'guides/AGENTS.md',
'guides/README.md',
'reference/README.md',
],
},
]
export default [
{
name: 'svg-sprites',
description: 'Use only when configuring, generating, or troubleshooting @gromlab/svg-sprites. Triggers: @gromlab/svg-sprites, svg-sprite.config.json, defineSpriteConfig, generateSprite, standalone@server, source: remote, ServerSvgInput, exact modes for standalone, React, Next.js, Vue, Nuxt, Svelte, Angular, Astro, Solid, Preact, Qwik, Lit, or Alpine.js, SpriteConfig.input, --input, SpriteViewer, or --icon-color-N. Do NOT use for custom SVG sprites, favicons, raster images, icon fonts, choosing an icon set, or inline SVG without this package.',
output: 'svg-sprites',
maxSkillBytes: 48_000,
documents: documents('en'),
copy: englishDocumentation,
},
{
name: 'svg-sprites-ru',
description: 'Используй только при настройке, изменении или диагностике @gromlab/svg-sprites. Триггеры: @gromlab/svg-sprites, svg-sprite.config.json, defineSpriteConfig, generateSprite, standalone@server, source: remote, ServerSvgInput, exact modes для standalone, React, Next.js, Vue, Nuxt, Svelte, Angular, Astro, Solid, Preact, Qwik, Lit или Alpine.js, SpriteConfig.input, --input, SpriteViewer и --icon-color-N. НЕ используй для самописных SVG-спрайтов, inline SVG, favicon, растровых изображений, icon fonts или выбора библиотеки иконок.',
output: 'svg-sprites-ru',
maxSkillBytes: 48_000,
documents: documents('ru'),
copy: russianDocumentation,
},
]
+352
View File
@@ -0,0 +1,352 @@
# @gromlab/svg-sprites
## What the package does
`@gromlab/svg-sprites` is a CLI generator that builds SVG sprites from user-provided SVG files. The package does not include its own icon set: it compiles project SVGs into an external sprite asset and creates a typed native component for the selected exact framework and bundler mode.
The package supports multiple independent sprites in one project. Each explicitly selected config file or config-less directory describes one sprite and gets its own:
- SVG asset;
- mode-specific manifest data;
- icon name types and production entry `.svg-sprite/index.js` for every mode except bare `standalone`;
- an isolated framework-native component and declarations for framework modes;
- a native Web Component with an explicit registration function for `standalone@vite`/`standalone@webpack`;
- a deployment-neutral JSON manifest without a public URL for bare `standalone`.
- a content-addressed server release with two compile profiles and an integrity manifest for `standalone@server`.
The project determines how many sprite directories exist and where they live. For example, `name: 'file-manager'` produces `FileManagerIcon`, `FileManagerIconName`, and `fileManagerIconNames`, while another directory with `name: 'navigation'` produces a separate `NavigationIcon`. These are examples of per-sprite APIs, not fixed package exports.
Generated production runtime and declarations do not import `@gromlab/svg-sprites`. Generation through `npx --yes @gromlab/svg-sprites <path-to-config>` does not add the package to the project. Install it as a development dependency only for the Viewer, package-provided config types, or the programmatic API.
Any consumer exact mode can use `source: 'remote'` with one local path or HTTP(S)
URL to a manifest produced by `standalone@server`. Generation verifies and downloads
the required profile before the adapter creates its normal local API and asset; the
browser never depends on the server manifest at runtime.
## Selecting a mode
Select exactly one supported mode key:
| Project | Mode key |
|---|---|
| Static HTML / custom publishing | `standalone` |
| Standalone + Vite | `standalone@vite` |
| Standalone + Webpack 5 | `standalone@webpack` |
| Server or CI release | `standalone@server` |
| React + Vite | `react@vite` |
| React + Webpack 5 | `react@webpack` |
| Vue + Vite | `vue@vite` |
| Vue + Webpack | `vue@webpack` |
| Nuxt + Vite | `nuxt@vite` |
| Nuxt + Webpack | `nuxt@webpack` |
| Svelte + Vite | `svelte@vite` |
| Svelte + Webpack | `svelte@webpack` |
| SvelteKit + Vite | `sveltekit@vite` |
| Angular application builder | `angular@application` |
| Angular + Webpack | `angular@webpack` |
| Astro + Vite | `astro@vite` |
| Solid + Vite | `solid@vite` |
| Solid + Webpack | `solid@webpack` |
| SolidStart + Vite | `solid-start@vite` |
| Preact + Vite | `preact@vite` |
| Preact + Webpack | `preact@webpack` |
| Qwik + Vite | `qwik@vite` |
| Lit + Vite | `lit@vite` |
| Lit + Webpack | `lit@webpack` |
| Alpine.js + Vite | `alpine@vite` |
| Alpine.js + Webpack | `alpine@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` |
Mode may come from the config, CLI, or programmatic API. Values are applied as `defaults → config → CLI/API overrides`. A mode must exist after merging.
`name` is optional. When omitted, the generator converts the sprite-module directory name to kebab-case; directories named `svg-sprite` and `svg-sprites` use their parent directory's name. An explicit `name` must already be kebab-case and begin with an ASCII letter.
The CLI accepts exactly one path. A `.ts`, `.js`, or `.json` file loads that exact config regardless of its name. A directory enables config-less generation with settings supplied through CLI flags.
```json
{
"scripts": {
"sprite:<name>": "npx --yes @gromlab/svg-sprites <path-to-config>",
"sprite:<name>:cli": "npx --yes @gromlab/svg-sprites --mode <mode-key> <sprite-directory>"
}
}
```
Generation through `npx` does not add the package to the project. Do not invent shortened or generic mode keys, and do not use the removed `legacy` mode. Select one complete key from the table. Use bare `standalone` only when the application publishes the SVG itself, and `standalone@server` only for a centralized release consumed during generation. Create one command per config file or directory when the project has multiple sprites.
## Inspecting the project
Establish the project's actual contract before making changes:
1. Read the root `package.json`, lockfile, and workspace configuration; identify the framework, bundler, and existing commands.
2. Find config files, commands containing `svg-sprites`, and imports of generated components. Config names are arbitrary; use the explicit CLI path and object fields.
3. Determine the framework, router when applicable, and actual bundler from scripts and configuration. For Next.js, separately determine the App/Pages Router and the bundler used by the real `dev`/`build` commands.
4. Check existing `predev`, `prebuild`, `pretypecheck`, and orchestration scripts. Do not overwrite them.
5. For a new sprite, choose a target directory without imposing a particular application layer or architecture.
6. Check TypeScript and alias settings. Package subpath exports require TypeScript 5+ with `moduleResolution: 'bundler'`, `'node16'`, or `'nodenext'`.
For a regular local consumer, all input paths are relative to the directory containing the explicitly selected config file; in config-less mode they are relative to the supplied directory. Inspect local `input` using this contract:
- `input?: string | string[]` defaults to `./icons`;
- each string is a folder, an exact SVG file, or a glob;
- a folder is scanned shallowly; nested files are included only by an explicit recursive glob such as `./icons/**/*.svg`;
- an array combines positive sources, while an item prefixed with `!` excludes its matches from the combined set;
- every positive source must resolve to at least one SVG, so a missing or empty folder, an unmatched glob, a missing file, or a non-SVG exact file is an error;
- resolved files are deduplicated and sorted deterministically;
- different files with the same basename are a conflict, even when they came from different sources.
Branch before applying those rules:
- `standalone@server` may combine local strings with `{ name, url, sha256? }` HTTP(S) descriptors; `name` is the public icon name and optional `sha256` verifies the downloaded bytes;
- `source: 'remote'` requires exactly one string containing a local manifest path or HTTP(S) manifest URL and does not accept source globs or descriptors;
- a remote consumer config contains only `mode`, `source`, and `input`; name, description, transforms, and generated notice come from the verified server manifest.
Do not copy a shared SVG into several folders: add its exact path or a suitable glob to `input` in every sprite that needs it. Use `**/*.svg` only when recursive inclusion is intentional.
## Setting up the integration
Do not reproduce mode setup from memory. After inspecting the project, select one exact mode and open the corresponding file under `references/docs/en/guides/`. Treat that guide as the base operational contract, then adapt it to the project's existing structure.
Work in this order:
1. Identify the source SVG directory and the directory for one sprite module. One config creates one independent sprite; multiple sets require separate config files and unique `name` values.
2. Confirm the framework, router, and bundler against the exact mode. For Next.js, inspect the actual `dev` and `build` scripts, not just the presence of `next.config.*`.
3. Prefer a JSON config when the project does not need package-provided config types. A TypeScript config also loads through the CLI, but the package must be installed when the config imports `defineSpriteConfig` or package types.
4. Resolve every `input` from the config-file directory. Do not reorganize SVGs unnecessarily: use a folder path, exact file, glob, or array of these sources.
5. Add a sprite command with an explicit config path. Preserve existing `dev`, `build`, `typecheck`, and lifecycle hooks; place generation before the first process that imports `.svg-sprite`.
6. Do not run one generation twice through both a concurrent `predev` and `npm run sprites && ...`. For multiple sprites, create separate commands and one aggregate script.
7. If the application imports the sprite-module directory, create a user-owned `index.ts` next to `.svg-sprite`; do not place user files inside the generated directory.
8. Run the first generation before typecheck or application startup, then inspect the mode-specific output and the actual component import.
For a centralized release, open `references/docs/en/guides/standalone-server.md`.
Generate and publish the complete `.svg-sprite` directory atomically. In each consumer,
retain its own exact framework mode, set `source: 'remote'`, and point `input` to that
manifest. Do not copy server files into a framework output or fetch the manifest from
application runtime.
Do not add the Viewer automatically. Connect it only when requested or when visual verification of the set, colors, or complex SVGs is needed. Get the production isolation pattern from the exact guide: frameworks, bundlers, and routers use different boundaries.
Do not copy snippets between exact modes even when their APIs look similar. Asset URLs, generated files, CSS handling, router boundaries, and debug-tool setup differ.
## Generated directory contract
For example, after generation a React/Next.js directory has this structure:
```text
svg-sprite/
├── icons/ # user-owned sources
├── svg-sprite.config.json # recommended config name
├── index.ts # optional user-owned barrel
├── .gitignore # managed by the generator
└── .svg-sprite/
├── index.js
├── index.d.ts
├── icon-data.js
├── icon-data.d.ts
├── sprite.svg
├── svg-sprite.manifest.js
├── svg-sprite.manifest.d.ts
└── react/
├── react-component.js
├── react-component.d.ts
└── react-component.module.css
```
Standalone modes do not create `react/`. Bare `standalone` generates `sprite.svg` and `svg-sprite.manifest.json`; `standalone@vite`/`standalone@webpack` additionally generate `index.*`, `icon-data.*`, and a resolved manifest. Their `index.*` also contains a native generated Web Component; bare `standalone` gets no JavaScript runtime and does not create `.gitignore`.
`standalone@server` generates `sprite.<content-hash>.svg`,
`sprite-root-viewbox.<content-hash>.svg`, and `svg-sprite.manifest.json`. It has no
consumer facade, browser runtime, Viewer entry, or `.gitignore`. The manifest records
both relative profile URLs, full SHA-256 digests, byte lengths, icon metadata, and
transform settings.
Edit the source SVGs, selected config, and user-owned `index.ts`. Do not manually change anything in `.svg-sprite`: the next generation will overwrite it. In every mode except bare `standalone`, the generated `.gitignore` is also managed by the generator. To import from the sprite-module root, create a barrel:
```ts
export * from './.svg-sprite/index.js'
```
The generator owns the complete `.svg-sprite` directory and replaces it on every run. Never put user files inside it. The generator also owns `.gitignore` when the selected mode creates it. Bare `standalone` preserves a user-owned `.gitignore`, but removes a managed `.gitignore` left by another mode. Generated paths must not contain symlinks.
Every exact-mode adapter owns its facade, framework directory, native component runtime, declarations, manifest source, styles, and asset URL. React/Next use `react/`; other framework modes use their own generated contract documented by the matching guide. Standalone bundler modes export Web Component helpers and types; bare `standalone` does not create a facade. Manifest declarations define their types locally and do not import the generator package.
In bundler modes, the sprite remains a separate asset and SVG path data is not embedded in JavaScript. The content hash depends on bundler settings. Bare `standalone` creates a fixed filename, and the application owns its public name and versioning:
- Vite-based adapters use a mode-owned static asset import that keeps the sprite external;
- `standalone@vite` uses the same Vite asset mechanism and exports an href helper plus a native Web Component without React;
- `standalone@webpack` uses Webpack Asset Modules and exports the same mode-local Web Component without React;
- Webpack-based adapters and all Next modes use their adapter-owned external asset mechanism, commonly `new URL(..., import.meta.url).href`;
- a custom Webpack SVG loader must not intercept the generated `sprite.svg`;
- in Next mode, the generated component does not contain `'use client'` and works in Server Components, SSR, and SSG; do not add a client boundary solely for an icon;
- the Next build command and mode key must agree: Turbopack with `.../turbopack`, Webpack with `.../webpack`.
- remote consumers still publish through their own adapter's local asset pipeline; do not preserve or construct the server profile URL in generated application code.
For bundler modes, do not move the generated sprite into `public` or rewrite its URL manually. For bare `standalone`, do not move the managed original: the application may explicitly copy it into deploy output and owns the public URL and stale-copy cleanup. Regenerate with the new complete key when changing mode.
## Usage, accessibility, and colors
The component name depends on the specific sprite's `name`. In `standalone@vite` and `standalone@webpack`, `name: 'file-manager'` creates the `<file-manager-icon>` tag and the `defineFileManagerIconElement()` function:
```ts
import { defineFileManagerIconElement } from './svg-sprite'
defineFileManagerIconElement()
```
```html
<file-manager-icon icon="folder" aria-hidden="true"></file-manager-icon>
```
The native element has no runtime dependencies, selects the generated ID and `viewBox`, obtains the URL through the bundler, and renders `<svg><use>` in Shadow DOM. Its `icon` property is typed with the exact name union, while plain HTML attribute values are validated only at runtime. It defaults to `1em × 1em`; resize the host with CSS. Bare `standalone` does not generate a Web Component.
In component modes, the same `name: 'file-manager'` creates a native `FileManagerIcon` component. Its syntax and props follow the exact-mode guide. For React/Next.js, `name: 'navigation'` creates `NavigationIcon`.
Import the component from the root of its sprite directory. `width` and `height` are optional: ordinary CSS classes can control the size.
```tsx
import { FileManagerIcon } from './svg-sprite'
export const OpenButton = () => (
<button type="button">
<FileManagerIcon icon="folder" className="icon" aria-hidden="true" />
<span>Open</span>
</button>
)
```
```css
.icon {
width: 24px;
height: 24px;
color: #4b5563;
}
```
`icon` accepts exact source filenames without `.svg`; an unknown name is a TypeScript error. For names that are not safe SVG IDs, the generator preserves the public name but creates an internal stable hash ID, so do not construct a fragment URL from the name manually.
By default, the component renders `<svg>` and accepts standard SVG attributes: optional `width`/`height`, `className`, `style`, `role`, `aria-*`, and event handlers. With `wrapped={true}`, the root becomes a `<span>`, props apply to the span, and the inner SVG fills the wrapper.
The generated component does not decide semantics for the application and does not add a `title`. For a decorative icon, pass `aria-hidden="true"`; for a standalone meaningful icon, pass `role="img"` and an accessible name through `aria-label`. Do not duplicate the name when adjacent text already announces the action. Put interactivity on a `button` or `a`, not on the icon itself.
The `removeSize`, `replaceColors`, and `addTransition` transforms are enabled by default. A monochrome icon's only color gets a `currentColor` fallback, so control it with the CSS `color` property. For a multicolor icon, pass typed custom properties:
```tsx
<FileManagerIcon
icon="folder"
style={{
'--icon-color-1': '#4b5563',
'--icon-color-2': '#14b8a6',
}}
/>
```
Automatic replacement targets `fill`/`stroke` attributes and inline `style`. The values `none`, `transparent`, `inherit`, `unset`, and `initial` are not replaced. Check CSS classes and external stylesheets, gradients, patterns, filters, and `url(#...)` against the actual output. Page variables work through `<svg><use>`, but do not cross into an external document loaded through `<img>` or `background-image`; a CSS mask preserves only a monochrome silhouette.
`SpriteViewer` is optional. Install `@gromlab/svg-sprites` as a development dependency only when the project needs the Viewer. It accepts manifests or statically discoverable loaders and provides search, themes, colors, and examples, but production components do not depend on it.
Open the exact guide before connecting the Viewer. Frameworks, bundlers, and routers require different debug entries or client boundaries. Do not transfer setup between modes.
## Verifying the result
After changing a config or SVG, perform these required checks:
1. Run the exact sprite command. It must exit with code `0` and report the name, icon count, mode, and `.svg-sprite` directory.
2. Inspect the output for the selected exact mode:
- bare `standalone` creates `sprite.svg` and `svg-sprite.manifest.json`;
- `standalone@server` creates two content-addressed SVG profiles and a server manifest whose hashes and relative paths match those files;
- `standalone@vite` and `standalone@webpack` additionally create `index.*`, `icon-data.*`, and a JS manifest, but no `react/` directory;
- framework modes also create their adapter-owned native component runtime, declaration, and styles.
3. For modes with a public facade, inspect `.svg-sprite/index.js`, the adjacent `index.d.ts`, the name list, and the actual import through the user-owned barrel.
4. Inspect the manifest: mode and target must match the selected adapter, and the icon list must match the source SVGs. In bundler modes the URL must use the mode-specific mechanism; the bare JSON manifest intentionally has no public `spriteUrl`.
5. Run the project's existing typecheck when the mode creates types or user-owned TypeScript changed.
6. Run the smallest application command affected by the change: `dev`, build, or a project-specific check.
Do not run a full production build solely to verify a new icon name. It is required when the bundler target, router, Webpack loader, asset URL, or deployment path changed, or when diagnosing a production-only error.
Perform visual, Network, and accessibility-tree checks only when a running application and browser tools are available. If those tools are unavailable, do not claim that colors, themes, accessibility, or the asset's HTTP response were verified; explicitly state what remains unchecked.
Use the Viewer for complex colors, transforms, and broad visual checks. Do not add a debug route for routine generation of one sprite.
## Diagnostics
Match the symptom to the relevant check and fix the root cause:
| Symptom | Likely cause | Action |
|---|---|---|
| `Missing sprite config file or module directory` | The positional path is missing | Pass one config file or a directory for config-less generation. |
| `Expected one config file or module directory` | Multiple paths were passed | Create one command per sprite and combine the scripts. |
| `Sprite mode is required` | Mode is absent from both config and CLI | Add `mode` to the object or pass the full `--mode`. |
| `Unsupported sprite config extension` | The supplied file is not `.ts`, `.js`, or `.json` | Use a supported config format. |
| A positive input source has no SVG matches | A folder is missing or empty, a glob matches nothing, or an exact path is missing or not an SVG | Resolve the source from the config directory and correct `input`; every positive item must produce at least one SVG. |
| Icons from a subdirectory are missing | A folder source was expected to scan recursively | Use an explicit glob such as `./icons/**/*.svg`; folders are shallow. |
| An excluded icon is still present | The exclusion lacks a leading `!`, is not in the `input` array, or is relative to the wrong directory | Add a matching `!` item and resolve it from the config directory. |
| CLI source selection is incomplete | Multiple sources were packed into one `--input` value or an option was omitted | Repeat `--input <path-or-glob>` once per source or exclusion. |
| Icon name or SVG ID collision | Two different files have the same basename, or a hash ID collides with a name | Rename one source SVG; do not select a file implicitly. |
| `Refusing to overwrite a user file` | A user-owned `.gitignore` already exists at the sprite-module root where the mode must create one | Do not overwrite it: choose another sprite directory or coordinate moving the existing `.gitignore`. |
| Missing `.svg-sprite/index.js` or name absent from autocomplete | This is expected for bare `standalone`; in other modes generation did not run, the barrel is wrong, or the type server cached an old module | Confirm the exact mode, run the sprite command, check `export * from './.svg-sprite/index.js'`, then typecheck; restart the TypeScript server if necessary. |
| SVG does not load or the URL is wrong | Mode and bundler differ, Webpack `publicPath` is wrong, or a custom loader intercepted the asset | Align mode with the build command, check Asset Modules/`publicPath`, and exclude the generated SVG from the incompatible loader. |
| Next build differs between SSR and browser | The module targets another bundler/router, or the URL was rewritten manually | Restore the generated `new URL(...)`, select the exact Next mode, and regenerate. |
| `color` does not change a multicolor icon | The icon uses several variables or is rendered through `<img>`/CSS background | Use `<FileManagerIcon>`/`<svg><use>` and the required `--icon-color-N` properties. |
| Gradient/filter renders incorrectly | Automatic color replacement cannot guarantee complex paint servers | Inspect the generated SVG; disable `replaceColors` for the sprite or simplify the source if necessary. |
| Viewer is empty | The manifest was not generated, the loader is not discoverable by the bundler, or the Client Component boundary is wrong | Generate the sprite first, then compare the manifest import and setup with the exact guide; in the App Router keep `'use client'` only in the Viewer component. |
| Remote manifest is rejected | It is not a `standalone@server` schema, contains an unsafe profile path, or its metadata is inconsistent | Publish the untouched complete server release and point `input` to its JSON manifest. |
| Remote sprite integrity check fails | The SVG is stale, truncated, or was changed independently from the manifest | Republish the manifest and both content-addressed profiles atomically; never overwrite a hashed SVG with different bytes. |
For an unknown error, record the complete CLI command, mode, config-file or directory path, and first stack/error message. Then reduce it to one sprite without deleting user files or a managed `.gitignore`.
## Operational reference map
References are included in the built skill. Open only the documents relevant to the current task, but always open the exact-mode guide before changing an integration.
### Overview
- [Package README](./references/README.md) covers capabilities, the primary React/Next.js example, all supported families, and documentation links.
### Configuration
- [Configuration](./references/docs/en/configuration.md) covers JSON, JavaScript, and TypeScript configs, config fields, `input`, and CLI invocation.
### Exact-mode guides
- [`standalone`](./references/docs/en/guides/standalone.md) covers static HTML and custom SVG publishing.
- [`standalone@vite`](./references/docs/en/guides/standalone-vite.md) covers a vanilla Vite application and the Web Component.
- [`standalone@webpack`](./references/docs/en/guides/standalone-webpack.md) covers a vanilla Webpack 5 application and the Web Component.
- [`standalone@server`](./references/docs/en/guides/standalone-server.md) covers centralized content-addressed releases and remote consumers.
- [`react@vite`](./references/docs/en/guides/react-vite.md) covers React with Vite.
- [`react@webpack`](./references/docs/en/guides/react-webpack.md) covers React with Webpack 5.
- [`vue@vite`](./references/docs/en/guides/vue-vite.md) covers Vue with Vite.
- [`vue@webpack`](./references/docs/en/guides/vue-webpack.md) covers Vue with Webpack.
- [`nuxt@vite`](./references/docs/en/guides/nuxt-vite.md) covers Nuxt with Vite.
- [`nuxt@webpack`](./references/docs/en/guides/nuxt-webpack.md) covers Nuxt with Webpack.
- [`svelte@vite`](./references/docs/en/guides/svelte-vite.md) covers Svelte with Vite.
- [`svelte@webpack`](./references/docs/en/guides/svelte-webpack.md) covers Svelte with Webpack.
- [`sveltekit@vite`](./references/docs/en/guides/sveltekit-vite.md) covers SvelteKit with Vite.
- [`angular@application`](./references/docs/en/guides/angular-application.md) covers the Angular application builder.
- [`angular@webpack`](./references/docs/en/guides/angular-webpack.md) covers Angular with Webpack.
- [`astro@vite`](./references/docs/en/guides/astro-vite.md) covers Astro with Vite.
- [`solid@vite`](./references/docs/en/guides/solid-vite.md) covers Solid with Vite.
- [`solid@webpack`](./references/docs/en/guides/solid-webpack.md) covers Solid with Webpack.
- [`solid-start@vite`](./references/docs/en/guides/solid-start-vite.md) covers SolidStart with Vite.
- [`preact@vite`](./references/docs/en/guides/preact-vite.md) covers Preact with Vite.
- [`preact@webpack`](./references/docs/en/guides/preact-webpack.md) covers Preact with Webpack.
- [`qwik@vite`](./references/docs/en/guides/qwik-vite.md) covers Qwik with Vite.
- [`lit@vite`](./references/docs/en/guides/lit-vite.md) covers Lit with Vite.
- [`lit@webpack`](./references/docs/en/guides/lit-webpack.md) covers Lit with Webpack.
- [`alpine@vite`](./references/docs/en/guides/alpine-vite.md) covers Alpine.js with Vite.
- [`alpine@webpack`](./references/docs/en/guides/alpine-webpack.md) covers Alpine.js with Webpack.
- [`next@app/turbopack`](./references/docs/en/guides/next-app-turbopack.md) covers the Next.js App Router with Turbopack.
- [`next@app/webpack`](./references/docs/en/guides/next-app-webpack.md) covers the Next.js App Router with Webpack.
- [`next@pages/turbopack`](./references/docs/en/guides/next-pages-turbopack.md) covers the Next.js Pages Router with Turbopack.
- [`next@pages/webpack`](./references/docs/en/guides/next-pages-webpack.md) covers the Next.js Pages Router with Webpack.
### Technical references
- [Technical reference](./references/docs/en/reference/technical.md) covers requirements, CLI, unified configuration, naming, generated APIs, assets, transforms, colors, Viewer, Git, CI, and troubleshooting.
- [Programmatic API](./references/docs/en/reference/programmatic-api.md) covers `generateSprite`, overrides, config APIs, low-level compilation, and Viewer runtime.
### Agent-specific reference
- [Complex SVGs](./references/complex-svg.md) covers gradients, patterns, filters, masks, `url(#...)`, `viewBox`, fragment IDs, and visual diagnostics.
@@ -0,0 +1,176 @@
# Complex SVGs: diagnostics and safe generation
## When to use this reference
Use this document when a source contains `<defs>`, gradients, patterns, filters, masks, clip paths, internal `<style>`/classes, `url(#id)`, CSS variables, `<use>`, text, an unusual `viewBox`, spaces in its filename, or changes visually after generation. Also use it for reports involving color, sizing, clipping, or fragment-ID collisions.
## Classify the risk first
Inspect the source SVG before editing it:
```bash
npm run sprite:file-manager
```
Use the actual package script for the sprite. Then compare the source with `.svg-sprite/sprite.svg` and the manifest; do not draw conclusions from a successful exit code alone.
Pay particular attention to:
- `fill="url(#gradient)"`, `stroke="url(#pattern)"`;
- `filter="url(#shadow)"`, `mask="url(#mask)"`, `clip-path="url(#clip)"`;
- CSS rules inside `<style>` and external stylesheets;
- colors expressed through classes, presentation attributes, and inline `style` at the same time;
- `currentColor`, existing `var(...)`, `context-fill`, and `context-stroke`;
- duplicate IDs in `<defs>` across different files;
- SVGs without a `viewBox`, or with width/height that does not match the viewBox;
- embedded images, fonts, scripts, or external references.
## Actual pipeline
The compiler first applies SVGO `preset-default` while preserving `viewBox`, then applies custom transforms in this order:
1. `removeSize` removes `width` and `height` from the root `<svg>`.
2. `replaceColors` collects `fill` and `stroke` values from attributes and inline `style`, then replaces them with `var(--icon-color-N, fallback)`.
3. `addTransition` adds inline color transitions to `path`, `circle`, `ellipse`, `rect`, `line`, `polyline`, `polygon`, `text`, `tspan`, and `use` elements.
All three options default to `true` and apply to the entire sprite, not to individual icons.
```ts
import { defineSpriteConfig } from '@gromlab/svg-sprites'
export default defineSpriteConfig({
mode: 'react@vite',
name: 'illustrations',
transform: {
removeSize: false,
replaceColors: false,
addTransition: false,
},
})
```
This is a config for one of potentially many sprite modules in a project; its directory does not have to match a module/feature directory. For Next, use the corresponding full `mode` with the same `transform`.
## Dimensions and viewBox
`removeSize: true` removes intrinsic `width`/`height`, but does not create a missing `viewBox`. If the source lacks a valid `viewBox`, the generated icon may scale incorrectly or have a zero-sized viewport.
Correct source preparation:
```svg
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="..." />
</svg>
```
If physical dimensions are part of an illustration's contract, set `removeSize: false` and verify component-prop behavior. Do not preserve width/height as a substitute for a missing viewBox.
React compilation leaves the root sprite `rootViewBox` disabled; Next enables it. Every shape must still have its own valid viewBox, which is included in the manifest and used by the Viewer.
## Colors
For one detected color, the fallback becomes `currentColor`:
```svg
stroke="var(--icon-color-1, currentColor)"
```
For multiple colors, the original fallbacks are preserved:
```svg
fill="var(--icon-color-1, #798198)"
fill="var(--icon-color-2, #ffffff)"
```
The values `none`, `transparent`, `inherit`, `unset`, and `initial` are not replaced. Color comparison normalizes case and whitespace but does not merge equivalent forms such as `#fff`, `#ffffff`, and `rgb(...)`.
Automatic analysis is primarily reliable for `fill`/`stroke` attributes and inline `style`. It does not parse CSS selectors in an internal `<style>` or external stylesheet as a full CSS AST.
For `url(#...)`, existing nested `var(...)`, gradients, and patterns, automatic replacement requires inspection of the generated output. If a paint-server reference changed or the Viewer shows incorrect controls, disable `replaceColors` for the entire sprite:
```ts
transform: {
replaceColors: false,
}
```
If ordinary recolorable icons are also needed, move complex illustrations into a separate sprite with a separate config. This is preferable to manually editing the generated SVG.
`addTransition` is independent of `replaceColors`. Even when original colors are preserved, transitions may still be added. For filters, animations, or custom CSS, disable both options if an inline transition changes behavior.
## Defs, references, and IDs
After SVGO and compilation, verify that each `url(#id)` or `<use href="#id">` refers to an existing ID within the corresponding shape. Do not assume IDs remain literal copies of the source; the optimizer/compiler may change them.
At minimum, check that:
- each gradient/pattern applies to the intended path;
- the filter region does not clip blur/shadow;
- masks and clip paths preserve their coordinate system (`userSpaceOnUse`/`objectBoundingBox`);
- an internal `<use>` is not confused with the sprite's external fragment;
- equal IDs from different source SVGs do not cause cross-icon collisions in the final document;
- external file/URL references are permitted by the production CSP and deployment.
If IDs collide, first make the source IDs unique and update all references within the SVG. Do not edit the compiled sprite.
## Filenames and external fragments
`FileManagerIcon` in the examples below is only an example generated name for a separate config with `name: 'file-manager'`; it is not a fixed API name.
A safe basename matches:
```text
^[a-zA-Z][a-zA-Z0-9_-]*$
```
It is preserved as the fragment ID. Other names, such as `folder open.svg` or `24-check.svg`, remain public TypeScript `icon` values but receive a stable `icon-<16 hex>` ID.
```tsx
<FileManagerIcon icon="folder open" />
```
Do not manually construct `#folder open`. Use the generated component or `.svg-sprite/svg-sprite.manifest.js`, which records both `name` and the actual `id`.
Different files with the same basename are forbidden, even from different directories. Rename one source meaningfully; source order or overlap never selects a winner.
## Rendering method
To control `color` and `--icon-color-N`, use the generated React component or `<svg><use>`:
```tsx
<FileManagerIcon
icon="diagram"
style={{
'--icon-color-1': '#334155',
'--icon-color-2': '#38bdf8',
}}
/>
```
The generated style type accepts `--icon-color-${number}`. `<img>` and CSS `background-image` load the SVG as an isolated document, so page variables do not propagate into it. A CSS mask keeps only the silhouette and loses gradients, filters, and color differences.
External stack-fragment support and paint-server behavior can vary across browsers. For critical complex graphics, when diagnosing runtime behavior and browser tools are available, test the target browsers; if they are incompatible, an SVG sprite may be the wrong delivery mechanism for that illustration.
## Required verification
1. Run generation with the correct mode.
2. Run the project's typecheck.
3. Open the generated sprite and find the shape using the ID from the manifest.
4. Statically compare `viewBox`, IDs, `url(#...)`, colors, and inline styles.
5. If the target/pipeline changed or a runtime issue is being diagnosed, build the production bundle and inspect the external hashed SVG.
6. When SpriteViewer and visual tools are available, test default colors and each `--icon-color-N` separately.
7. When browser tools are available and the runtime risk warrants it, test SSR/hydration for Next.js and target browsers for external fragments.
8. Do not claim visual or accessibility equivalence between source and output without the necessary tools and an actual comparison.
## Common symptoms and actions
- Icon became entirely `currentColor`: the pipeline detected one color. If the source semantics are more complex, disable `replaceColors` or normalize the source attributes.
- Gradient disappeared: check whether `fill="url(#...)"` was transformed, whether the target ID exists, and whether it collides with another icon.
- Shadow is clipped: inspect the filter region and viewBox; `removeSize` does not expand the area by itself.
- Viewer has no color controls: the color is defined through a class/stylesheet, or `replaceColors: false`; this is expected.
- Transition is duplicated or interferes with animation: an existing inline `transition` is not overwritten, but generated CSS also adds transitions; disable `addTransition` for the sprite.
- `<img>` ignores variables: switch to `<svg><use>` or the generated component; page variables cannot be passed into an isolated SVG document.
- A manual fragment fails for a name containing spaces: use the ID from the manifest.
- One complex icon requires different transforms: move it to a separate sprite; per-icon transform config is not supported.
For mode-specific execution and verification, return to the exact-mode guide selected from the main `SKILL.md`.
+352
View File
@@ -0,0 +1,352 @@
# @gromlab/svg-sprites
## Что делает пакет
`@gromlab/svg-sprites` — CLI-генератор SVG-спрайтов для пользовательских SVG-файлов. Пакет не содержит собственного набора иконок: он собирает SVG проекта во внешний sprite asset и создаёт типизированный нативный компонент для выбранного exact framework/bundler mode.
Пакет рассчитан на несколько независимых спрайтов в одном проекте. Каждый явно выбранный config-файл или config-less каталог описывает один спрайт и получает собственные:
- SVG asset;
- mode-specific manifest data;
- для всех modes, кроме bare `standalone`, — типы имён и production entry `.svg-sprite/index.js`;
- для framework modes — изолированный нативный компонент и declarations;
- для `standalone@vite`/`standalone@webpack` — нативный Web Component с явной функцией регистрации;
- для bare `standalone` — deployment-neutral JSON manifest без публичного URL.
- для `standalone@server` — content-addressed server release с двумя compile profiles и integrity manifest.
Количество и расположение каталогов определяет проект. Например, `name: 'file-manager'` создаёт `FileManagerIcon`, `FileManagerIconName` и `fileManagerIconNames`, а другой каталог с `name: 'navigation'` создаст отдельный `NavigationIcon`. Это примеры API отдельных спрайтов, а не фиксированные экспорты пакета.
Generated production runtime и declarations не импортируют `@gromlab/svg-sprites`. Генерация через `npx --yes @gromlab/svg-sprites <path-to-config>` не добавляет package в проект. Устанавливай его как development dependency только для Viewer, package-типов config или программного API.
Любой consumer exact mode может использовать `source: 'remote'` с одним local path
или HTTP(S) URL manifest, созданного `standalone@server`. До запуска adapter генератор
скачивает и проверяет нужный profile, после чего создаётся обычный локальный API и
asset; в runtime браузер не зависит от server manifest.
## Выбор режима
Выбери ровно один поддерживаемый mode key:
| Проект | Mode key |
|---|---|
| Static HTML / собственная публикация | `standalone` |
| Standalone + Vite | `standalone@vite` |
| Standalone + Webpack 5 | `standalone@webpack` |
| Server или CI release | `standalone@server` |
| React + Vite | `react@vite` |
| React + Webpack 5 | `react@webpack` |
| Vue + Vite | `vue@vite` |
| Vue + Webpack | `vue@webpack` |
| Nuxt + Vite | `nuxt@vite` |
| Nuxt + Webpack | `nuxt@webpack` |
| Svelte + Vite | `svelte@vite` |
| Svelte + Webpack | `svelte@webpack` |
| SvelteKit + Vite | `sveltekit@vite` |
| Angular application builder | `angular@application` |
| Angular + Webpack | `angular@webpack` |
| Astro + Vite | `astro@vite` |
| Solid + Vite | `solid@vite` |
| Solid + Webpack | `solid@webpack` |
| SolidStart + Vite | `solid-start@vite` |
| Preact + Vite | `preact@vite` |
| Preact + Webpack | `preact@webpack` |
| Qwik + Vite | `qwik@vite` |
| Lit + Vite | `lit@vite` |
| Lit + Webpack | `lit@webpack` |
| Alpine.js + Vite | `alpine@vite` |
| Alpine.js + Webpack | `alpine@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` |
Mode задаётся в config, CLI или программном API. Порядок применения: `defaults → config → CLI/API overrides`. После объединения mode обязателен.
`name` необязателен. Если он не задан, генератор преобразует имя каталога sprite-модуля в kebab-case; для каталогов `svg-sprite` и `svg-sprites` используется имя родительского каталога. Явное `name` должно уже быть записано в kebab-case и начинаться с латинской буквы.
CLI принимает ровно один путь. Путь к файлу `.ts`, `.js` или `.json` загружает именно этот конфиг независимо от имени. Путь к каталогу включает config-less генерацию, и настройки передаются флагами CLI.
```json
{
"scripts": {
"sprite:<name>": "npx --yes @gromlab/svg-sprites <path-to-config>",
"sprite:<name>:cli": "npx --yes @gromlab/svg-sprites --mode <mode-key> <sprite-directory>"
}
}
```
Генерация через `npx` не добавляет package в проект. Не придумывай сокращённые или generic mode keys и не используй удалённый `legacy`: выбери один полный key из таблицы. Bare `standalone` выбирай только когда приложение само публикует SVG, а `standalone@server` — только для централизованного release, используемого во время генерации consumers. Для нескольких спрайтов создай отдельную команду для каждого config-файла или каталога.
## Инспекция проекта
До изменений установи фактический контракт проекта:
1. Прочитай корневой `package.json`, lock-файл и workspace-конфигурацию; определи framework, bundler и существующие команды.
2. Найди config-файлы, команды `svg-sprites` и импорты generated-компонентов. Имя конфига произвольное; ориентируйся на переданный CLI путь и поля объекта.
3. Определи framework, router при его наличии и фактический bundler по scripts и конфигу. Для Next.js отдельно определи App/Pages Router и сборщик реальных `dev`/`build` команд.
4. Проверь существующие `predev`, `prebuild`, `pretypecheck` и агрегирующие scripts. Не перезаписывай их.
5. Для нового спрайта выбери целевой каталог, не навязывая конкретный слой или архитектуру приложения.
6. Проверь TypeScript и alias-настройки. Для package subpath exports нужен TypeScript 5+ с `moduleResolution: 'bundler'`, `'node16'` или `'nodenext'`.
Для обычного local consumer все input-пути считаются относительно каталога, содержащего явно переданный config-файл; в config-less режиме — относительно переданного каталога. Проверяй local `input` по этому контракту:
- `input?: string | string[]` по умолчанию равен `./icons`;
- каждая строка задаёт папку, точный SVG-файл или glob;
- папка сканируется плоско; вложенные файлы включаются только явным recursive glob, например `./icons/**/*.svg`;
- массив объединяет positive-источники, а элемент с префиксом `!` исключает свои совпадения из общего набора;
- каждый positive-источник должен разрешаться хотя бы в один SVG, поэтому отсутствующая или пустая папка, glob без совпадений, отсутствующий файл или точный путь не к SVG являются ошибкой;
- разрешённые файлы дедуплицируются и детерминированно сортируются;
- разные файлы с одинаковым basename конфликтуют, даже если получены из разных источников.
До применения этих правил выбери нужную ветку:
- `standalone@server` может объединять local strings и HTTP(S) descriptors `{ name, url, sha256? }`; `name` задаёт публичное имя иконки, а необязательный `sha256` проверяет скачанные байты;
- `source: 'remote'` требует ровно одну строку с local path или HTTP(S) URL manifest и не принимает source globs или descriptors;
- remote consumer config содержит только `mode`, `source` и `input`; name, description, transforms и generated notice приходят из проверенного server manifest.
Не копируй общий SVG в несколько папок: добавь его точный путь или подходящий glob в `input` каждого нужного спрайта. Используй `**/*.svg` только для намеренного рекурсивного включения.
## Настройка интеграции
Не воспроизводи настройку mode по памяти. После инспекции проекта выбери один exact mode и открой соответствующий файл из `references/docs/ru/guides/`. Используй guide как базовый рабочий контракт, затем адаптируй его к существующей структуре проекта.
Работай в таком порядке:
1. Определи каталог исходных SVG и каталог одного sprite-модуля. Один config создаёт один независимый спрайт; для нескольких наборов нужны отдельные config-файлы и уникальные `name`.
2. Сверь framework, router и bundler с exact mode. Для Next.js проверяй реальные `dev` и `build` scripts, а не только наличие `next.config.*`.
3. Предпочитай JSON-конфиг, если проекту не нужны package-типы config. TypeScript-конфиг также загружается через CLI, но установка package нужна, когда он импортирует `defineSpriteConfig` или типы.
4. Разрешай все `input` относительно каталога config-файла. Не меняй структуру SVG без необходимости: используй путь к папке, точный файл, glob или массив этих источников.
5. Добавь sprite-команду с явным путём к config. Сохрани существующие `dev`, `build`, `typecheck` и lifecycle hooks; встрой генерацию до первого процесса, импортирующего `.svg-sprite`.
6. Не запускай одну генерацию дважды через одновременный `predev` и `npm run sprites && ...`. Для нескольких спрайтов создай отдельные команды и один агрегирующий script.
7. Если приложение импортирует каталог sprite-модуля, создай пользовательский `index.ts` рядом с `.svg-sprite`; не помещай пользовательские файлы внутрь generated-каталога.
8. Выполни первую генерацию до typecheck или запуска приложения, затем проверь mode-specific output и фактический импорт компонента.
Для централизованного release открой `references/docs/ru/guides/standalone-server.md`.
Генерируй и публикуй весь каталог `.svg-sprite` атомарно. В каждом consumer сохрани
его собственный exact framework mode, укажи `source: 'remote'` и направь `input` на
manifest. Не копируй server files во framework output и не загружай manifest из
runtime приложения.
Не добавляй Viewer автоматически. Подключай его только по запросу пользователя или когда нужна визуальная проверка набора, цветов либо сложных SVG. Способ изоляции Viewer от production бери из exact guide: frameworks, bundlers и routers используют разные границы.
Не копируй snippets между exact modes даже при похожем API. Различаются asset URL, generated-файлы, CSS handling, router boundary и способ подключения debug-инструментов.
## Контракт generated-каталога
Например, после генерации React/Next-каталог имеет следующий вид:
```text
svg-sprite/
├── icons/ # пользовательские исходники
├── svg-sprite.config.json # рекомендуемое имя конфига
├── index.ts # необязательный пользовательский barrel
├── .gitignore # управляет генератор
└── .svg-sprite/
├── index.js
├── index.d.ts
├── icon-data.js
├── icon-data.d.ts
├── sprite.svg
├── svg-sprite.manifest.js
├── svg-sprite.manifest.d.ts
└── react/
├── react-component.js
├── react-component.d.ts
└── react-component.module.css
```
Standalone не создаёт `react/`. Bare `standalone` генерирует `sprite.svg` и `svg-sprite.manifest.json`; `standalone@vite`/`standalone@webpack` дополнительно генерируют `index.*`, `icon-data.*` и resolved manifest. Их `index.*` также содержит нативный generated Web Component; bare `standalone` не получает JS runtime и не создаёт `.gitignore`.
`standalone@server` генерирует `sprite.<content-hash>.svg`,
`sprite-root-viewbox.<content-hash>.svg` и `svg-sprite.manifest.json`. У него нет
consumer facade, browser runtime, Viewer entry или `.gitignore`. Manifest хранит
relative URL обоих profiles, полные SHA-256, размеры в байтах, metadata иконок и
настройки transforms.
Редактируй исходные SVG, config-файл и пользовательский `index.ts`. Не изменяй вручную содержимое `.svg-sprite`: повторная генерация его перезапишет. Во всех modes, кроме bare `standalone`, generated `.gitignore` также находится под управлением генератора. Для импорта из корня sprite-модуля создай barrel:
```ts
export * from './.svg-sprite/index.js'
```
Генератор полностью владеет каталогом `.svg-sprite` и заменяет его при каждом запуске. Никогда не помещай туда пользовательские файлы. Генератор также владеет `.gitignore`, когда выбранный mode его создаёт. Bare `standalone` сохраняет пользовательский `.gitignore`, но удаляет управляемый `.gitignore`, оставшийся после другого mode. Generated-пути не должны содержать symlink.
Каждый exact-mode adapter владеет facade, framework-каталогом, runtime нативного компонента, declarations, manifest source, styles и asset URL. React/Next используют `react/`; остальные framework modes используют собственный generated-контракт из соответствующего guide. Standalone bundler modes экспортируют Web Component helpers и типы, а bare `standalone` не создаёт facade. Manifest declarations объявляют типы локально и не импортируют generator package.
В bundler modes спрайт остаётся отдельным asset, а SVG path-данные не встраиваются в JavaScript. Content hash зависит от настроек сборщика. Bare `standalone` создаёт файл с фиксированным именем, а приложение само определяет его публичное имя и версионирование:
- Vite-based adapters используют mode-owned static asset import, сохраняющий sprite внешним;
- `standalone@vite` использует тот же Vite asset-механизм и экспортирует href helper и нативный Web Component без React;
- `standalone@webpack` использует Webpack Asset Modules и экспортирует такой же mode-local Web Component без React;
- Webpack-based adapters и все Next modes используют adapter-owned механизм внешнего asset, обычно `new URL(..., import.meta.url).href`;
- кастомный Webpack SVG loader не должен перехватывать generated `sprite.svg`;
- в Next mode generated-компонент не содержит `'use client'` и работает в Server Components, SSR и SSG; не добавляй клиентскую границу только ради иконки;
- команда сборки Next и mode key должны совпадать: Turbopack с `.../turbopack`, Webpack с `.../webpack`.
- remote consumers всё равно публикуются через локальный asset pipeline своего adapter; не сохраняй и не собирай URL server profile в generated application code.
Для bundler modes не перемещай generated sprite в `public` и не переписывай URL вручную. Для bare `standalone` не перемещай managed original: приложение может явно копировать его в deploy output и само отвечает за публичный URL и очистку копии. При смене mode перегенерируй спрайт с новым полным key.
## Использование, доступность и цвета
Имя компонента зависит от `name` конкретного спрайта. В `standalone@vite` и `standalone@webpack` значение `name: 'file-manager'` создаёт tag `<file-manager-icon>` и функцию `defineFileManagerIconElement()`:
```ts
import { defineFileManagerIconElement } from './svg-sprite'
defineFileManagerIconElement()
```
```html
<file-manager-icon icon="folder" aria-hidden="true"></file-manager-icon>
```
Нативный элемент не имеет runtime-зависимостей, сам выбирает generated ID и `viewBox`, получает URL через bundler и рендерит `<svg><use>` в Shadow DOM. Его property `icon` типизирован точным union имён, но строковые HTML attributes проверяются только в runtime. Размер по умолчанию равен `1em × 1em`; меняй его через CSS на host. Bare `standalone` Web Component не генерирует.
В component modes тот же `name: 'file-manager'` создаёт нативный компонент `FileManagerIcon`; его синтаксис и props определяет exact-mode guide. В React/Next.js значение `name: 'navigation'` создаёт `NavigationIcon`.
Импортируй компонент из корня соответствующего каталога спрайта. `width` и `height` не обязательны: размером можно управлять обычным CSS-классом.
```tsx
import { FileManagerIcon } from './svg-sprite'
export const OpenButton = () => (
<button type="button">
<FileManagerIcon icon="folder" className="icon" aria-hidden="true" />
<span>Открыть</span>
</button>
)
```
```css
.icon {
width: 24px;
height: 24px;
color: #4b5563;
}
```
`icon` принимает точные имена исходных файлов без `.svg`; неизвестное имя является ошибкой TypeScript. Для небезопасных SVG ID имён генератор хранит публичное имя, но создаёт внутренний стабильный hash ID, поэтому не собирай fragment URL из имени вручную.
По умолчанию компонент рендерит `<svg>` и принимает стандартные SVG attributes: необязательные `width`/`height`, `className`, `style`, `role`, `aria-*` и обработчики. С `wrapped={true}` корнем становится `<span>`, props относятся к span, а внутренний SVG занимает размер wrapper.
Generated-компонент не выбирает семантику за приложение и не добавляет `title`. Для декоративной иконки передай `aria-hidden="true"`; для самостоятельной смысловой иконки передай `role="img"` и доступное имя через `aria-label`. Не дублируй имя, если соседний текст уже озвучивает действие. Интерактивность размещай на `button` или `a`, а не на самой иконке.
Трансформации `removeSize`, `replaceColors` и `addTransition` включены по умолчанию. Для монохромной иконки единственный цвет получает fallback `currentColor`, поэтому управляй CSS-свойством `color`. Для многоцветной передавай типизированные custom properties:
```tsx
<FileManagerIcon
icon="folder"
style={{
'--icon-color-1': '#4b5563',
'--icon-color-2': '#14b8a6',
}}
/>
```
Автозамена рассчитана на `fill`/`stroke` attributes и inline `style`. Значения `none`, `transparent`, `inherit`, `unset`, `initial` не заменяются. CSS-классы и внешние stylesheets, gradients, patterns, filters и `url(#...)` проверяй на реальном результате. Переменные страницы работают через `<svg><use>`, но не проникают во внешний документ при `<img>` или `background-image`; CSS mask оставляет только одноцветный силуэт.
`SpriteViewer` необязателен. Установи `@gromlab/svg-sprites` как development dependency, только если проекту нужен Viewer. Он принимает manifests или статически обнаружимые loaders, показывает поиск, темы, цвета и примеры, но production-компоненты от него не зависят.
Перед подключением Viewer открой exact guide. Frameworks, bundlers и routers требуют разных debug entries или client boundaries. Не переноси способ подключения между modes.
## Проверка результата
После изменения конфига или SVG выполни обязательные проверки:
1. Запусти точную sprite-команду. Процесс должен завершиться с кодом `0` и сообщить имя, число иконок, mode и каталог `.svg-sprite`.
2. Проверь output выбранного exact mode:
- bare `standalone` создаёт `sprite.svg` и `svg-sprite.manifest.json`;
- `standalone@server` создаёт два content-addressed SVG profiles и server manifest, hashes и relative paths которого соответствуют этим файлам;
- `standalone@vite` и `standalone@webpack` дополнительно создают `index.*`, `icon-data.*` и JS manifest, но не каталог `react/`;
- framework modes также создают adapter-owned runtime нативного компонента, declaration и styles.
3. Для modes с public facade проверь `.svg-sprite/index.js`, соседний `index.d.ts`, список имён и фактический импорт через пользовательский barrel.
4. Проверь manifest: mode и target должны соответствовать выбранному adapter, а список иконок — исходным SVG. В bundler modes URL должен формироваться mode-specific способом; bare JSON manifest намеренно не содержит публичного `spriteUrl`.
5. Запусти существующий typecheck проекта, если mode создаёт типы или изменился пользовательский TypeScript-код.
6. Запусти минимальную команду приложения, затронутую изменением: `dev`, build или специализированную проверку проекта.
Не запускай полную production-сборку только ради проверки нового имени иконки. Она нужна, если менялся bundler target, router, Webpack loader, asset URL, deployment path или диагностируется production-only ошибка.
Визуальную проверку, Network и accessibility tree выполняй только при наличии запущенного приложения и браузерных инструментов. Если таких инструментов нет, не утверждай, что цвета, темы, доступность или HTTP-ответ asset проверены; явно укажи непроверенную часть.
Viewer используй для сложных цветов, transforms и массовой визуальной проверки. Не добавляй debug route ради обычной генерации одного спрайта.
## Диагностика
Сопоставь симптом с проверкой и исправляй первопричину:
| Симптом | Вероятная причина | Действие |
|---|---|---|
| `Missing sprite config file or module directory` | Не передан позиционный путь | Передай один config-файл либо каталог для config-less запуска. |
| `Expected one config file or module directory` | Передано несколько путей | Создай отдельную команду на каждый спрайт и объедини scripts. |
| `Sprite mode is required` | Mode отсутствует и в config, и в CLI | Добавь `mode` в объект или передай полный `--mode`. |
| `Unsupported sprite config extension` | Передан файл не `.ts`, `.js` или `.json` | Используй поддерживаемый формат config-файла. |
| Positive input-источник не нашёл SVG | Папка отсутствует или пуста, glob не совпал либо точный путь отсутствует или ведёт не к SVG | Разреши источник от каталога конфига и исправь `input`; каждый positive-элемент должен дать хотя бы один SVG. |
| Иконки из подпапки не появились | От папки ожидалось рекурсивное сканирование | Используй явный glob, например `./icons/**/*.svg`; папки сканируются плоско. |
| Исключённая иконка всё ещё присутствует | У исключения нет префикса `!`, оно находится не в массиве `input` или считается не от того каталога | Добавь совпадающий `!`-элемент и считай его от каталога конфига. |
| CLI выбрал не все источники | Несколько источников поместили в одно значение `--input` или пропустили option | Повтори `--input <path-or-glob>` отдельно для каждого источника или исключения. |
| Конфликт имени иконки или SVG ID | Два разных файла имеют одинаковый basename либо hash-ID столкнулся с именем | Переименуй один исходный SVG; не выбирай файл неявно. |
| `Refusing to overwrite a user file` | В корне sprite-модуля уже есть пользовательский `.gitignore`, который mode должен создать | Не перезаписывай файл: выбери другой sprite-каталог или согласуй перенос существующего `.gitignore`. |
| Нет `.svg-sprite/index.js` или имя отсутствует в autocomplete | Для bare `standalone` это ожидаемо; в остальных modes генерация не запускалась, barrel неверен либо type server держит старый модуль | Сверь exact mode, запусти sprite-команду, проверь `export * from './.svg-sprite/index.js'`, затем typecheck; при необходимости перезапусти TypeScript server. |
| SVG не загружается или URL неверен | Mode не совпадает со сборщиком, неверен Webpack `publicPath` либо кастомный loader перехватил asset | Сверь mode и build-команду, проверь Asset Modules/`publicPath`, исключи generated SVG из несовместимого loader. |
| Next build расходится между SSR и браузером | Модуль сгенерирован для другого bundler/router или URL переписан вручную | Верни generated `new URL(...)`, выбери точный Next mode и перегенерируй. |
| `color` не меняет многоцветную иконку | У иконки несколько переменных или она показана через `<img>`/CSS background | Используй `<FileManagerIcon>`/`<svg><use>` и нужные `--icon-color-N`. |
| Gradient/filter выглядит неверно | Автозамена цветов не гарантирует сложные paint servers | Изучи generated SVG; при необходимости отключи `replaceColors` для спрайта или упрости источник. |
| Viewer пуст | Manifest не создан, loader не обнаружен сборщиком или неверна Client Component boundary | Сначала сгенерируй спрайт, затем сверь manifest import и способ подключения с exact guide; в App Router оставь `'use client'` только в компоненте Viewer. |
| Remote manifest отклонён | Это не schema `standalone@server`, profile path небезопасен или metadata противоречивы | Опубликуй неизменённый полный server release и направь `input` на его JSON manifest. |
| Не прошла integrity-проверка remote sprite | SVG устарел, обрезан или изменён отдельно от manifest | Атомарно переопубликуй manifest и оба content-addressed profiles; никогда не перезаписывай hashed SVG другими байтами. |
При неизвестной ошибке зафиксируй полную CLI-команду, mode, путь к config-файлу или каталогу и первый stack/error message. Затем минимально воспроизведи проблему на одном спрайте, не удаляя пользовательские файлы и управляемый `.gitignore`.
## Карта reference-документации
References являются частью собранного skill. Открывай только документы, относящиеся к текущей задаче, но перед изменением интеграции exact-mode guide обязателен.
### Обзор
- [README пакета](./references/README_RU.md) — возможности, основной React/Next.js пример, все поддерживаемые families и ссылки на документацию.
### Конфигурация
- [Конфигурация](./references/docs/ru/configuration.md) — JSON, JavaScript, TypeScript, поля config, `input` и запуск CLI.
### Exact-mode guides
- [`standalone`](./references/docs/ru/guides/standalone.md) — static HTML и собственная публикация SVG.
- [`standalone@vite`](./references/docs/ru/guides/standalone-vite.md) — vanilla-приложение с Vite и Web Component.
- [`standalone@webpack`](./references/docs/ru/guides/standalone-webpack.md) — vanilla-приложение с Webpack 5 и Web Component.
- [`standalone@server`](./references/docs/ru/guides/standalone-server.md) — централизованный content-addressed release и remote consumers.
- [`react@vite`](./references/docs/ru/guides/react-vite.md) — React с Vite.
- [`react@webpack`](./references/docs/ru/guides/react-webpack.md) — React с Webpack 5.
- [`vue@vite`](./references/docs/ru/guides/vue-vite.md) — Vue с Vite.
- [`vue@webpack`](./references/docs/ru/guides/vue-webpack.md) — Vue с Webpack.
- [`nuxt@vite`](./references/docs/ru/guides/nuxt-vite.md) — Nuxt с Vite.
- [`nuxt@webpack`](./references/docs/ru/guides/nuxt-webpack.md) — Nuxt с Webpack.
- [`svelte@vite`](./references/docs/ru/guides/svelte-vite.md) — Svelte с Vite.
- [`svelte@webpack`](./references/docs/ru/guides/svelte-webpack.md) — Svelte с Webpack.
- [`sveltekit@vite`](./references/docs/ru/guides/sveltekit-vite.md) — SvelteKit с Vite.
- [`angular@application`](./references/docs/ru/guides/angular-application.md) — Angular application builder.
- [`angular@webpack`](./references/docs/ru/guides/angular-webpack.md) — Angular с Webpack.
- [`astro@vite`](./references/docs/ru/guides/astro-vite.md) — Astro с Vite.
- [`solid@vite`](./references/docs/ru/guides/solid-vite.md) — Solid с Vite.
- [`solid@webpack`](./references/docs/ru/guides/solid-webpack.md) — Solid с Webpack.
- [`solid-start@vite`](./references/docs/ru/guides/solid-start-vite.md) — SolidStart с Vite.
- [`preact@vite`](./references/docs/ru/guides/preact-vite.md) — Preact с Vite.
- [`preact@webpack`](./references/docs/ru/guides/preact-webpack.md) — Preact с Webpack.
- [`qwik@vite`](./references/docs/ru/guides/qwik-vite.md) — Qwik с Vite.
- [`lit@vite`](./references/docs/ru/guides/lit-vite.md) — Lit с Vite.
- [`lit@webpack`](./references/docs/ru/guides/lit-webpack.md) — Lit с Webpack.
- [`alpine@vite`](./references/docs/ru/guides/alpine-vite.md) — Alpine.js с Vite.
- [`alpine@webpack`](./references/docs/ru/guides/alpine-webpack.md) — Alpine.js с Webpack.
- [`next@app/turbopack`](./references/docs/ru/guides/next-app-turbopack.md) — Next.js App Router с Turbopack.
- [`next@app/webpack`](./references/docs/ru/guides/next-app-webpack.md) — Next.js App Router с Webpack.
- [`next@pages/turbopack`](./references/docs/ru/guides/next-pages-turbopack.md) — Next.js Pages Router с Turbopack.
- [`next@pages/webpack`](./references/docs/ru/guides/next-pages-webpack.md) — Next.js Pages Router с Webpack.
### Технические справочники
- [Технический справочник](./references/docs/ru/reference/technical.md) — requirements, CLI, naming, generated API, assets, transforms, цвета, Viewer, Git, CI и диагностика.
- [Программный API](./references/docs/ru/reference/programmatic-api.md) — `generateSprite`, overrides, config API, compiler и Viewer runtime.
### Agent-specific reference
- [Сложные SVG](./references/complex-svg.md) — gradients, patterns, filters, masks, `url(#...)`, `viewBox`, fragment IDs и визуальная диагностика.
@@ -0,0 +1,176 @@
# Сложные SVG: диагностика и безопасная генерация
## Когда открывать
Открывай этот документ, если исходник содержит `<defs>`, gradients, patterns, filters, masks, clip paths, внутренние `<style>`/classes, `url(#id)`, CSS variables, `<use>`, text, нестандартный `viewBox`, пробелы в имени файла или визуально меняется после генерации. Также открывай его при жалобах на цвет, размер, обрезание или конфликт fragment ID.
## Сначала классифицируй риск
Проверь исходный SVG до редактирования:
```bash
npm run sprite:file-manager
```
Используй фактический package script нужного спрайта. Затем сравни source с `.svg-sprite/sprite.svg` и manifest, не делая вывод только по успешному exit code.
Особого внимания требуют:
- `fill="url(#gradient)"`, `stroke="url(#pattern)"`;
- `filter="url(#shadow)"`, `mask="url(#mask)"`, `clip-path="url(#clip)"`;
- CSS rules внутри `<style>` и внешние stylesheets;
- цвета через classes, presentation attributes и inline `style` одновременно;
- `currentColor`, уже существующие `var(...)`, `context-fill` и `context-stroke`;
- повторяющиеся IDs в `<defs>` разных файлов;
- SVG без `viewBox` или с width/height, не соответствующими viewBox;
- embedded images, fonts, scripts или external references.
## Фактический pipeline
Компилятор сначала применяет SVGO `preset-default`, сохраняя `viewBox`, затем custom transforms в таком порядке:
1. `removeSize` удаляет `width` и `height` с корневого `<svg>`.
2. `replaceColors` собирает значения `fill` и `stroke` из attributes и inline `style`, затем заменяет их на `var(--icon-color-N, fallback)`.
3. `addTransition` добавляет inline transition цветным `path`, `circle`, `ellipse`, `rect`, `line`, `polyline`, `polygon`, `text`, `tspan` и `use`.
Все три опции по умолчанию `true` и применяются ко всему спрайту, не к отдельной иконке.
```ts
import { defineSpriteConfig } from '@gromlab/svg-sprites'
export default defineSpriteConfig({
mode: 'react@vite',
name: 'illustrations',
transform: {
removeSize: false,
replaceColors: false,
addTransition: false,
},
})
```
Это config для одного из потенциально многих sprite-модулей; его каталог не обязан совпадать с module/feature-каталогом. Для Next укажи соответствующий полный `mode` с тем же `transform`.
## Размеры и viewBox
`removeSize: true` удаляет intrinsic `width`/`height`, но не создаёт отсутствующий `viewBox`. Если source не имеет корректного `viewBox`, generated icon может получить неверное масштабирование или нулевую область просмотра.
Правильная подготовка source:
```svg
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="..." />
</svg>
```
Если физические размеры являются частью контракта иллюстрации, установи `removeSize: false` и проверь поведение component props. Не используй сохранение width/height как замену отсутствующему viewBox.
React compile оставляет root sprite `rootViewBox` выключенным; Next включает его. У каждой shape всё равно должен быть собственный корректный viewBox, который попадает в manifest и используется Viewer.
## Цвета
Для одного обнаруженного цвета fallback становится `currentColor`:
```svg
stroke="var(--icon-color-1, currentColor)"
```
Для нескольких цветов сохраняются исходные fallbacks:
```svg
fill="var(--icon-color-1, #798198)"
fill="var(--icon-color-2, #ffffff)"
```
Значения `none`, `transparent`, `inherit`, `unset` и `initial` не заменяются. Сравнение цветов нормализует регистр и пробелы, но не приводит эквивалентные формы (`#fff`, `#ffffff`, `rgb(...)`) к одному цвету.
Автоматический анализ надёжен прежде всего для `fill`/`stroke` attributes и inline `style`. Он не разбирает CSS selectors во внутреннем `<style>` и внешний stylesheet как полноценный CSS AST.
Для `url(#...)`, уже вложенных `var(...)`, gradients и patterns автоматическая замена требует проверки generated output. Если ссылка на paint server изменилась или Viewer неверно показывает controls, отключи `replaceColors` для всего этого спрайта:
```ts
transform: {
replaceColors: false,
}
```
Если рядом нужны обычные recolorable icons, вынеси сложные иллюстрации в отдельный sprite с отдельным config. Это предпочтительнее ручной правки generated SVG.
`addTransition` независим от `replaceColors`. При сохранении исходных цветов transition всё равно может добавиться. Для filters, анимаций или собственного CSS отключай обе опции, если inline transition меняет поведение.
## Defs, references и IDs
После SVGO и сборки проверь, что каждая ссылка `url(#id)` или `<use href="#id">` указывает на реально существующий ID внутри соответствующей shape. Не предполагай, что IDs останутся буквальной копией source: optimizer/compiler может их изменить.
Проверяй как минимум:
- gradient/pattern применяется к нужному path;
- filter region не обрезает blur/shadow;
- mask и clipPath сохраняют coordinate system (`userSpaceOnUse`/`objectBoundingBox`);
- internal `<use>` не спутан с внешним fragment спрайта;
- одинаковые IDs из разных source SVG не создают cross-icon collision в итоговом документе;
- external file/URL references допустимы в production CSP и deployment.
Если IDs конфликтуют, сначала сделай source IDs уникальными и обнови все ссылки внутри SVG. Не правь compiled sprite.
## Имена файлов и внешний fragment
`FileManagerIcon` в примерах ниже — только пример generated-имени для отдельного config с `name: 'file-manager'`; это не фиксированное имя API.
Безопасный basename соответствует:
```text
^[a-zA-Z][a-zA-Z0-9_-]*$
```
Он сохраняется как fragment ID. Остальные имена, например `folder open.svg` или `24-check.svg`, остаются публичными значениями TypeScript `icon`, но получают стабильный ID `icon-<16 hex>`.
```tsx
<FileManagerIcon icon="folder open" />
```
Не создавай вручную `#folder open`. Используй generated component либо `.svg-sprite/svg-sprite.manifest.js`, где записаны `name` и фактический `id`.
Разные файлы с одинаковым basename запрещены даже из разных directories. Переименуй один source осмысленно; порядок или пересечение источников не выбирают победителя.
## Способ отображения
Для управления `color` и `--icon-color-N` используй generated React-компонент или `<svg><use>`:
```tsx
<FileManagerIcon
icon="diagram"
style={{
'--icon-color-1': '#334155',
'--icon-color-2': '#38bdf8',
}}
/>
```
Generated style type допускает `--icon-color-${number}`. `<img>` и CSS `background-image` загружают SVG как изолированный document, поэтому variables страницы внутрь не передаются. CSS mask оставляет только силуэт и теряет gradients, filters и различия цветов.
External stack fragment support и поведение paint servers могут различаться между browsers. Для критичной сложной графики при диагностике runtime и наличии browser-инструментов проверь целевые browsers; при несовместимости SVG sprite может быть неподходящим способом доставки именно этой иллюстрации.
## Обязательная проверка
1. Запусти генерацию с правильным mode.
2. Запусти typecheck проекта.
3. Открой generated sprite и найди shape по ID из manifest.
4. Статически сверь `viewBox`, IDs, `url(#...)`, colors и inline styles.
5. Если менялись target/pipeline или диагностируется runtime, собери production bundle и проверь внешний hashed SVG.
6. При наличии SpriteViewer и визуальных инструментов проверь default colors и каждую `--icon-color-N` отдельно.
7. При наличии browser-инструментов и соответствующем runtime-риске проверь SSR/hydration для Next.js и целевые browsers для external fragments.
8. Не утверждай визуальную или a11y эквивалентность source и результата без доступных инструментов и фактического сравнения.
## Типовые симптомы и действия
- Иконка стала полностью `currentColor`: pipeline увидел один цвет. Если исходная семантика сложнее, отключи `replaceColors` или нормализуй source attributes.
- Gradient исчез: проверь, не преобразован ли `fill="url(#...)"`, существует ли target ID и не конфликтует ли он с другим icon.
- Shadow обрезан: проверь filter region и viewBox; `removeSize` сам по себе не расширяет область.
- Цветовые controls Viewer отсутствуют: цвет задан через class/stylesheet либо `replaceColors: false`; это ожидаемо.
- Transition дублируется или мешает animation: существующий inline `transition` не перезаписывается, но generated CSS также добавляет transitions; отключи `addTransition` для sprite.
- `<img>` игнорирует variables: смени rendering на `<svg><use>`/generated component, не пытайся передать page variables в изолированный SVG.
- Ручной fragment не работает для имени с пробелом: используй ID из manifest.
- Один сложный icon требует иных transforms: вынеси его в отдельный sprite; per-icon transform config отсутствует.
Для mode-specific запуска и проверки вернись к exact-mode guide, выбранному в основном `SKILL.md`.