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,99 @@
1
+ import { LitElement, html, css, svg } from 'lit'
2
+ import { property } from 'lit/decorators.js'
3
+ import type { IconNode } from 'vectify'
4
+
5
+ export interface IconOptions {
6
+ size?: number | string
7
+ color?: string
8
+ strokeWidth?: number | string
9
+ title?: string
10
+ ariaLabel?: string
11
+ ariaHidden?: boolean
12
+ }
13
+
14
+ export abstract class IconBase extends LitElement {
15
+ @property({ type: Number }) size = 24
16
+ @property({ type: String }) color = 'currentColor'
17
+ @property({ type: Number }) strokeWidth = 2
18
+ @property({ type: String }) title = ''
19
+ @property({ type: String }) ariaLabel = ''
20
+ @property({ type: Boolean }) ariaHidden = false
21
+
22
+ abstract iconNode: IconNode[]
23
+ abstract keepColors: boolean
24
+
25
+ static styles = css`
26
+ :host {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ }
31
+ `
32
+
33
+ private renderNode(node: IconNode): any {
34
+ const [type, attrs, children] = node
35
+ let cleanedAttrs: Record<string, any>
36
+
37
+ if (this.keepColors) {
38
+ cleanedAttrs = attrs
39
+ } else {
40
+ // Track color attributes
41
+ let hasFill = false
42
+ let hasStroke = false
43
+ let originalStrokeWidth: number | string | undefined
44
+
45
+ if (attrs.fill && attrs.fill !== 'none') {
46
+ hasFill = true
47
+ }
48
+ if (attrs.stroke) {
49
+ hasStroke = true
50
+ }
51
+ if (attrs.strokeWidth || attrs['stroke-width']) {
52
+ originalStrokeWidth = attrs.strokeWidth || attrs['stroke-width']
53
+ }
54
+
55
+ // Keep non-color attributes
56
+ cleanedAttrs = Object.fromEntries(
57
+ Object.entries(attrs).filter(([key]) =>
58
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
59
+ )
60
+ )
61
+
62
+ // Apply color
63
+ if (hasFill) {
64
+ cleanedAttrs.fill = this.color
65
+ } else if (hasStroke) {
66
+ cleanedAttrs.fill = 'none'
67
+ cleanedAttrs.stroke = this.color
68
+ cleanedAttrs['stroke-width'] = originalStrokeWidth ?? this.strokeWidth
69
+ cleanedAttrs['stroke-linecap'] = 'round'
70
+ cleanedAttrs['stroke-linejoin'] = 'round'
71
+ }
72
+ }
73
+
74
+ const childNodes = children && children.length > 0
75
+ ? children.map(child => this.renderNode(child))
76
+ : []
77
+
78
+ return svg`<${type} ...${cleanedAttrs}>${childNodes}</${type}>`
79
+ }
80
+
81
+ render() {
82
+ const shouldHide = this.ariaHidden !== undefined ? this.ariaHidden : (!this.title && !this.ariaLabel)
83
+
84
+ return html`
85
+ <svg
86
+ width="${this.size}"
87
+ height="${this.size}"
88
+ viewBox="0 0 24 24"
89
+ class="vectify-icon"
90
+ aria-hidden="${shouldHide}"
91
+ aria-label="${this.ariaLabel || undefined}"
92
+ role="${this.title || this.ariaLabel ? 'img' : undefined}"
93
+ >
94
+ ${this.title ? svg`<title>${this.title}</title>` : ''}
95
+ ${this.iconNode.map(node => this.renderNode(node))}
96
+ </svg>
97
+ `
98
+ }
99
+ }
@@ -0,0 +1,8 @@
1
+ import { createIcon } from './createIcon'
2
+
3
+ export const iconNode = [
4
+ {{{formattedNodes}}}
5
+ ]
6
+
7
+ const {{componentName}} = createIcon('{{componentName}}', iconNode, {{keepColors}})
8
+ export default {{componentName}}
@@ -0,0 +1,11 @@
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
+ const {{componentName}} = createIcon('{{componentName}}', iconNode, {{keepColors}})
11
+ export default {{componentName}}
@@ -0,0 +1,101 @@
1
+ import { createElement } from 'preact'
2
+
3
+ export function createIcon(name, iconNode, keepColors = false) {
4
+ const Icon = ({
5
+ size = 24,
6
+ color = 'currentColor',
7
+ strokeWidth = 2,
8
+ class: classAttr,
9
+ className,
10
+ title,
11
+ 'aria-label': ariaLabel,
12
+ 'aria-hidden': ariaHidden,
13
+ ref,
14
+ ...props
15
+ }) => {
16
+ // Determine if icon should be hidden from screen readers
17
+ const shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
18
+
19
+ const allClassNames = [classAttr, className].filter(Boolean).join(' ')
20
+ const mergedClassName = allClassNames ? `vectify-icon ${allClassNames}` : 'vectify-icon'
21
+
22
+ return (
23
+ <svg
24
+ ref={ref}
25
+ width={size}
26
+ height={size}
27
+ viewBox="0 0 24 24"
28
+ aria-hidden={shouldHide}
29
+ aria-label={ariaLabel}
30
+ role={title || ariaLabel ? 'img' : undefined}
31
+ {...props}
32
+ class={mergedClassName}
33
+ >
34
+ {title && <title>{title}</title>}
35
+ {renderIconNode(iconNode, keepColors, color, strokeWidth)}
36
+ </svg>
37
+ )
38
+ }
39
+
40
+ Icon.displayName = name
41
+ return Icon
42
+ }
43
+
44
+ function renderIconNode(nodes, keepColors, color, strokeWidth) {
45
+ return nodes.map((node, index) => {
46
+ const [type, attrs, children] = node
47
+
48
+ let cleanedAttrs
49
+
50
+ if (keepColors) {
51
+ cleanedAttrs = attrs
52
+ } else {
53
+ // Track color attributes to determine icon type
54
+ let hasFill = false
55
+ let hasStroke = false
56
+ let originalStrokeWidth
57
+
58
+ Object.entries(attrs).forEach(([key, value]) => {
59
+ if (key === 'fill' && value !== 'none') {
60
+ hasFill = true
61
+ }
62
+ if (key === 'stroke') {
63
+ hasStroke = true
64
+ }
65
+ if (key === 'strokeWidth' || key === 'stroke-width') {
66
+ originalStrokeWidth = value
67
+ }
68
+ })
69
+
70
+ // Keep non-color attributes
71
+ cleanedAttrs = Object.fromEntries(
72
+ Object.entries(attrs).filter(([key]) =>
73
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
74
+ )
75
+ )
76
+
77
+ // Apply color based on original attributes
78
+ if (hasFill) {
79
+ cleanedAttrs.fill = color
80
+ } else if (hasStroke) {
81
+ cleanedAttrs.fill = 'none'
82
+ cleanedAttrs.stroke = color
83
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
84
+ cleanedAttrs.strokeLinecap = 'round'
85
+ cleanedAttrs.strokeLinejoin = 'round'
86
+ }
87
+ }
88
+
89
+ const props = { key: index, ...cleanedAttrs }
90
+
91
+ if (children && children.length > 0) {
92
+ return createElement(
93
+ type,
94
+ props,
95
+ renderIconNode(children, keepColors, color, strokeWidth)
96
+ )
97
+ }
98
+
99
+ return createElement(type, props)
100
+ })
101
+ }
@@ -0,0 +1,121 @@
1
+ import { createElement } from 'preact'
2
+ import type { IconNode, IconProps } from 'vectify'
3
+ import type { JSX } from 'preact'
4
+
5
+ export interface CreateIconProps extends IconProps {
6
+ size?: number | string
7
+ color?: string
8
+ strokeWidth?: number | string
9
+ class?: string
10
+ className?: string
11
+ title?: string
12
+ 'aria-label'?: string
13
+ 'aria-hidden'?: boolean | 'true' | 'false'
14
+ ref?: any
15
+ [key: string]: any
16
+ }
17
+
18
+ export function createIcon(name: string, iconNode: IconNode[], keepColors = false) {
19
+ const Icon = ({
20
+ size = 24,
21
+ color = 'currentColor',
22
+ strokeWidth = 2,
23
+ class: classAttr,
24
+ className,
25
+ title,
26
+ 'aria-label': ariaLabel,
27
+ 'aria-hidden': ariaHidden,
28
+ ref,
29
+ ...props
30
+ }: CreateIconProps) => {
31
+ // Determine if icon should be hidden from screen readers
32
+ const shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
33
+
34
+ const allClassNames = [classAttr, className].filter(Boolean).join(' ')
35
+ const mergedClassName = allClassNames ? `vectify-icon ${allClassNames}` : 'vectify-icon'
36
+
37
+ return (
38
+ <svg
39
+ ref={ref}
40
+ width={size}
41
+ height={size}
42
+ viewBox="0 0 24 24"
43
+ aria-hidden={shouldHide}
44
+ aria-label={ariaLabel}
45
+ role={title || ariaLabel ? 'img' : undefined}
46
+ {...props}
47
+ class={mergedClassName}
48
+ >
49
+ {title && <title>{title}</title>}
50
+ {renderIconNode(iconNode, keepColors, color, strokeWidth)}
51
+ </svg>
52
+ )
53
+ }
54
+
55
+ Icon.displayName = name
56
+ return Icon
57
+ }
58
+
59
+ function renderIconNode(
60
+ nodes: IconNode[],
61
+ keepColors: boolean,
62
+ color: string,
63
+ strokeWidth: number | string
64
+ ): JSX.Element[] {
65
+ return nodes.map((node, index) => {
66
+ const [type, attrs, children] = node
67
+
68
+ let cleanedAttrs: Record<string, any>
69
+
70
+ if (keepColors) {
71
+ cleanedAttrs = attrs
72
+ } else {
73
+ // Track color attributes to determine icon type
74
+ let hasFill = false
75
+ let hasStroke = false
76
+ let originalStrokeWidth: number | string | undefined
77
+
78
+ Object.entries(attrs).forEach(([key, value]) => {
79
+ if (key === 'fill' && value !== 'none') {
80
+ hasFill = true
81
+ }
82
+ if (key === 'stroke') {
83
+ hasStroke = true
84
+ }
85
+ if (key === 'strokeWidth' || key === 'stroke-width') {
86
+ originalStrokeWidth = value
87
+ }
88
+ })
89
+
90
+ // Keep non-color attributes
91
+ cleanedAttrs = Object.fromEntries(
92
+ Object.entries(attrs).filter(([key]) =>
93
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
94
+ )
95
+ )
96
+
97
+ // Apply color based on original attributes
98
+ if (hasFill) {
99
+ cleanedAttrs.fill = color
100
+ } else if (hasStroke) {
101
+ cleanedAttrs.fill = 'none'
102
+ cleanedAttrs.stroke = color
103
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
104
+ cleanedAttrs.strokeLinecap = 'round'
105
+ cleanedAttrs.strokeLinejoin = 'round'
106
+ }
107
+ }
108
+
109
+ const props = { key: index, ...cleanedAttrs }
110
+
111
+ if (children && children.length > 0) {
112
+ return createElement(
113
+ type,
114
+ props,
115
+ renderIconNode(children, keepColors, color, strokeWidth)
116
+ )
117
+ }
118
+
119
+ return createElement(type, props)
120
+ })
121
+ }
@@ -0,0 +1,7 @@
1
+ import { createIcon } from './createIcon'
2
+
3
+ export const iconNode = [
4
+ {{{formattedNodes}}}
5
+ ]
6
+
7
+ export const {{componentName}} = createIcon('{{componentName}}', iconNode, {{keepColors}})
@@ -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,100 @@
1
+ import { component$ } from '@builder.io/qwik'
2
+
3
+ export function createIcon(name, iconNode, keepColors = false) {
4
+ return component$(({
5
+ size = 24,
6
+ color = 'currentColor',
7
+ strokeWidth = 2,
8
+ class: className,
9
+ title,
10
+ 'aria-label': ariaLabel,
11
+ 'aria-hidden': ariaHidden,
12
+ ...props
13
+ }) => {
14
+ // Determine if icon should be hidden from screen readers
15
+ const shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
16
+
17
+ const mergedClassName = className ? `vectify-icon ${className}` : 'vectify-icon'
18
+
19
+ return (
20
+ <svg
21
+ width={size}
22
+ height={size}
23
+ viewBox="0 0 24 24"
24
+ aria-hidden={shouldHide}
25
+ aria-label={ariaLabel}
26
+ role={title || ariaLabel ? 'img' : undefined}
27
+ {...props}
28
+ class={mergedClassName}
29
+ >
30
+ {title && <title>{title}</title>}
31
+ {renderIconNode(iconNode, keepColors, color, strokeWidth)}
32
+ </svg>
33
+ )
34
+ })
35
+ }
36
+
37
+ function renderIconNode(
38
+ nodes,
39
+ keepColors,
40
+ color,
41
+ strokeWidth
42
+ ) {
43
+ return nodes.map((node, index) => {
44
+ const [type, attrs, children] = node
45
+
46
+ let cleanedAttrs
47
+
48
+ if (keepColors) {
49
+ cleanedAttrs = attrs
50
+ } else {
51
+ // Track color attributes to determine icon type
52
+ let hasFill = false
53
+ let hasStroke = false
54
+ let originalStrokeWidth
55
+
56
+ Object.entries(attrs).forEach(([key, value]) => {
57
+ if (key === 'fill' && value !== 'none') {
58
+ hasFill = true
59
+ }
60
+ if (key === 'stroke') {
61
+ hasStroke = true
62
+ }
63
+ if (key === 'strokeWidth' || key === 'stroke-width') {
64
+ originalStrokeWidth = value
65
+ }
66
+ })
67
+
68
+ // Keep non-color attributes
69
+ cleanedAttrs = Object.fromEntries(
70
+ Object.entries(attrs).filter(([key]) =>
71
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
72
+ )
73
+ )
74
+
75
+ // Apply color based on original attributes
76
+ if (hasFill) {
77
+ cleanedAttrs.fill = color
78
+ } else if (hasStroke) {
79
+ cleanedAttrs.fill = 'none'
80
+ cleanedAttrs.stroke = color
81
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
82
+ cleanedAttrs.strokeLinecap = 'round'
83
+ cleanedAttrs.strokeLinejoin = 'round'
84
+ }
85
+ }
86
+
87
+ const props = { key: index, ...cleanedAttrs }
88
+ const Element = type
89
+
90
+ if (children && children.length > 0) {
91
+ return (
92
+ <Element {...props}>
93
+ {renderIconNode(children, keepColors, color, strokeWidth)}
94
+ </Element>
95
+ )
96
+ }
97
+
98
+ return <Element {...props} />
99
+ })
100
+ }
@@ -0,0 +1,111 @@
1
+ import { component$, type QwikIntrinsicElements } from '@builder.io/qwik'
2
+ import type { IconNode, IconProps } from 'vectify'
3
+
4
+ export interface CreateIconProps extends IconProps, Omit<QwikIntrinsicElements['svg'], keyof IconProps> {
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
+ }
13
+
14
+ export function createIcon(name: string, iconNode: IconNode[], keepColors = false) {
15
+ return component$<CreateIconProps>(({
16
+ size = 24,
17
+ color = 'currentColor',
18
+ strokeWidth = 2,
19
+ class: className,
20
+ title,
21
+ 'aria-label': ariaLabel,
22
+ 'aria-hidden': ariaHidden,
23
+ ...props
24
+ }) => {
25
+ // Determine if icon should be hidden from screen readers
26
+ const shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
27
+
28
+ const mergedClassName = className ? `vectify-icon ${className}` : 'vectify-icon'
29
+
30
+ return (
31
+ <svg
32
+ width={size}
33
+ height={size}
34
+ viewBox="0 0 24 24"
35
+ aria-hidden={shouldHide}
36
+ aria-label={ariaLabel}
37
+ role={title || ariaLabel ? 'img' : undefined}
38
+ {...props}
39
+ class={mergedClassName}
40
+ >
41
+ {title && <title>{title}</title>}
42
+ {renderIconNode(iconNode, keepColors, color, strokeWidth)}
43
+ </svg>
44
+ )
45
+ })
46
+ }
47
+
48
+ function renderIconNode(
49
+ nodes: IconNode[],
50
+ keepColors: boolean,
51
+ color: string,
52
+ strokeWidth: number | string
53
+ ): any {
54
+ return nodes.map((node, index) => {
55
+ const [type, attrs, children] = node
56
+
57
+ let cleanedAttrs: Record<string, any>
58
+
59
+ if (keepColors) {
60
+ cleanedAttrs = attrs
61
+ } else {
62
+ // Track color attributes to determine icon type
63
+ let hasFill = false
64
+ let hasStroke = false
65
+ let originalStrokeWidth: number | string | undefined
66
+
67
+ Object.entries(attrs).forEach(([key, value]) => {
68
+ if (key === 'fill' && value !== 'none') {
69
+ hasFill = true
70
+ }
71
+ if (key === 'stroke') {
72
+ hasStroke = true
73
+ }
74
+ if (key === 'strokeWidth' || key === 'stroke-width') {
75
+ originalStrokeWidth = value
76
+ }
77
+ })
78
+
79
+ // Keep non-color attributes
80
+ cleanedAttrs = Object.fromEntries(
81
+ Object.entries(attrs).filter(([key]) =>
82
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
83
+ )
84
+ )
85
+
86
+ // Apply color based on original attributes
87
+ if (hasFill) {
88
+ cleanedAttrs.fill = color
89
+ } else if (hasStroke) {
90
+ cleanedAttrs.fill = 'none'
91
+ cleanedAttrs.stroke = color
92
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
93
+ cleanedAttrs.strokeLinecap = 'round'
94
+ cleanedAttrs.strokeLinejoin = 'round'
95
+ }
96
+ }
97
+
98
+ const props = { key: index, ...cleanedAttrs }
99
+ const Element = type as any
100
+
101
+ if (children && children.length > 0) {
102
+ return (
103
+ <Element {...props}>
104
+ {renderIconNode(children, keepColors, color, strokeWidth)}
105
+ </Element>
106
+ )
107
+ }
108
+
109
+ return <Element {...props} />
110
+ })
111
+ }
@@ -0,0 +1,8 @@
1
+ import { createIcon } from './createIcon'
2
+
3
+ export const iconNode = [
4
+ {{{formattedNodes}}}
5
+ ]
6
+
7
+ const {{componentName}} = createIcon('{{componentName}}', iconNode, {{keepColors}})
8
+ export default {{componentName}}
@@ -0,0 +1,11 @@
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
+ const {{componentName}} = createIcon('{{componentName}}', iconNode, {{keepColors}})
11
+ export default {{componentName}}
@@ -0,0 +1,100 @@
1
+ import { createElement, forwardRef } from 'react'
2
+
3
+ export function createIcon(name, iconNode, keepColors = false) {
4
+ const Icon = forwardRef(({
5
+ size = 24,
6
+ color = 'currentColor',
7
+ strokeWidth = 2,
8
+ className,
9
+ title,
10
+ 'aria-label': ariaLabel,
11
+ 'aria-hidden': ariaHidden,
12
+ ...props
13
+ }, ref) => {
14
+ // Determine if icon should be hidden from screen readers
15
+ const shouldHide = ariaHidden !== undefined ? ariaHidden : (!title && !ariaLabel)
16
+
17
+ const { className: propsClassName, ...restProps } = props
18
+ const allClassNames = [className, propsClassName].filter(Boolean).join(' ')
19
+ const mergedClassName = allClassNames ? `vectify-icon ${allClassNames}` : 'vectify-icon'
20
+
21
+ return (
22
+ <svg
23
+ ref={ref}
24
+ width={size}
25
+ height={size}
26
+ viewBox="0 0 24 24"
27
+ aria-hidden={shouldHide}
28
+ aria-label={ariaLabel}
29
+ role={title || ariaLabel ? 'img' : undefined}
30
+ {...restProps}
31
+ className={mergedClassName}
32
+ >
33
+ {title && <title>{title}</title>}
34
+ {renderIconNode(iconNode, keepColors, color, strokeWidth)}
35
+ </svg>
36
+ )
37
+ })
38
+
39
+ Icon.displayName = name
40
+ return Icon
41
+ }
42
+
43
+ function renderIconNode(nodes, keepColors, color, strokeWidth) {
44
+ return nodes.map((node, index) => {
45
+ const [type, attrs, children] = node
46
+
47
+ let cleanedAttrs
48
+
49
+ if (keepColors) {
50
+ cleanedAttrs = attrs
51
+ } else {
52
+ // Track color attributes to determine icon type
53
+ let hasFill = false
54
+ let hasStroke = false
55
+ let originalStrokeWidth
56
+
57
+ Object.entries(attrs).forEach(([key, value]) => {
58
+ if (key === 'fill' && value !== 'none') {
59
+ hasFill = true
60
+ }
61
+ if (key === 'stroke') {
62
+ hasStroke = true
63
+ }
64
+ if (key === 'strokeWidth' || key === 'stroke-width') {
65
+ originalStrokeWidth = value
66
+ }
67
+ })
68
+
69
+ // Keep non-color attributes
70
+ cleanedAttrs = Object.fromEntries(
71
+ Object.entries(attrs).filter(([key]) =>
72
+ !['stroke', 'fill', 'strokeWidth', 'stroke-width'].includes(key)
73
+ )
74
+ )
75
+
76
+ // Apply color based on original attributes
77
+ if (hasFill) {
78
+ cleanedAttrs.fill = color
79
+ } else if (hasStroke) {
80
+ cleanedAttrs.fill = 'none'
81
+ cleanedAttrs.stroke = color
82
+ cleanedAttrs.strokeWidth = originalStrokeWidth ?? strokeWidth
83
+ cleanedAttrs.strokeLinecap = 'round'
84
+ cleanedAttrs.strokeLinejoin = 'round'
85
+ }
86
+ }
87
+
88
+ const props = { key: index, ...cleanedAttrs }
89
+
90
+ if (children && children.length > 0) {
91
+ return createElement(
92
+ type,
93
+ props,
94
+ renderIconNode(children, keepColors, color, strokeWidth)
95
+ )
96
+ }
97
+
98
+ return createElement(type, props)
99
+ })
100
+ }