@uniweb/kit 0.3.1 → 0.4.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/package.json +2 -2
- package/src/components/Icon/Icon.jsx +95 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"fuse.js": "^7.0.0",
|
|
40
40
|
"shiki": "^3.0.0",
|
|
41
41
|
"tailwind-merge": "^2.6.0",
|
|
42
|
-
"@uniweb/core": "0.
|
|
42
|
+
"@uniweb/core": "0.3.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* - Direct SVG content
|
|
6
6
|
* - URL to SVG file
|
|
7
7
|
* - Built-in icons
|
|
8
|
+
* - Library-based icons via resolver (lucide, heroicons, etc.)
|
|
8
9
|
*
|
|
9
10
|
* @module @uniweb/kit/Icon
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import React, { useState, useEffect, useMemo } from 'react'
|
|
14
|
+
import { getUniweb } from '@uniweb/core'
|
|
13
15
|
import { cn } from '../../utils/index.js'
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -63,8 +65,17 @@ function parseSvg(svgContent) {
|
|
|
63
65
|
/**
|
|
64
66
|
* Icon - SVG icon component
|
|
65
67
|
*
|
|
68
|
+
* Resolution order:
|
|
69
|
+
* 1. Direct SVG (svg prop)
|
|
70
|
+
* 2. URL-based (url prop)
|
|
71
|
+
* 3. Built-in icons (name prop, no library)
|
|
72
|
+
* 4. Library-based via resolver (library + name props)
|
|
73
|
+
*
|
|
74
|
+
* SSR: Renders sized placeholder, hydrates with SVG client-side
|
|
75
|
+
*
|
|
66
76
|
* @param {Object} props
|
|
67
|
-
* @param {string} [props.
|
|
77
|
+
* @param {string} [props.library] - Icon family: lucide, heroicons, etc.
|
|
78
|
+
* @param {string} [props.name] - Built-in icon name or library icon name
|
|
68
79
|
* @param {string} [props.svg] - Direct SVG content
|
|
69
80
|
* @param {string} [props.url] - URL to fetch SVG from
|
|
70
81
|
* @param {string} [props.size='24'] - Icon size in pixels
|
|
@@ -79,6 +90,10 @@ function parseSvg(svgContent) {
|
|
|
79
90
|
* <Icon name="check" size="20" color="green" />
|
|
80
91
|
*
|
|
81
92
|
* @example
|
|
93
|
+
* // Library icon (lucide)
|
|
94
|
+
* <Icon library="lucide" name="home" size="24" />
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
82
97
|
* // From URL
|
|
83
98
|
* <Icon url="/icons/custom.svg" size="32" />
|
|
84
99
|
*
|
|
@@ -87,6 +102,7 @@ function parseSvg(svgContent) {
|
|
|
87
102
|
* <Icon svg="<svg>...</svg>" />
|
|
88
103
|
*/
|
|
89
104
|
export function Icon({
|
|
105
|
+
library,
|
|
90
106
|
name,
|
|
91
107
|
svg,
|
|
92
108
|
url,
|
|
@@ -103,33 +119,66 @@ export function Icon({
|
|
|
103
119
|
const [loading, setLoading] = useState(false)
|
|
104
120
|
const [error, setError] = useState(false)
|
|
105
121
|
|
|
106
|
-
//
|
|
122
|
+
// Normalize props (handle legacy icon object)
|
|
123
|
+
const iconLibrary = library || (typeof icon === 'object' ? icon.library : null)
|
|
107
124
|
const iconUrl = url || (typeof icon === 'string' ? icon : icon?.url)
|
|
108
125
|
const iconSvg = svg || (typeof icon === 'object' ? icon.svg : null)
|
|
109
126
|
const iconName = name || (typeof icon === 'object' ? icon.name : null)
|
|
110
127
|
|
|
111
|
-
// Fetch SVG from URL
|
|
128
|
+
// Fetch SVG from URL or resolve from library
|
|
112
129
|
useEffect(() => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
setLoading(true)
|
|
130
|
+
// Reset state when source changes
|
|
131
|
+
setFetchedSvg(null)
|
|
116
132
|
setError(false)
|
|
117
133
|
|
|
118
|
-
fetch
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
// Direct SVG - no fetch needed
|
|
135
|
+
if (iconSvg) return
|
|
136
|
+
|
|
137
|
+
// URL-based fetch
|
|
138
|
+
if (iconUrl) {
|
|
139
|
+
setLoading(true)
|
|
140
|
+
fetch(iconUrl)
|
|
141
|
+
.then((res) => {
|
|
142
|
+
if (!res.ok) throw new Error('Failed to fetch icon')
|
|
143
|
+
return res.text()
|
|
144
|
+
})
|
|
145
|
+
.then((svgText) => {
|
|
146
|
+
setFetchedSvg(svgText)
|
|
147
|
+
setLoading(false)
|
|
148
|
+
})
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
console.warn('[Icon] Error fetching:', err)
|
|
151
|
+
setError(true)
|
|
152
|
+
setLoading(false)
|
|
153
|
+
})
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Built-in icon - no fetch needed
|
|
158
|
+
if (iconName && !iconLibrary && BUILT_IN_ICONS[iconName]) return
|
|
159
|
+
|
|
160
|
+
// Library-based resolution via Uniweb resolver
|
|
161
|
+
if (iconLibrary && iconName) {
|
|
162
|
+
const uniweb = getUniweb()
|
|
163
|
+
if (uniweb?.resolveIcon) {
|
|
164
|
+
setLoading(true)
|
|
165
|
+
uniweb
|
|
166
|
+
.resolveIcon(iconLibrary, iconName)
|
|
167
|
+
.then((svgContent) => {
|
|
168
|
+
if (svgContent) {
|
|
169
|
+
setFetchedSvg(svgContent)
|
|
170
|
+
} else {
|
|
171
|
+
setError(true)
|
|
172
|
+
}
|
|
173
|
+
setLoading(false)
|
|
174
|
+
})
|
|
175
|
+
.catch(() => {
|
|
176
|
+
setError(true)
|
|
177
|
+
setLoading(false)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}, [iconUrl, iconSvg, iconLibrary, iconName])
|
|
133
182
|
|
|
134
183
|
// Determine the SVG content to render
|
|
135
184
|
const svgData = useMemo(() => {
|
|
@@ -142,9 +191,9 @@ export function Icon({
|
|
|
142
191
|
return parseSvg(fetchedSvg)
|
|
143
192
|
}
|
|
144
193
|
|
|
145
|
-
// Built-in icons
|
|
194
|
+
// Built-in icons (only if no library specified)
|
|
146
195
|
const builtInName = iconName || name
|
|
147
|
-
if (builtInName && BUILT_IN_ICONS[builtInName]) {
|
|
196
|
+
if (builtInName && !iconLibrary && BUILT_IN_ICONS[builtInName]) {
|
|
148
197
|
return {
|
|
149
198
|
viewBox: '0 0 24 24',
|
|
150
199
|
content: BUILT_IN_ICONS[builtInName],
|
|
@@ -153,15 +202,19 @@ export function Icon({
|
|
|
153
202
|
}
|
|
154
203
|
|
|
155
204
|
return null
|
|
156
|
-
}, [iconSvg, fetchedSvg, iconName, name])
|
|
205
|
+
}, [iconSvg, fetchedSvg, iconName, name, iconLibrary])
|
|
157
206
|
|
|
158
|
-
// Loading state
|
|
207
|
+
// Loading state - SSR-safe placeholder
|
|
159
208
|
if (loading) {
|
|
160
|
-
return
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
209
|
+
return (
|
|
210
|
+
loadingComponent || (
|
|
211
|
+
<span
|
|
212
|
+
className={cn('inline-flex items-center justify-center', className)}
|
|
213
|
+
style={{ width: size, height: size }}
|
|
214
|
+
role="img"
|
|
215
|
+
aria-hidden="true"
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
165
218
|
)
|
|
166
219
|
}
|
|
167
220
|
|
|
@@ -186,6 +239,17 @@ export function Icon({
|
|
|
186
239
|
/>
|
|
187
240
|
)
|
|
188
241
|
}
|
|
242
|
+
// Library icon pending resolution - render placeholder
|
|
243
|
+
if (iconLibrary && iconName) {
|
|
244
|
+
return (
|
|
245
|
+
<span
|
|
246
|
+
className={cn('inline-flex items-center justify-center', className)}
|
|
247
|
+
style={{ width: size, height: size }}
|
|
248
|
+
role="img"
|
|
249
|
+
aria-hidden="true"
|
|
250
|
+
/>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
189
253
|
return null
|
|
190
254
|
}
|
|
191
255
|
|
|
@@ -199,7 +263,7 @@ export function Icon({
|
|
|
199
263
|
return (
|
|
200
264
|
<svg
|
|
201
265
|
viewBox={svgData.viewBox}
|
|
202
|
-
fill={svgData.isBuiltIn ? 'none' :
|
|
266
|
+
fill={svgData.isBuiltIn ? 'none' : preserveColors ? undefined : 'currentColor'}
|
|
203
267
|
stroke={svgData.isBuiltIn ? 'currentColor' : undefined}
|
|
204
268
|
className={cn('inline-block', className)}
|
|
205
269
|
style={style}
|