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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +679 -0
  3. package/README.zh-CN.md +683 -0
  4. package/dist/chunk-4BWKFV7W.mjs +1311 -0
  5. package/dist/chunk-CIKTK6HI.mjs +96 -0
  6. package/dist/cli.d.mts +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +1483 -0
  9. package/dist/cli.mjs +56 -0
  10. package/dist/helpers-UPZEBRGK.mjs +26 -0
  11. package/dist/index.d.mts +281 -0
  12. package/dist/index.d.ts +281 -0
  13. package/dist/index.js +1463 -0
  14. package/dist/index.mjs +27 -0
  15. package/dist/templates/angular/component.ts.hbs +121 -0
  16. package/dist/templates/angular/createIcon.ts.hbs +116 -0
  17. package/dist/templates/astro/component.astro.hbs +110 -0
  18. package/dist/templates/astro/createIcon.astro.hbs +111 -0
  19. package/dist/templates/lit/component.js.hbs +12 -0
  20. package/dist/templates/lit/component.ts.hbs +19 -0
  21. package/dist/templates/lit/createIcon.js.hbs +98 -0
  22. package/dist/templates/lit/createIcon.ts.hbs +99 -0
  23. package/dist/templates/preact/component.jsx.hbs +8 -0
  24. package/dist/templates/preact/component.tsx.hbs +11 -0
  25. package/dist/templates/preact/createIcon.jsx.hbs +101 -0
  26. package/dist/templates/preact/createIcon.tsx.hbs +121 -0
  27. package/dist/templates/qwik/component.jsx.hbs +7 -0
  28. package/dist/templates/qwik/component.tsx.hbs +8 -0
  29. package/dist/templates/qwik/createIcon.jsx.hbs +100 -0
  30. package/dist/templates/qwik/createIcon.tsx.hbs +111 -0
  31. package/dist/templates/react/component.jsx.hbs +8 -0
  32. package/dist/templates/react/component.tsx.hbs +11 -0
  33. package/dist/templates/react/createIcon.jsx.hbs +100 -0
  34. package/dist/templates/react/createIcon.tsx.hbs +117 -0
  35. package/dist/templates/solid/component.tsx.hbs +10 -0
  36. package/dist/templates/solid/createIcon.jsx.hbs +127 -0
  37. package/dist/templates/solid/createIcon.tsx.hbs +139 -0
  38. package/dist/templates/svelte/component.js.svelte.hbs +9 -0
  39. package/dist/templates/svelte/component.ts.svelte.hbs +10 -0
  40. package/dist/templates/svelte/icon.js.svelte.hbs +123 -0
  41. package/dist/templates/svelte/icon.ts.svelte.hbs +124 -0
  42. package/dist/templates/template-engine.ts +107 -0
  43. package/dist/templates/vanilla/component.ts.hbs +8 -0
  44. package/dist/templates/vanilla/createIcon.js.hbs +111 -0
  45. package/dist/templates/vanilla/createIcon.ts.hbs +124 -0
  46. package/dist/templates/vue/component.js.vue.hbs +21 -0
  47. package/dist/templates/vue/component.ts.vue.hbs +22 -0
  48. package/dist/templates/vue/icon.js.vue.hbs +155 -0
  49. package/dist/templates/vue/icon.ts.vue.hbs +157 -0
  50. 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,8 @@
1
+ import { createIcon } from './createIcon'
2
+ import type { IconNode } from 'vectify'
3
+
4
+ export const iconNode: IconNode[] = [
5
+ {{{formattedNodes}}}
6
+ ]
7
+
8
+ export const {{componentName}} = createIcon('{{componentName}}', iconNode, {{keepColors}})
@@ -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>