@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/kit",
3
- "version": "0.4.1",
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.1"
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
- const parser = new DOMParser()
46
- const doc = parser.parseFromString(svgContent, 'image/svg+xml')
47
- const svg = doc.querySelector('svg')
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
- if (!svg) return null
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
- const viewBox = svg.getAttribute('viewBox') || '0 0 24 24'
52
- const width = svg.getAttribute('width')
53
- const height = svg.getAttribute('height')
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 content = svg.innerHTML
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 { viewBox, content, width, height }
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
- // Fetch SVG from URL or resolve from library
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={svgData.isBuiltIn ? 'none' : preserveColors ? undefined : 'currentColor'}
267
- stroke={svgData.isBuiltIn ? 'currentColor' : undefined}
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"