@uniweb/kit 0.3.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/kit",
3
- "version": "0.3.2",
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.2.5"
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.name] - Built-in icon name or custom name
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
- // Handle legacy icon prop
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
- if (!iconUrl) return
114
-
115
- setLoading(true)
130
+ // Reset state when source changes
131
+ setFetchedSvg(null)
116
132
  setError(false)
117
133
 
118
- fetch(iconUrl)
119
- .then((res) => {
120
- if (!res.ok) throw new Error('Failed to fetch icon')
121
- return res.text()
122
- })
123
- .then((svgText) => {
124
- setFetchedSvg(svgText)
125
- setLoading(false)
126
- })
127
- .catch((err) => {
128
- console.warn('[Icon] Error fetching:', err)
129
- setError(true)
130
- setLoading(false)
131
- })
132
- }, [iconUrl])
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 loadingComponent || (
161
- <span
162
- className={cn('inline-block animate-pulse bg-gray-200 rounded', className)}
163
- style={{ width: size, height: size }}
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' : (preserveColors ? undefined : 'currentColor')}
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}