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