vectify 2.0.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/LICENSE +21 -0
- package/README.md +679 -0
- package/README.zh-CN.md +683 -0
- package/dist/chunk-4BWKFV7W.mjs +1311 -0
- package/dist/chunk-CIKTK6HI.mjs +96 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1483 -0
- package/dist/cli.mjs +56 -0
- package/dist/helpers-UPZEBRGK.mjs +26 -0
- package/dist/index.d.mts +281 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.js +1463 -0
- package/dist/index.mjs +27 -0
- package/dist/templates/angular/component.ts.hbs +121 -0
- package/dist/templates/angular/createIcon.ts.hbs +116 -0
- package/dist/templates/astro/component.astro.hbs +110 -0
- package/dist/templates/astro/createIcon.astro.hbs +111 -0
- package/dist/templates/lit/component.js.hbs +12 -0
- package/dist/templates/lit/component.ts.hbs +19 -0
- package/dist/templates/lit/createIcon.js.hbs +98 -0
- package/dist/templates/lit/createIcon.ts.hbs +99 -0
- package/dist/templates/preact/component.jsx.hbs +8 -0
- package/dist/templates/preact/component.tsx.hbs +11 -0
- package/dist/templates/preact/createIcon.jsx.hbs +101 -0
- package/dist/templates/preact/createIcon.tsx.hbs +121 -0
- package/dist/templates/qwik/component.jsx.hbs +7 -0
- package/dist/templates/qwik/component.tsx.hbs +8 -0
- package/dist/templates/qwik/createIcon.jsx.hbs +100 -0
- package/dist/templates/qwik/createIcon.tsx.hbs +111 -0
- package/dist/templates/react/component.jsx.hbs +8 -0
- package/dist/templates/react/component.tsx.hbs +11 -0
- package/dist/templates/react/createIcon.jsx.hbs +100 -0
- package/dist/templates/react/createIcon.tsx.hbs +117 -0
- package/dist/templates/solid/component.tsx.hbs +10 -0
- package/dist/templates/solid/createIcon.jsx.hbs +127 -0
- package/dist/templates/solid/createIcon.tsx.hbs +139 -0
- package/dist/templates/svelte/component.js.svelte.hbs +9 -0
- package/dist/templates/svelte/component.ts.svelte.hbs +10 -0
- package/dist/templates/svelte/icon.js.svelte.hbs +123 -0
- package/dist/templates/svelte/icon.ts.svelte.hbs +124 -0
- package/dist/templates/template-engine.ts +107 -0
- package/dist/templates/vanilla/component.ts.hbs +8 -0
- package/dist/templates/vanilla/createIcon.js.hbs +111 -0
- package/dist/templates/vanilla/createIcon.ts.hbs +124 -0
- package/dist/templates/vue/component.js.vue.hbs +21 -0
- package/dist/templates/vue/component.ts.vue.hbs +22 -0
- package/dist/templates/vue/icon.js.vue.hbs +155 -0
- package/dist/templates/vue/icon.ts.vue.hbs +157 -0
- package/package.json +78 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import Handlebars from 'handlebars'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Template data interface
|
|
8
|
+
*/
|
|
9
|
+
export interface TemplateData {
|
|
10
|
+
typescript: boolean
|
|
11
|
+
componentName?: string
|
|
12
|
+
formattedNodes?: string
|
|
13
|
+
[key: string]: any
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the templates directory path
|
|
18
|
+
* When bundled by tsup:
|
|
19
|
+
* - CJS: code is in dist/index.js, __dirname = dist/
|
|
20
|
+
* - ESM: code is in dist/chunk-*.mjs or dist/index.mjs, __dirname = dist/
|
|
21
|
+
* Templates are copied to dist/templates/
|
|
22
|
+
*/
|
|
23
|
+
function getTemplatesDir(): string {
|
|
24
|
+
// Check if __dirname is defined (CJS or bundled code)
|
|
25
|
+
if (typeof __dirname !== 'undefined') {
|
|
26
|
+
return path.join(__dirname, 'templates')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ESM fallback: use import.meta.url
|
|
30
|
+
// import.meta is only available in ESM
|
|
31
|
+
const currentFile = fileURLToPath(import.meta.url)
|
|
32
|
+
return path.join(path.dirname(currentFile), 'templates')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load and compile a Handlebars template
|
|
37
|
+
*/
|
|
38
|
+
function loadTemplate(templatePath: string): HandlebarsTemplateDelegate {
|
|
39
|
+
const templatesDir = getTemplatesDir()
|
|
40
|
+
const fullPath = path.join(templatesDir, templatePath)
|
|
41
|
+
const templateContent = fs.readFileSync(fullPath, 'utf-8')
|
|
42
|
+
return Handlebars.compile(templateContent, { noEscape: true })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render a template with data
|
|
47
|
+
*/
|
|
48
|
+
export function renderTemplate(templatePath: string, data: TemplateData): string {
|
|
49
|
+
const template = loadTemplate(templatePath)
|
|
50
|
+
return template(data)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get template path for React components
|
|
55
|
+
*/
|
|
56
|
+
export function getReactTemplatePath(typescript: boolean, type: 'component' | 'createIcon'): string {
|
|
57
|
+
const ext = typescript ? 'tsx' : 'jsx'
|
|
58
|
+
return `react/${type}.${ext}.hbs`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get template path for Vue components
|
|
63
|
+
*/
|
|
64
|
+
export function getVueTemplatePath(typescript: boolean, type: 'component' | 'icon'): string {
|
|
65
|
+
const suffix = typescript ? 'ts' : 'js'
|
|
66
|
+
return `vue/${type}.${suffix}.vue.hbs`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get template path for Svelte components
|
|
71
|
+
*/
|
|
72
|
+
export function getSvelteTemplatePath(typescript: boolean, type: 'component' | 'icon'): string {
|
|
73
|
+
const suffix = typescript ? 'ts' : 'js'
|
|
74
|
+
return `svelte/${type}.${suffix}.svelte.hbs`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get template path for Solid components
|
|
79
|
+
*/
|
|
80
|
+
export function getSolidTemplatePath(typescript: boolean, type: 'component' | 'createIcon'): string {
|
|
81
|
+
const ext = typescript ? 'tsx' : 'jsx'
|
|
82
|
+
return `solid/${type}.${ext}.hbs`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get template path for Preact components
|
|
87
|
+
*/
|
|
88
|
+
export function getPreactTemplatePath(typescript: boolean, type: 'component' | 'createIcon'): string {
|
|
89
|
+
const ext = typescript ? 'tsx' : 'jsx'
|
|
90
|
+
return `preact/${type}.${ext}.hbs`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get template path for Vanilla JS components
|
|
95
|
+
*/
|
|
96
|
+
export function getVanillaTemplatePath(typescript: boolean, type: 'component' | 'createIcon'): string {
|
|
97
|
+
const ext = typescript ? 'ts' : 'js'
|
|
98
|
+
return `vanilla/${type}.${ext}.hbs`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get template path for Qwik components
|
|
103
|
+
*/
|
|
104
|
+
export function getQwikTemplatePath(typescript: boolean, type: 'component' | 'createIcon'): string {
|
|
105
|
+
const ext = typescript ? 'tsx' : 'jsx'
|
|
106
|
+
return `qwik/${type}.${ext}.hbs`
|
|
107
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function createIcon(name, iconNode, keepColors = false) {
|
|
2
|
+
return (options = {}) => {
|
|
3
|
+
const {
|
|
4
|
+
size = 24,
|
|
5
|
+
color = 'currentColor',
|
|
6
|
+
strokeWidth = 2,
|
|
7
|
+
className = '',
|
|
8
|
+
title,
|
|
9
|
+
ariaLabel,
|
|
10
|
+
ariaHidden,
|
|
11
|
+
...attrs
|
|
12
|
+
} = options
|
|
13
|
+
|
|
14
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
15
|
+
svg.setAttribute('width', String(size))
|
|
16
|
+
svg.setAttribute('height', String(size))
|
|
17
|
+
svg.setAttribute('viewBox', '0 0 24 24')
|
|
18
|
+
svg.setAttribute('class', className ? `vectify-icon ${className}` : 'vectify-icon')
|
|
19
|
+
|
|
20
|
+
// Accessibility
|
|
21
|
+
const shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
|
|
22
|
+
if (shouldHide) {
|
|
23
|
+
svg.setAttribute('aria-hidden', 'true')
|
|
24
|
+
}
|
|
25
|
+
if (ariaLabel) {
|
|
26
|
+
svg.setAttribute('aria-label', ariaLabel)
|
|
27
|
+
}
|
|
28
|
+
if (title || ariaLabel) {
|
|
29
|
+
svg.setAttribute('role', 'img')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Set additional attributes
|
|
33
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
34
|
+
svg.setAttribute(key, String(value))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Add title if provided
|
|
38
|
+
if (title) {
|
|
39
|
+
const titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title')
|
|
40
|
+
titleEl.textContent = title
|
|
41
|
+
svg.appendChild(titleEl)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Render icon nodes
|
|
45
|
+
function renderNode(node) {
|
|
46
|
+
const [type, nodeAttrs, children] = node
|
|
47
|
+
const element = document.createElementNS('http://www.w3.org/2000/svg', type)
|
|
48
|
+
|
|
49
|
+
let cleanedAttrs
|
|
50
|
+
|
|
51
|
+
if (keepColors) {
|
|
52
|
+
cleanedAttrs = nodeAttrs
|
|
53
|
+
} else {
|
|
54
|
+
// Track color attributes
|
|
55
|
+
let hasFill = false
|
|
56
|
+
let hasStroke = false
|
|
57
|
+
let originalStrokeWidth
|
|
58
|
+
|
|
59
|
+
Object.entries(nodeAttrs).forEach(([key, value]) => {
|
|
60
|
+
if (key === 'fill' && value !== 'none') {
|
|
61
|
+
hasFill = true
|
|
62
|
+
}
|
|
63
|
+
if (key === 'stroke') {
|
|
64
|
+
hasStroke = true
|
|
65
|
+
}
|
|
66
|
+
if (key === 'strokeWidth' || key === 'stroke-width') {
|
|
67
|
+
originalStrokeWidth = value
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Keep non-color attributes
|
|
72
|
+
cleanedAttrs = Object.fromEntries(
|
|
73
|
+
Object.entries(nodeAttrs).filter(([key]) =>
|
|
74
|
+
!['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Apply color
|
|
79
|
+
if (hasFill) {
|
|
80
|
+
cleanedAttrs.fill = color
|
|
81
|
+
} else if (hasStroke) {
|
|
82
|
+
cleanedAttrs.fill = 'none'
|
|
83
|
+
cleanedAttrs.stroke = color
|
|
84
|
+
cleanedAttrs['stroke-width'] = originalStrokeWidth ?? strokeWidth
|
|
85
|
+
cleanedAttrs['stroke-linecap'] = 'round'
|
|
86
|
+
cleanedAttrs['stroke-linejoin'] = 'round'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Set attributes
|
|
91
|
+
Object.entries(cleanedAttrs).forEach(([key, value]) => {
|
|
92
|
+
element.setAttribute(key, String(value))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Render children
|
|
96
|
+
if (children && children.length > 0) {
|
|
97
|
+
children.forEach(child => {
|
|
98
|
+
element.appendChild(renderNode(child))
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return element
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
iconNode.forEach(node => {
|
|
106
|
+
svg.appendChild(renderNode(node))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return svg
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export interface IconOptions {
|
|
2
|
+
size?: number | string
|
|
3
|
+
color?: string
|
|
4
|
+
strokeWidth?: number | string
|
|
5
|
+
className?: string
|
|
6
|
+
title?: string
|
|
7
|
+
ariaLabel?: string
|
|
8
|
+
ariaHidden?: boolean
|
|
9
|
+
[key: string]: any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type IconNode = [string, Record<string, any>, IconNode[]?]
|
|
13
|
+
|
|
14
|
+
export function createIcon(name: string, iconNode: IconNode[], keepColors = false) {
|
|
15
|
+
return (options: IconOptions = {}): SVGSVGElement => {
|
|
16
|
+
const {
|
|
17
|
+
size = 24,
|
|
18
|
+
color = 'currentColor',
|
|
19
|
+
strokeWidth = 2,
|
|
20
|
+
className = '',
|
|
21
|
+
title,
|
|
22
|
+
ariaLabel,
|
|
23
|
+
ariaHidden,
|
|
24
|
+
...attrs
|
|
25
|
+
} = options
|
|
26
|
+
|
|
27
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
28
|
+
svg.setAttribute('width', String(size))
|
|
29
|
+
svg.setAttribute('height', String(size))
|
|
30
|
+
svg.setAttribute('viewBox', '0 0 24 24')
|
|
31
|
+
svg.setAttribute('class', className ? `vectify-icon ${className}` : 'vectify-icon')
|
|
32
|
+
|
|
33
|
+
// Accessibility
|
|
34
|
+
const shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
|
|
35
|
+
if (shouldHide) {
|
|
36
|
+
svg.setAttribute('aria-hidden', 'true')
|
|
37
|
+
}
|
|
38
|
+
if (ariaLabel) {
|
|
39
|
+
svg.setAttribute('aria-label', ariaLabel)
|
|
40
|
+
}
|
|
41
|
+
if (title || ariaLabel) {
|
|
42
|
+
svg.setAttribute('role', 'img')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Set additional attributes
|
|
46
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
47
|
+
svg.setAttribute(key, String(value))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Add title if provided
|
|
51
|
+
if (title) {
|
|
52
|
+
const titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title')
|
|
53
|
+
titleEl.textContent = title
|
|
54
|
+
svg.appendChild(titleEl)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Render icon nodes
|
|
58
|
+
function renderNode(node: IconNode): SVGElement {
|
|
59
|
+
const [type, nodeAttrs, children] = node
|
|
60
|
+
const element = document.createElementNS('http://www.w3.org/2000/svg', type)
|
|
61
|
+
|
|
62
|
+
let cleanedAttrs: Record<string, any>
|
|
63
|
+
|
|
64
|
+
if (keepColors) {
|
|
65
|
+
cleanedAttrs = nodeAttrs
|
|
66
|
+
} else {
|
|
67
|
+
// Track color attributes
|
|
68
|
+
let hasFill = false
|
|
69
|
+
let hasStroke = false
|
|
70
|
+
let originalStrokeWidth: number | string | undefined
|
|
71
|
+
|
|
72
|
+
Object.entries(nodeAttrs).forEach(([key, value]) => {
|
|
73
|
+
if (key === 'fill' && value !== 'none') {
|
|
74
|
+
hasFill = true
|
|
75
|
+
}
|
|
76
|
+
if (key === 'stroke') {
|
|
77
|
+
hasStroke = true
|
|
78
|
+
}
|
|
79
|
+
if (key === 'strokeWidth' || key === 'stroke-width') {
|
|
80
|
+
originalStrokeWidth = value
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Keep non-color attributes
|
|
85
|
+
cleanedAttrs = Object.fromEntries(
|
|
86
|
+
Object.entries(nodeAttrs).filter(([key]) =>
|
|
87
|
+
!['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// Apply color
|
|
92
|
+
if (hasFill) {
|
|
93
|
+
cleanedAttrs.fill = color
|
|
94
|
+
} else if (hasStroke) {
|
|
95
|
+
cleanedAttrs.fill = 'none'
|
|
96
|
+
cleanedAttrs.stroke = color
|
|
97
|
+
cleanedAttrs['stroke-width'] = originalStrokeWidth ?? strokeWidth
|
|
98
|
+
cleanedAttrs['stroke-linecap'] = 'round'
|
|
99
|
+
cleanedAttrs['stroke-linejoin'] = 'round'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set attributes
|
|
104
|
+
Object.entries(cleanedAttrs).forEach(([key, value]) => {
|
|
105
|
+
element.setAttribute(key, String(value))
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Render children
|
|
109
|
+
if (children && children.length > 0) {
|
|
110
|
+
children.forEach(child => {
|
|
111
|
+
element.appendChild(renderNode(child))
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return element
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
iconNode.forEach(node => {
|
|
119
|
+
svg.appendChild(renderNode(node))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return svg
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Icon :iconNode="iconNode" v-bind="$attrs" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
import { defineComponent } from 'vue'
|
|
7
|
+
import Icon from './Icon.vue'
|
|
8
|
+
|
|
9
|
+
export default defineComponent({
|
|
10
|
+
name: '{{componentName}}',
|
|
11
|
+
components: { Icon },
|
|
12
|
+
inheritAttrs: false,
|
|
13
|
+
setup() {
|
|
14
|
+
const iconNode = [
|
|
15
|
+
{{{formattedNodes}}}
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
return { iconNode }
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Icon :iconNode="iconNode" v-bind="$attrs" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { defineComponent } from 'vue'
|
|
7
|
+
import Icon from './Icon.vue'
|
|
8
|
+
import type { IconNode } from 'vectify'
|
|
9
|
+
|
|
10
|
+
export default defineComponent({
|
|
11
|
+
name: '{{componentName}}',
|
|
12
|
+
components: { Icon },
|
|
13
|
+
inheritAttrs: false,
|
|
14
|
+
setup() {
|
|
15
|
+
const iconNode: IconNode[] = [
|
|
16
|
+
{{{formattedNodes}}}
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
return { iconNode }
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
:width="size"
|
|
4
|
+
:height="size"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
:aria-hidden="shouldHide"
|
|
7
|
+
:aria-label="ariaLabel"
|
|
8
|
+
:role="title || ariaLabel ? 'img' : undefined"
|
|
9
|
+
v-bind="$attrs"
|
|
10
|
+
:class="mergedClass"
|
|
11
|
+
>
|
|
12
|
+
<title v-if="title">{{ title }}</title>
|
|
13
|
+
<component
|
|
14
|
+
v-for="(node, index) in cleanedIconNode"
|
|
15
|
+
:key="index"
|
|
16
|
+
:is="renderNode(node)"
|
|
17
|
+
/>
|
|
18
|
+
</svg>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import { computed, defineComponent, h } from 'vue'
|
|
23
|
+
|
|
24
|
+
export default defineComponent({
|
|
25
|
+
name: 'Icon',
|
|
26
|
+
inheritAttrs: false,
|
|
27
|
+
props: {
|
|
28
|
+
iconNode: {
|
|
29
|
+
type: Array,
|
|
30
|
+
required: true
|
|
31
|
+
},
|
|
32
|
+
size: {
|
|
33
|
+
type: [Number, String],
|
|
34
|
+
default: 24
|
|
35
|
+
},
|
|
36
|
+
color: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: 'currentColor'
|
|
39
|
+
},
|
|
40
|
+
strokeWidth: {
|
|
41
|
+
type: [Number, String],
|
|
42
|
+
default: 2
|
|
43
|
+
},
|
|
44
|
+
className: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: ''
|
|
47
|
+
},
|
|
48
|
+
title: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: ''
|
|
51
|
+
},
|
|
52
|
+
ariaLabel: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: ''
|
|
55
|
+
},
|
|
56
|
+
ariaHidden: {
|
|
57
|
+
type: [Boolean, String],
|
|
58
|
+
default: undefined
|
|
59
|
+
},
|
|
60
|
+
keepColors: {
|
|
61
|
+
type: Boolean,
|
|
62
|
+
default: false
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
setup(props) {
|
|
66
|
+
// Merge className with default class
|
|
67
|
+
const mergedClass = computed(() => {
|
|
68
|
+
return props.className ? `vectify-icon ${props.className}` : 'vectify-icon'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Determine if icon should be hidden from screen readers
|
|
72
|
+
const shouldHide = computed(() => {
|
|
73
|
+
return props.ariaHidden !== undefined ? props.ariaHidden : (!props.title && !props.ariaLabel)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Clean icon node to apply color
|
|
77
|
+
const cleanedIconNode = computed(() => {
|
|
78
|
+
if (props.keepColors) {
|
|
79
|
+
return props.iconNode
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return cleanIconNodes(props.iconNode, props.color, props.strokeWidth)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
function cleanIconNodes(nodes, color, strokeWidth) {
|
|
86
|
+
return nodes.map(node => {
|
|
87
|
+
const [type, attrs, children] = node
|
|
88
|
+
|
|
89
|
+
// Keep non-color attributes and determine if we need fill or stroke
|
|
90
|
+
const cleanedAttrs = {}
|
|
91
|
+
let hasFill = false
|
|
92
|
+
let hasStroke = false
|
|
93
|
+
let originalStrokeWidth
|
|
94
|
+
|
|
95
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
96
|
+
// Track color attributes
|
|
97
|
+
if (key === 'fill') {
|
|
98
|
+
if (value !== 'none') {
|
|
99
|
+
hasFill = true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (key === 'stroke') {
|
|
103
|
+
hasStroke = true
|
|
104
|
+
}
|
|
105
|
+
if (key === 'strokeWidth' || key === 'stroke-width') {
|
|
106
|
+
originalStrokeWidth = value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Keep non-color attributes
|
|
110
|
+
if (!['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)) {
|
|
111
|
+
cleanedAttrs[key] = value
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Apply color based on original attributes
|
|
116
|
+
if (hasFill) {
|
|
117
|
+
cleanedAttrs.fill = color
|
|
118
|
+
} else if (hasStroke) {
|
|
119
|
+
// Stroke-based icon: set fill to none to prevent default black fill
|
|
120
|
+
cleanedAttrs.fill = 'none'
|
|
121
|
+
cleanedAttrs.stroke = color
|
|
122
|
+
cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
|
|
123
|
+
cleanedAttrs.strokeLinecap = 'round'
|
|
124
|
+
cleanedAttrs.strokeLinejoin = 'round'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cleanedChildren = children ? cleanIconNodes(children, color, strokeWidth) : undefined
|
|
128
|
+
|
|
129
|
+
return [type, cleanedAttrs, cleanedChildren]
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderNode(node) {
|
|
134
|
+
const [type, attrs, children] = node
|
|
135
|
+
|
|
136
|
+
if (children && children.length > 0) {
|
|
137
|
+
return h(
|
|
138
|
+
type,
|
|
139
|
+
attrs,
|
|
140
|
+
children.map(child => renderNode(child))
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return h(type, attrs)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
renderNode,
|
|
149
|
+
mergedClass,
|
|
150
|
+
shouldHide,
|
|
151
|
+
cleanedIconNode
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
</script>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
:width="size"
|
|
4
|
+
:height="size"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
:aria-hidden="shouldHide"
|
|
7
|
+
:aria-label="ariaLabel"
|
|
8
|
+
:role="title || ariaLabel ? 'img' : undefined"
|
|
9
|
+
v-bind="$attrs"
|
|
10
|
+
:class="mergedClass"
|
|
11
|
+
>
|
|
12
|
+
<title v-if="title">{{ title }}</title>
|
|
13
|
+
<component
|
|
14
|
+
v-for="(node, index) in cleanedIconNode"
|
|
15
|
+
:key="index"
|
|
16
|
+
:is="renderNode(node)"
|
|
17
|
+
/>
|
|
18
|
+
</svg>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script lang="ts">
|
|
22
|
+
import { computed, defineComponent, h } from 'vue'
|
|
23
|
+
import type { PropType, VNode } from 'vue'
|
|
24
|
+
import type { IconNode } from 'vectify'
|
|
25
|
+
|
|
26
|
+
export default defineComponent({
|
|
27
|
+
name: 'Icon',
|
|
28
|
+
inheritAttrs: false,
|
|
29
|
+
props: {
|
|
30
|
+
iconNode: {
|
|
31
|
+
type: Array as PropType<IconNode[]>,
|
|
32
|
+
required: true
|
|
33
|
+
},
|
|
34
|
+
size: {
|
|
35
|
+
type: [Number, String],
|
|
36
|
+
default: 24
|
|
37
|
+
},
|
|
38
|
+
color: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: 'currentColor'
|
|
41
|
+
},
|
|
42
|
+
strokeWidth: {
|
|
43
|
+
type: [Number, String],
|
|
44
|
+
default: 2
|
|
45
|
+
},
|
|
46
|
+
className: {
|
|
47
|
+
type: String,
|
|
48
|
+
default: ''
|
|
49
|
+
},
|
|
50
|
+
title: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: ''
|
|
53
|
+
},
|
|
54
|
+
ariaLabel: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: ''
|
|
57
|
+
},
|
|
58
|
+
ariaHidden: {
|
|
59
|
+
type: [Boolean, String] as PropType<boolean | 'true' | 'false'>,
|
|
60
|
+
default: undefined
|
|
61
|
+
},
|
|
62
|
+
keepColors: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: false
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
setup(props) {
|
|
68
|
+
// Merge className with default class
|
|
69
|
+
const mergedClass = computed(() => {
|
|
70
|
+
return props.className ? `vectify-icon ${props.className}` : 'vectify-icon'
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Determine if icon should be hidden from screen readers
|
|
74
|
+
const shouldHide = computed(() => {
|
|
75
|
+
return props.ariaHidden !== undefined ? props.ariaHidden : (!props.title && !props.ariaLabel)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Clean icon node to apply color
|
|
79
|
+
const cleanedIconNode = computed(() => {
|
|
80
|
+
if (props.keepColors) {
|
|
81
|
+
return props.iconNode
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return cleanIconNodes(props.iconNode, props.color, props.strokeWidth)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function cleanIconNodes(nodes: IconNode[], color: string, strokeWidth: number | string): IconNode[] {
|
|
88
|
+
return nodes.map(node => {
|
|
89
|
+
const [type, attrs, children] = node
|
|
90
|
+
|
|
91
|
+
// Keep non-color attributes and determine if we need fill or stroke
|
|
92
|
+
const cleanedAttrs: Record<string, any> = {}
|
|
93
|
+
let hasFill = false
|
|
94
|
+
let hasStroke = false
|
|
95
|
+
let originalStrokeWidth: number | string | undefined
|
|
96
|
+
|
|
97
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
98
|
+
// Track color attributes
|
|
99
|
+
if (key === 'fill') {
|
|
100
|
+
if (value !== 'none') {
|
|
101
|
+
hasFill = true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (key === 'stroke') {
|
|
105
|
+
hasStroke = true
|
|
106
|
+
}
|
|
107
|
+
if (key === 'strokeWidth' || key === 'stroke-width') {
|
|
108
|
+
originalStrokeWidth = value
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Keep non-color attributes
|
|
112
|
+
if (!['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)) {
|
|
113
|
+
cleanedAttrs[key] = value
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Apply color based on original attributes
|
|
118
|
+
if (hasFill) {
|
|
119
|
+
cleanedAttrs.fill = color
|
|
120
|
+
} else if (hasStroke) {
|
|
121
|
+
// Stroke-based icon: set fill to none to prevent default black fill
|
|
122
|
+
cleanedAttrs.fill = 'none'
|
|
123
|
+
cleanedAttrs.stroke = color
|
|
124
|
+
cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
|
|
125
|
+
cleanedAttrs.strokeLinecap = 'round'
|
|
126
|
+
cleanedAttrs.strokeLinejoin = 'round'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cleanedChildren = children ? cleanIconNodes(children, color, strokeWidth) : undefined
|
|
130
|
+
|
|
131
|
+
return [type, cleanedAttrs, cleanedChildren] as IconNode
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderNode(node: IconNode): VNode {
|
|
136
|
+
const [type, attrs, children] = node
|
|
137
|
+
|
|
138
|
+
if (children && children.length > 0) {
|
|
139
|
+
return h(
|
|
140
|
+
type,
|
|
141
|
+
attrs,
|
|
142
|
+
children.map(child => renderNode(child))
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return h(type, attrs)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
renderNode,
|
|
151
|
+
mergedClass,
|
|
152
|
+
shouldHide,
|
|
153
|
+
cleanedIconNode
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
</script>
|