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,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,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>
|