@uniweb/kit 0.4.1 → 0.4.3
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 +71 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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.3.
|
|
42
|
+
"@uniweb/core": "0.3.3"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -38,24 +38,47 @@ const BUILT_IN_ICONS = {
|
|
|
38
38
|
* @param {string} svgContent - Raw SVG string
|
|
39
39
|
* @returns {Object} { viewBox, content, width, height }
|
|
40
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Extract an attribute value from an SVG opening tag string
|
|
43
|
+
*/
|
|
44
|
+
function getAttr(svgTag, name) {
|
|
45
|
+
const match = svgTag.match(new RegExp(`${name}="([^"]*)"`, 'i'))
|
|
46
|
+
return match ? match[1] : null
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
function parseSvg(svgContent) {
|
|
42
50
|
if (!svgContent) return null
|
|
43
51
|
|
|
44
52
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
53
|
+
// Extract the <svg ...> opening tag and inner content
|
|
54
|
+
// Works in both browser and Node.js (no DOMParser needed)
|
|
55
|
+
const svgTagMatch = svgContent.match(/<svg\s[^>]*>/i)
|
|
56
|
+
if (!svgTagMatch) return null
|
|
57
|
+
|
|
58
|
+
const svgTag = svgTagMatch[0]
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
const viewBox = getAttr(svgTag, 'viewBox') || '0 0 24 24'
|
|
61
|
+
const width = getAttr(svgTag, 'width')
|
|
62
|
+
const height = getAttr(svgTag, 'height')
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
// Preserve SVG presentation attributes from the source
|
|
65
|
+
// Different icon families use different rendering styles:
|
|
66
|
+
// - Lucide, Feather, Heroicons: stroke-based (fill="none", stroke="currentColor")
|
|
67
|
+
// - Font Awesome, Bootstrap: fill-based (fill="currentColor")
|
|
68
|
+
const fill = getAttr(svgTag, 'fill')
|
|
69
|
+
const stroke = getAttr(svgTag, 'stroke')
|
|
70
|
+
const strokeWidth = getAttr(svgTag, 'stroke-width')
|
|
71
|
+
const strokeLinecap = getAttr(svgTag, 'stroke-linecap')
|
|
72
|
+
const strokeLinejoin = getAttr(svgTag, 'stroke-linejoin')
|
|
54
73
|
|
|
55
|
-
// Get inner content
|
|
56
|
-
const
|
|
74
|
+
// Get inner content (everything between <svg> and </svg>)
|
|
75
|
+
const innerMatch = svgContent.match(/<svg\s[^>]*>([\s\S]*)<\/svg>/i)
|
|
76
|
+
const content = innerMatch ? innerMatch[1] : ''
|
|
57
77
|
|
|
58
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
viewBox, content, width, height,
|
|
80
|
+
fill, stroke, strokeWidth, strokeLinecap, strokeLinejoin
|
|
81
|
+
}
|
|
59
82
|
} catch (error) {
|
|
60
83
|
console.warn('[Icon] Error parsing SVG:', error)
|
|
61
84
|
return null
|
|
@@ -115,25 +138,37 @@ export function Icon({
|
|
|
115
138
|
errorComponent,
|
|
116
139
|
...props
|
|
117
140
|
}) {
|
|
118
|
-
const [fetchedSvg, setFetchedSvg] = useState(null)
|
|
119
|
-
const [loading, setLoading] = useState(false)
|
|
120
|
-
const [error, setError] = useState(false)
|
|
121
|
-
|
|
122
141
|
// Normalize props (handle legacy icon object)
|
|
123
142
|
const iconLibrary = library || (typeof icon === 'object' ? icon.library : null)
|
|
124
143
|
const iconUrl = url || (typeof icon === 'string' ? icon : icon?.url)
|
|
125
144
|
const iconSvg = svg || (typeof icon === 'object' ? icon.svg : null)
|
|
126
145
|
const iconName = name || (typeof icon === 'object' ? icon.name : null)
|
|
127
146
|
|
|
128
|
-
//
|
|
147
|
+
// Check sync cache for SSR (pre-populated by prerender)
|
|
148
|
+
const cachedSvg = useMemo(() => {
|
|
149
|
+
if (iconLibrary && iconName) {
|
|
150
|
+
const uniweb = getUniweb()
|
|
151
|
+
return uniweb?.getIconSync?.(iconLibrary, iconName) || null
|
|
152
|
+
}
|
|
153
|
+
return null
|
|
154
|
+
}, [iconLibrary, iconName])
|
|
155
|
+
|
|
156
|
+
const [fetchedSvg, setFetchedSvg] = useState(cachedSvg)
|
|
157
|
+
const [loading, setLoading] = useState(false)
|
|
158
|
+
const [error, setError] = useState(false)
|
|
159
|
+
|
|
160
|
+
// Fetch SVG from URL or resolve from library (client-side only)
|
|
129
161
|
useEffect(() => {
|
|
162
|
+
// Already have SVG from cache or direct prop
|
|
163
|
+
if (cachedSvg || iconSvg) {
|
|
164
|
+
setFetchedSvg(cachedSvg)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
130
168
|
// Reset state when source changes
|
|
131
169
|
setFetchedSvg(null)
|
|
132
170
|
setError(false)
|
|
133
171
|
|
|
134
|
-
// Direct SVG - no fetch needed
|
|
135
|
-
if (iconSvg) return
|
|
136
|
-
|
|
137
172
|
// URL-based fetch
|
|
138
173
|
if (iconUrl) {
|
|
139
174
|
setLoading(true)
|
|
@@ -178,7 +213,7 @@ export function Icon({
|
|
|
178
213
|
})
|
|
179
214
|
}
|
|
180
215
|
}
|
|
181
|
-
}, [iconUrl, iconSvg, iconLibrary, iconName])
|
|
216
|
+
}, [iconUrl, iconSvg, iconLibrary, iconName, cachedSvg])
|
|
182
217
|
|
|
183
218
|
// Determine the SVG content to render
|
|
184
219
|
const svgData = useMemo(() => {
|
|
@@ -260,11 +295,26 @@ export function Icon({
|
|
|
260
295
|
...(color && !preserveColors ? { color } : {})
|
|
261
296
|
}
|
|
262
297
|
|
|
298
|
+
// Determine fill/stroke from source SVG, built-in defaults, or fallback
|
|
299
|
+
const svgFill = svgData.isBuiltIn
|
|
300
|
+
? 'none'
|
|
301
|
+
: preserveColors
|
|
302
|
+
? undefined
|
|
303
|
+
: svgData.fill ?? 'currentColor'
|
|
304
|
+
const svgStroke = svgData.isBuiltIn
|
|
305
|
+
? 'currentColor'
|
|
306
|
+
: preserveColors
|
|
307
|
+
? undefined
|
|
308
|
+
: svgData.stroke ?? undefined
|
|
309
|
+
|
|
263
310
|
return (
|
|
264
311
|
<svg
|
|
265
312
|
viewBox={svgData.viewBox}
|
|
266
|
-
fill={
|
|
267
|
-
stroke={
|
|
313
|
+
fill={svgFill}
|
|
314
|
+
stroke={svgStroke}
|
|
315
|
+
strokeWidth={svgData.strokeWidth ?? undefined}
|
|
316
|
+
strokeLinecap={svgData.strokeLinecap ?? undefined}
|
|
317
|
+
strokeLinejoin={svgData.strokeLinejoin ?? undefined}
|
|
268
318
|
className={cn('inline-block', className)}
|
|
269
319
|
style={style}
|
|
270
320
|
role="img"
|