@sparknest/ui-icons-vue 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +23 -3
- package/src/index.mjs +113 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparknest/ui-icons-vue",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Shared SVG-to-Vue icon source generator for Cedar and Maple",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"svgo": "^3.3.2"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"sync:workspace-icons": "node scripts/sync-workspace-icons.mjs"
|
|
25
|
+
}
|
|
6
26
|
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, dirname, resolve } from 'node:path'
|
|
3
|
+
import { optimize } from 'svgo'
|
|
4
|
+
|
|
5
|
+
const pascalCase = (value) => value.split(/[-_\s]+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
|
|
6
|
+
|
|
7
|
+
async function svgFiles(root) {
|
|
8
|
+
const entries = await readdir(root, { withFileTypes: true })
|
|
9
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
10
|
+
const path = resolve(root, entry.name)
|
|
11
|
+
return entry.isDirectory() ? svgFiles(path) : path.endsWith('.svg') ? [path] : []
|
|
12
|
+
}))).flat().sort()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function componentSource({ classPrefix, runtimeName, svg }) {
|
|
16
|
+
return `<template>\n ${svg}\n</template>\n\n<script setup lang="ts">\nimport { computed, type CSSProperties } from 'vue'\n\ndefineOptions({ name: '${runtimeName}' })\n\nconst props = withDefaults(defineProps<{\n size?: string | number\n color?: string\n rotate?: number\n spin?: boolean\n title?: string\n strokeWidth?: number\n strokeLinecap?: 'butt' | 'round' | 'square'\n strokeLinejoin?: 'bevel' | 'inherit' | 'miter' | 'round'\n}>(), {\n strokeWidth: 4,\n strokeLinecap: 'butt',\n strokeLinejoin: 'miter',\n})\nconst emit = defineEmits<{ click: [event: MouseEvent] }>()\nconst numericSizeRE = /^-?(?:\\d+|\\d*\\.\\d+)$/\nconst style = computed<CSSProperties>(() => ({\n width: '1em',\n height: '1em',\n flexShrink: 0,\n color: props.color,\n fontSize: typeof props.size === 'number' || numericSizeRE.test(props.size?.trim() ?? '') ? \`\${props.size}px\` : props.size,\n transform: props.rotate ? \`rotate(\${props.rotate}deg)\` : undefined,\n}))\n</script>\n`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function componentIndex({ componentName, fileName }) {
|
|
20
|
+
return `import type { App } from 'vue'\nimport _${componentName} from './${fileName}.vue'\n\nexport const ${componentName} = Object.assign(_${componentName}, {\n install(app: App) { app.component(_${componentName}.name, _${componentName}) },\n})\n`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function generateVueIcons({
|
|
24
|
+
svgRoot,
|
|
25
|
+
sourceRoot,
|
|
26
|
+
exportPrefix = '',
|
|
27
|
+
runtimePrefix = '',
|
|
28
|
+
classPrefix = 'ui-icon',
|
|
29
|
+
check = false,
|
|
30
|
+
}) {
|
|
31
|
+
const expected = new Map()
|
|
32
|
+
expected.set('../style.css', `svg.${classPrefix}--spin {
|
|
33
|
+
animation: ${classPrefix}-spin var(--${classPrefix.replace(/-svg-icon$/, '')}-icon-spin-duration, 1s) infinite linear;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@keyframes ${classPrefix}-spin {
|
|
37
|
+
from { rotate: 0deg; }
|
|
38
|
+
to { rotate: 360deg; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (prefers-reduced-motion: reduce) {
|
|
42
|
+
svg.${classPrefix}--spin { animation: none; }
|
|
43
|
+
}
|
|
44
|
+
`)
|
|
45
|
+
const icons = []
|
|
46
|
+
const names = new Map()
|
|
47
|
+
|
|
48
|
+
for (const path of await svgFiles(svgRoot)) {
|
|
49
|
+
const source = await readFile(path, 'utf8')
|
|
50
|
+
const rawName = basename(path, '.svg')
|
|
51
|
+
const preserveColor = rawName.endsWith('-color')
|
|
52
|
+
const normalizedSource = source.replace(/<svg\b[^>]*>/i, (root) => {
|
|
53
|
+
const withoutStrokeDefaults = root.replace(
|
|
54
|
+
/\s+stroke-(?:width|linecap|linejoin)=(?:"[^"]*"|'[^']*')/gi,
|
|
55
|
+
'',
|
|
56
|
+
)
|
|
57
|
+
return preserveColor
|
|
58
|
+
? withoutStrokeDefaults
|
|
59
|
+
: withoutStrokeDefaults.replace(/\s+fill=(?:"[^"]*"|'[^']*')/gi, '')
|
|
60
|
+
})
|
|
61
|
+
const rootUsesNoFill = /<svg\b[^>]*\bfill=["']none["']/i.test(source)
|
|
62
|
+
const fileName = `icon-${rawName}`
|
|
63
|
+
const componentName = `${exportPrefix}${pascalCase(rawName)}`
|
|
64
|
+
const runtimeName = `${runtimePrefix}${componentName}`
|
|
65
|
+
if (names.has(componentName)) throw new Error(`Duplicate icon ${componentName}: ${names.get(componentName)} and ${path}`)
|
|
66
|
+
names.set(componentName, path)
|
|
67
|
+
const result = optimize(normalizedSource, {
|
|
68
|
+
path,
|
|
69
|
+
plugins: [
|
|
70
|
+
{ name: 'preset-default', params: { overrides: { removeViewBox: false } } },
|
|
71
|
+
'removeDimensions',
|
|
72
|
+
{ name: 'removeAttrs', params: { attrs: preserveColor ? ['class', 'style'] : ['class', 'style', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'] } },
|
|
73
|
+
{ name: 'addAttributesToSVGElement', params: { attributes: [
|
|
74
|
+
{ ':class': `['${classPrefix}', { '${classPrefix}--spin': props.spin }]` },
|
|
75
|
+
{ ':style': 'style' }, ...(preserveColor ? [] : [{ fill: rootUsesNoFill ? 'none' : 'currentColor' }]), { ':aria-hidden': '!props.title' },
|
|
76
|
+
{ ':stroke-width': 'props.strokeWidth' }, { ':stroke-linecap': 'props.strokeLinecap' },
|
|
77
|
+
{ ':stroke-linejoin': 'props.strokeLinejoin' }, { role: 'img' }, { '@click': `emit('click', $event)` },
|
|
78
|
+
] } },
|
|
79
|
+
],
|
|
80
|
+
})
|
|
81
|
+
const svg = result.data.replace(/<svg([^>]*)>/, `<svg$1><title v-if="props.title">{{ props.title }}</title>`)
|
|
82
|
+
icons.push({ fileName, componentName })
|
|
83
|
+
expected.set(`${fileName}/${fileName}.vue`, componentSource({ classPrefix, runtimeName, svg }))
|
|
84
|
+
expected.set(`${fileName}/index.ts`, componentIndex({ componentName, fileName }))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
expected.set('index.ts', `export const iconNames = ${JSON.stringify(icons.map(({ fileName }) => fileName.slice(5)))} as const\n${icons.map(({ fileName, componentName }) => `export { ${componentName} } from './${fileName}'`).join('\n')}\n`)
|
|
88
|
+
expected.set('icons.json', `${JSON.stringify(icons, null, 2)}\n`)
|
|
89
|
+
const stale = []
|
|
90
|
+
for (const [target, content] of expected) {
|
|
91
|
+
const path = resolve(sourceRoot, target)
|
|
92
|
+
if (check) {
|
|
93
|
+
try { if (await readFile(path, 'utf8') !== content) stale.push(target) } catch { stale.push(target) }
|
|
94
|
+
} else {
|
|
95
|
+
await mkdir(dirname(path), { recursive: true })
|
|
96
|
+
await writeFile(path, content)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const expectedDirs = new Set(icons.map(({ fileName }) => fileName))
|
|
101
|
+
try {
|
|
102
|
+
for (const entry of await readdir(sourceRoot, { withFileTypes: true })) {
|
|
103
|
+
if (entry.isDirectory() && entry.name.startsWith('icon-') && !expectedDirs.has(entry.name)) {
|
|
104
|
+
if (check) stale.push(entry.name)
|
|
105
|
+
else await rm(resolve(sourceRoot, entry.name), { recursive: true, force: true })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (check || error?.code !== 'ENOENT') throw error
|
|
110
|
+
}
|
|
111
|
+
if (stale.length) throw new Error(`Generated icon sources are stale:\n${stale.join('\n')}`)
|
|
112
|
+
return { iconCount: icons.length, fileCount: expected.size }
|
|
113
|
+
}
|
package/README.md
DELETED