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,117 @@
1
+ import { createElement, forwardRef } from 'react'
2
+ import type { IconNode, IconProps } from 'vectify'
3
+ import type { ReactNode, SVGProps } from 'react'
4
+
5
+ export interface CreateIconProps extends IconProps, Omit<SVGProps<SVGSVGElement>, keyof IconProps> {
6
+ size?: number | string
7
+ color?: string
8
+ strokeWidth?: number | string
9
+ className?: string
10
+ title?: string
11
+ 'aria-label'?: string
12
+ 'aria-hidden'?: boolean | 'true' | 'false'
13
+ }
14
+
15
+ export function createIcon(name: string, iconNode: IconNode[], keepColors = false) {
16
+ const Icon = forwardRef<SVGSVGElement, CreateIconProps>(({
17
+ size = 24,
18
+ color = 'currentColor',
19
+ strokeWidth = 2,
20
+ className,
21
+ title,
22
+ 'aria-label': ariaLabel,
23
+ 'aria-hidden': ariaHidden,
24
+ ...props
25
+ }, ref) => {
26
+ // Determine if icon should be hidden from screen readers
27
+ const shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
28
+
29
+ const { className: propsClassName, ...restProps } = props as any
30
+ const allClassNames = [className, propsClassName].filter(Boolean).join(' ')
31
+ const mergedClassName = allClassNames ? `vectify-icon ${allClassNames}` : 'vectify-icon'
32
+
33
+ return (
34
+ <svg
35
+ ref={ref}
36
+ width={size}
37
+ height={size}
38
+ viewBox="0 0 24 24"
39
+ aria-hidden={shouldHide}
40
+ aria-label={ariaLabel}
41
+ role={title || ariaLabel ? 'img' : undefined}
42
+ {...restProps}
43
+ className={mergedClassName}
44
+ >
45
+ {title && <title>{title}</title>}
46
+ {renderIconNode(iconNode, keepColors, color, strokeWidth)}
47
+ </svg>
48
+ )
49
+ })
50
+
51
+ Icon.displayName = name
52
+ return Icon
53
+ }
54
+
55
+ function renderIconNode(
56
+ nodes: IconNode[],
57
+ keepColors: boolean,
58
+ color: string,
59
+ strokeWidth: number | string
60
+ ): ReactNode {
61
+ return nodes.map((node, index) => {
62
+ const [type, attrs, children] = node
63
+
64
+ let cleanedAttrs: Record<string, any>
65
+
66
+ if (keepColors) {
67
+ cleanedAttrs = attrs
68
+ } else {
69
+ // Track color attributes to determine icon type
70
+ let hasFill = false
71
+ let hasStroke = false
72
+ let originalStrokeWidth: number | string | undefined
73
+
74
+ Object.entries(attrs).forEach(([key, value]) => {
75
+ if (key === 'fill' && value !== 'none') {
76
+ hasFill = true
77
+ }
78
+ if (key === 'stroke') {
79
+ hasStroke = true
80
+ }
81
+ if (key === 'strokeWidth' || key === 'stroke-width') {
82
+ originalStrokeWidth = value
83
+ }
84
+ })
85
+
86
+ // Keep non-color attributes
87
+ cleanedAttrs = Object.fromEntries(
88
+ Object.entries(attrs).filter(([key]) =>
89
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
90
+ )
91
+ )
92
+
93
+ // Apply color based on original attributes
94
+ if (hasFill) {
95
+ cleanedAttrs.fill = color
96
+ } else if (hasStroke) {
97
+ cleanedAttrs.fill = 'none'
98
+ cleanedAttrs.stroke = color
99
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
100
+ cleanedAttrs.strokeLinecap = 'round'
101
+ cleanedAttrs.strokeLinejoin = 'round'
102
+ }
103
+ }
104
+
105
+ const props = { key: index, ...cleanedAttrs }
106
+
107
+ if (children && children.length > 0) {
108
+ return createElement(
109
+ type,
110
+ props,
111
+ renderIconNode(children, keepColors, color, strokeWidth)
112
+ )
113
+ }
114
+
115
+ return createElement(type, props)
116
+ })
117
+ }
@@ -0,0 +1,10 @@
1
+ {{#if typescript}}
2
+ import type { IconNode } from 'vectify'
3
+ {{/if}}
4
+ import { createIcon } from './createIcon'
5
+
6
+ export const iconNode{{#if typescript}}: IconNode[]{{/if}} = [
7
+ {{{formattedNodes}}}
8
+ ]
9
+
10
+ export const {{componentName}} = createIcon('{{componentName}}', iconNode, {{keepColors}})
@@ -0,0 +1,127 @@
1
+ import { splitProps } from 'solid-js'
2
+
3
+ export function createIcon(name, iconNode, keepColors = false) {
4
+ return (props) => {
5
+ const [local, others] = splitProps(props, [
6
+ 'size',
7
+ 'color',
8
+ 'strokeWidth',
9
+ 'class',
10
+ 'title',
11
+ 'aria-label',
12
+ 'aria-hidden'
13
+ ])
14
+
15
+ const size = () => local.size ?? 24
16
+ const color = () => local.color ?? 'currentColor'
17
+ const strokeWidth = () => local.strokeWidth ?? 2
18
+
19
+ // Determine if icon should be hidden from screen readers
20
+ const shouldHide = () => local['aria-hidden'] !== undefined
21
+ ? local['aria-hidden']
22
+ : (!local.title && !local['aria-label'])
23
+
24
+ const mergedClass = () => local.class ? `vectify-icon ${local.class}` : 'vectify-icon'
25
+
26
+ const renderIconNode = (node, index) => {
27
+ const [type, attrs, children] = node
28
+
29
+ let cleanedAttrs
30
+
31
+ if (keepColors) {
32
+ cleanedAttrs = attrs
33
+ } else {
34
+ // Track color attributes to determine icon type
35
+ let hasFill = false
36
+ let hasStroke = false
37
+ let originalStrokeWidth
38
+
39
+ Object.entries(attrs).forEach(([key, value]) => {
40
+ if (key === 'fill' && value !== 'none') {
41
+ hasFill = true
42
+ }
43
+ if (key === 'stroke') {
44
+ hasStroke = true
45
+ }
46
+ if (key === 'strokeWidth' || key === 'stroke-width') {
47
+ originalStrokeWidth = value
48
+ }
49
+ })
50
+
51
+ // Keep non-color attributes
52
+ cleanedAttrs = Object.fromEntries(
53
+ Object.entries(attrs).filter(([key]) =>
54
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
55
+ )
56
+ )
57
+
58
+ // Apply color based on original attributes
59
+ if (hasFill) {
60
+ cleanedAttrs.fill = color()
61
+ } else if (hasStroke) {
62
+ cleanedAttrs.fill = 'none'
63
+ cleanedAttrs.stroke = color()
64
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth()
65
+ cleanedAttrs.strokeLinecap = 'round'
66
+ cleanedAttrs.strokeLinejoin = 'round'
67
+ }
68
+ }
69
+
70
+ // Convert strokeWidth to stroke-width for SVG
71
+ if (cleanedAttrs.strokeWidth) {
72
+ cleanedAttrs['stroke-width'] = cleanedAttrs.strokeWidth
73
+ delete cleanedAttrs.strokeWidth
74
+ }
75
+ if (cleanedAttrs.strokeLinecap) {
76
+ cleanedAttrs['stroke-linecap'] = cleanedAttrs.strokeLinecap
77
+ delete cleanedAttrs.strokeLinecap
78
+ }
79
+ if (cleanedAttrs.strokeLinejoin) {
80
+ cleanedAttrs['stroke-linejoin'] = cleanedAttrs.strokeLinejoin
81
+ delete cleanedAttrs.strokeLinejoin
82
+ }
83
+
84
+ const childElements = children && children.length > 0
85
+ ? children.map((child, idx) => renderIconNode(child, idx))
86
+ : undefined
87
+
88
+ // Create SVG element using the type as tag name
89
+ switch (type) {
90
+ case 'path':
91
+ return <path {...cleanedAttrs}>{childElements}</path>
92
+ case 'circle':
93
+ return <circle {...cleanedAttrs}>{childElements}</circle>
94
+ case 'rect':
95
+ return <rect {...cleanedAttrs}>{childElements}</rect>
96
+ case 'line':
97
+ return <line {...cleanedAttrs}>{childElements}</line>
98
+ case 'polyline':
99
+ return <polyline {...cleanedAttrs}>{childElements}</polyline>
100
+ case 'polygon':
101
+ return <polygon {...cleanedAttrs}>{childElements}</polygon>
102
+ case 'ellipse':
103
+ return <ellipse {...cleanedAttrs}>{childElements}</ellipse>
104
+ case 'g':
105
+ return <g {...cleanedAttrs}>{childElements}</g>
106
+ default:
107
+ return <g {...cleanedAttrs}>{childElements}</g>
108
+ }
109
+ }
110
+
111
+ return (
112
+ <svg
113
+ width={size()}
114
+ height={size()}
115
+ viewBox="0 0 24 24"
116
+ aria-hidden={shouldHide()}
117
+ aria-label={local['aria-label']}
118
+ role={local.title || local['aria-label'] ? 'img' : undefined}
119
+ {...others}
120
+ class={mergedClass()}
121
+ >
122
+ {local.title && <title>{local.title}</title>}
123
+ {iconNode.map((node, index) => renderIconNode(node, index))}
124
+ </svg>
125
+ )
126
+ }
127
+ }
@@ -0,0 +1,139 @@
1
+ import { JSX, splitProps, createMemo } from 'solid-js'
2
+ import type { IconNode } from 'vectify'
3
+
4
+ export interface CreateIconProps {
5
+ size?: number | string
6
+ color?: string
7
+ strokeWidth?: number | string
8
+ class?: string
9
+ title?: string
10
+ 'aria-label'?: string
11
+ 'aria-hidden'?: boolean | 'true' | 'false'
12
+ [key: string]: any
13
+ }
14
+
15
+ export function createIcon(name: string, iconNode: IconNode[], keepColors = false) {
16
+ return (props: CreateIconProps): JSX.Element => {
17
+ const [local, others] = splitProps(props, [
18
+ 'size',
19
+ 'color',
20
+ 'strokeWidth',
21
+ 'class',
22
+ 'title',
23
+ 'aria-label',
24
+ 'aria-hidden'
25
+ ])
26
+
27
+ const size = () => local.size ?? 24
28
+ const color = () => local.color ?? 'currentColor'
29
+ const strokeWidth = () => local.strokeWidth ?? 2
30
+
31
+ // Determine if icon should be hidden from screen readers
32
+ const shouldHide = () => local['aria-hidden'] !== undefined
33
+ ? local['aria-hidden']
34
+ : (!local.title && !local['aria-label'])
35
+
36
+ const mergedClass = () => local.class ? `vectify-icon ${local.class}` : 'vectify-icon'
37
+
38
+ const renderIconNode = (node: IconNode, index: number): JSX.Element => {
39
+ const [type, attrs, children] = node
40
+
41
+ let cleanedAttrs: Record<string, any>
42
+
43
+ if (keepColors) {
44
+ cleanedAttrs = attrs
45
+ } else {
46
+ // Track color attributes to determine icon type
47
+ let hasFill = false
48
+ let hasStroke = false
49
+ let originalStrokeWidth: number | string | undefined
50
+
51
+ Object.entries(attrs).forEach(([key, value]) => {
52
+ if (key === 'fill' && value !== 'none') {
53
+ hasFill = true
54
+ }
55
+ if (key === 'stroke') {
56
+ hasStroke = true
57
+ }
58
+ if (key === 'strokeWidth' || key === 'stroke-width') {
59
+ originalStrokeWidth = value
60
+ }
61
+ })
62
+
63
+ // Keep non-color attributes
64
+ cleanedAttrs = Object.fromEntries(
65
+ Object.entries(attrs).filter(([key]) =>
66
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
67
+ )
68
+ )
69
+
70
+ // Apply color based on original attributes
71
+ if (hasFill) {
72
+ cleanedAttrs.fill = color()
73
+ } else if (hasStroke) {
74
+ cleanedAttrs.fill = 'none'
75
+ cleanedAttrs.stroke = color()
76
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth()
77
+ cleanedAttrs.strokeLinecap = 'round'
78
+ cleanedAttrs.strokeLinejoin = 'round'
79
+ }
80
+ }
81
+
82
+ // Convert strokeWidth to stroke-width for SVG
83
+ if (cleanedAttrs.strokeWidth) {
84
+ cleanedAttrs['stroke-width'] = cleanedAttrs.strokeWidth
85
+ delete cleanedAttrs.strokeWidth
86
+ }
87
+ if (cleanedAttrs.strokeLinecap) {
88
+ cleanedAttrs['stroke-linecap'] = cleanedAttrs.strokeLinecap
89
+ delete cleanedAttrs.strokeLinecap
90
+ }
91
+ if (cleanedAttrs.strokeLinejoin) {
92
+ cleanedAttrs['stroke-linejoin'] = cleanedAttrs.strokeLinejoin
93
+ delete cleanedAttrs.strokeLinejoin
94
+ }
95
+
96
+ const childElements = children && children.length > 0
97
+ ? children.map((child, idx) => renderIconNode(child, idx))
98
+ : undefined
99
+
100
+ // Create SVG element using the type as tag name
101
+ switch (type) {
102
+ case 'path':
103
+ return <path {...cleanedAttrs}>{childElements}</path>
104
+ case 'circle':
105
+ return <circle {...cleanedAttrs}>{childElements}</circle>
106
+ case 'rect':
107
+ return <rect {...cleanedAttrs}>{childElements}</rect>
108
+ case 'line':
109
+ return <line {...cleanedAttrs}>{childElements}</line>
110
+ case 'polyline':
111
+ return <polyline {...cleanedAttrs}>{childElements}</polyline>
112
+ case 'polygon':
113
+ return <polygon {...cleanedAttrs}>{childElements}</polygon>
114
+ case 'ellipse':
115
+ return <ellipse {...cleanedAttrs}>{childElements}</ellipse>
116
+ case 'g':
117
+ return <g {...cleanedAttrs}>{childElements}</g>
118
+ default:
119
+ return <g {...cleanedAttrs}>{childElements}</g>
120
+ }
121
+ }
122
+
123
+ return (
124
+ <svg
125
+ width={size()}
126
+ height={size()}
127
+ viewBox="0 0 24 24"
128
+ aria-hidden={shouldHide()}
129
+ aria-label={local['aria-label']}
130
+ role={local.title || local['aria-label'] ? 'img' : undefined}
131
+ {...others}
132
+ class={mergedClass()}
133
+ >
134
+ {local.title && <title>{local.title}</title>}
135
+ {iconNode.map((node, index) => renderIconNode(node, index))}
136
+ </svg>
137
+ )
138
+ }
139
+ }
@@ -0,0 +1,9 @@
1
+ <script>
2
+ import Icon from './Icon.svelte'
3
+
4
+ const iconNode = [
5
+ {{{formattedNodes}}}
6
+ ]
7
+ </script>
8
+
9
+ <Icon {iconNode} {...$$restProps} />
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import Icon from './Icon.svelte'
3
+ import type { IconNode } from 'vectify'
4
+
5
+ const iconNode: IconNode[] = [
6
+ {{{formattedNodes}}}
7
+ ]
8
+ </script>
9
+
10
+ <Icon {iconNode} {...$$restProps} />
@@ -0,0 +1,123 @@
1
+ <script>
2
+ import { onMount } from 'svelte'
3
+
4
+ export let iconNode
5
+ export let size = 24
6
+ export let color = 'currentColor'
7
+ export let strokeWidth = 2
8
+ export let className = ''
9
+ export let title = ''
10
+ export let ariaLabel = ''
11
+ export let ariaHidden = undefined
12
+ export let keepColors = false
13
+
14
+ let svgElement
15
+
16
+ // Merge className with default class
17
+ $: mergedClass = className ? `vectify-icon ${className}` : 'vectify-icon'
18
+
19
+ // Determine if icon should be hidden from screen readers
20
+ $: shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
21
+
22
+ // Clean icon node to apply color
23
+ $: cleanedIconNode = keepColors ? iconNode : cleanIconNodes(iconNode, color, strokeWidth)
24
+
25
+ function cleanIconNodes(nodes, color, strokeWidth) {
26
+ return nodes.map(node => {
27
+ const [type, attrs, children] = node
28
+
29
+ // Keep non-color attributes and determine if we need fill or stroke
30
+ const cleanedAttrs = {}
31
+ let hasFill = false
32
+ let hasStroke = false
33
+ let originalStrokeWidth
34
+
35
+ Object.entries(attrs).forEach(([key, value]) => {
36
+ // Track color attributes
37
+ if (key === 'fill') {
38
+ if (value !== 'none') {
39
+ hasFill = true
40
+ }
41
+ }
42
+ if (key === 'stroke') {
43
+ hasStroke = true
44
+ }
45
+ if (key === 'strokeWidth' || key === 'stroke-width') {
46
+ originalStrokeWidth = value
47
+ }
48
+
49
+ // Keep non-color attributes
50
+ if (!['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)) {
51
+ cleanedAttrs[key] = value
52
+ }
53
+ })
54
+
55
+ // Apply color based on original attributes
56
+ if (hasFill) {
57
+ cleanedAttrs.fill = color
58
+ } else if (hasStroke) {
59
+ // Stroke-based icon: set fill to none to prevent default black fill
60
+ cleanedAttrs.fill = 'none'
61
+ cleanedAttrs.stroke = color
62
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
63
+ cleanedAttrs.strokeLinecap = 'round'
64
+ cleanedAttrs.strokeLinejoin = 'round'
65
+ }
66
+
67
+ const cleanedChildren = children ? cleanIconNodes(children, color, strokeWidth) : undefined
68
+
69
+ return cleanedChildren ? [type, cleanedAttrs, cleanedChildren] : [type, cleanedAttrs]
70
+ })
71
+ }
72
+
73
+ function renderNode(node) {
74
+ const [type, attrs, children] = node
75
+ const element = document.createElementNS('http://www.w3.org/2000/svg', type)
76
+
77
+ // Set attributes
78
+ Object.entries(attrs).forEach(([key, value]) => {
79
+ element.setAttribute(key, String(value))
80
+ })
81
+
82
+ // Render children
83
+ if (children && children.length > 0) {
84
+ children.forEach(child => {
85
+ element.appendChild(renderNode(child))
86
+ })
87
+ }
88
+
89
+ return element
90
+ }
91
+
92
+ onMount(() => {
93
+ // Clear existing content (except title if present)
94
+ while (svgElement.firstChild) {
95
+ if (svgElement.firstChild.nodeName !== 'title') {
96
+ svgElement.removeChild(svgElement.firstChild)
97
+ } else {
98
+ break
99
+ }
100
+ }
101
+
102
+ // Render icon nodes after title
103
+ cleanedIconNode.forEach(node => {
104
+ svgElement.appendChild(renderNode(node))
105
+ })
106
+ })
107
+ </script>
108
+
109
+ <svg
110
+ bind:this={svgElement}
111
+ width={size}
112
+ height={size}
113
+ viewBox="0 0 24 24"
114
+ aria-hidden={shouldHide}
115
+ aria-label={ariaLabel || undefined}
116
+ role={title || ariaLabel ? 'img' : undefined}
117
+ {...$$restProps}
118
+ class={mergedClass}
119
+ >
120
+ {#if title}
121
+ <title>{title}</title>
122
+ {/if}
123
+ </svg>
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+ import type { IconNode } from 'vectify'
4
+
5
+ export let iconNode: IconNode[]
6
+ export let size: number | string = 24
7
+ export let color: string = 'currentColor'
8
+ export let strokeWidth: number | string = 2
9
+ export let className: string = ''
10
+ export let title: string = ''
11
+ export let ariaLabel: string = ''
12
+ export let ariaHidden: boolean | 'true' | 'false' | undefined = undefined
13
+ export let keepColors: boolean = false
14
+
15
+ let svgElement: SVGSVGElement
16
+
17
+ // Merge className with default class
18
+ $: mergedClass = className ? `vectify-icon ${className}` : 'vectify-icon'
19
+
20
+ // Determine if icon should be hidden from screen readers
21
+ $: shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
22
+
23
+ // Clean icon node to apply color
24
+ $: cleanedIconNode = keepColors ? iconNode : cleanIconNodes(iconNode, color, strokeWidth)
25
+
26
+ function cleanIconNodes(nodes: IconNode[], color: string, strokeWidth: number | string): IconNode[] {
27
+ return nodes.map(node => {
28
+ const [type, attrs, children] = node
29
+
30
+ // Keep non-color attributes and determine if we need fill or stroke
31
+ const cleanedAttrs: Record<string, any> = {}
32
+ let hasFill = false
33
+ let hasStroke = false
34
+ let originalStrokeWidth: number | string | undefined
35
+
36
+ Object.entries(attrs).forEach(([key, value]) => {
37
+ // Track color attributes
38
+ if (key === 'fill') {
39
+ if (value !== 'none') {
40
+ hasFill = true
41
+ }
42
+ }
43
+ if (key === 'stroke') {
44
+ hasStroke = true
45
+ }
46
+ if (key === 'strokeWidth' || key === 'stroke-width') {
47
+ originalStrokeWidth = value
48
+ }
49
+
50
+ // Keep non-color attributes
51
+ if (!['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)) {
52
+ cleanedAttrs[key] = value
53
+ }
54
+ })
55
+
56
+ // Apply color based on original attributes
57
+ if (hasFill) {
58
+ cleanedAttrs.fill = color
59
+ } else if (hasStroke) {
60
+ // Stroke-based icon: set fill to none to prevent default black fill
61
+ cleanedAttrs.fill = 'none'
62
+ cleanedAttrs.stroke = color
63
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
64
+ cleanedAttrs.strokeLinecap = 'round'
65
+ cleanedAttrs.strokeLinejoin = 'round'
66
+ }
67
+
68
+ const cleanedChildren = children ? cleanIconNodes(children, color, strokeWidth) : undefined
69
+
70
+ return cleanedChildren ? [type, cleanedAttrs, cleanedChildren] : [type, cleanedAttrs]
71
+ }) as IconNode[]
72
+ }
73
+
74
+ function renderNode(node: IconNode): SVGElement {
75
+ const [type, attrs, children] = node
76
+ const element = document.createElementNS('http://www.w3.org/2000/svg', type)
77
+
78
+ // Set attributes
79
+ Object.entries(attrs).forEach(([key, value]) => {
80
+ element.setAttribute(key, String(value))
81
+ })
82
+
83
+ // Render children
84
+ if (children && children.length > 0) {
85
+ children.forEach(child => {
86
+ element.appendChild(renderNode(child))
87
+ })
88
+ }
89
+
90
+ return element
91
+ }
92
+
93
+ onMount(() => {
94
+ // Clear existing content (except title if present)
95
+ while (svgElement.firstChild) {
96
+ if (svgElement.firstChild.nodeName !== 'title') {
97
+ svgElement.removeChild(svgElement.firstChild)
98
+ } else {
99
+ break
100
+ }
101
+ }
102
+
103
+ // Render icon nodes after title
104
+ cleanedIconNode.forEach(node => {
105
+ svgElement.appendChild(renderNode(node))
106
+ })
107
+ })
108
+ </script>
109
+
110
+ <svg
111
+ bind:this={svgElement}
112
+ width={size}
113
+ height={size}
114
+ viewBox="0 0 24 24"
115
+ aria-hidden={shouldHide}
116
+ aria-label={ariaLabel || undefined}
117
+ role={title || ariaLabel ? 'img' : undefined}
118
+ {...$$restProps}
119
+ class={mergedClass}
120
+ >
121
+ {#if title}
122
+ <title>{title}</title>
123
+ {/if}
124
+ </svg>