@retray-dev/ui-kit 10.1.0 → 10.2.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.
Files changed (112) hide show
  1. package/COMPONENTS.md +136 -5
  2. package/README.md +4 -4
  3. package/dist/Accordion.js +1 -1
  4. package/dist/Accordion.mjs +2 -2
  5. package/dist/AlertBanner.js +1 -1
  6. package/dist/AlertBanner.mjs +2 -2
  7. package/dist/AppHeader.js +1 -1
  8. package/dist/AppHeader.mjs +3 -3
  9. package/dist/Badge.js +1 -1
  10. package/dist/Badge.mjs +2 -2
  11. package/dist/Button.js +1 -1
  12. package/dist/Button.mjs +2 -2
  13. package/dist/CategoryStrip.js +1 -1
  14. package/dist/CategoryStrip.mjs +2 -2
  15. package/dist/Chip.js +1 -1
  16. package/dist/Chip.mjs +2 -2
  17. package/dist/ConfirmDialog.js +1 -1
  18. package/dist/ConfirmDialog.mjs +3 -3
  19. package/dist/CurrencyInput.js +1 -1
  20. package/dist/CurrencyInput.mjs +3 -3
  21. package/dist/DetailRow.d.mts +1 -1
  22. package/dist/DetailRow.d.ts +1 -1
  23. package/dist/DetailRow.js +1 -1
  24. package/dist/DetailRow.mjs +2 -2
  25. package/dist/EmptyState.js +1 -1
  26. package/dist/EmptyState.mjs +3 -3
  27. package/dist/ErrorBoundary.js +1 -1
  28. package/dist/ErrorBoundary.mjs +2 -2
  29. package/dist/IconButton.js +1 -1
  30. package/dist/IconButton.mjs +2 -2
  31. package/dist/IconPicker.d.mts +17 -0
  32. package/dist/IconPicker.d.ts +17 -0
  33. package/dist/IconPicker.js +997 -0
  34. package/dist/IconPicker.mjs +7 -0
  35. package/dist/ImageUpload.d.mts +3 -1
  36. package/dist/ImageUpload.d.ts +3 -1
  37. package/dist/ImageUpload.js +28 -10
  38. package/dist/ImageUpload.mjs +1 -1
  39. package/dist/ImageViewer.js +1 -1
  40. package/dist/ImageViewer.mjs +4 -4
  41. package/dist/Input.js +1 -1
  42. package/dist/Input.mjs +2 -2
  43. package/dist/LabelValue.js +1 -1
  44. package/dist/LabelValue.mjs +2 -2
  45. package/dist/ListItem.js +1 -1
  46. package/dist/ListItem.mjs +2 -2
  47. package/dist/MediaCard.js +1 -1
  48. package/dist/MediaCard.mjs +2 -2
  49. package/dist/MenuItem.js +1 -1
  50. package/dist/MenuItem.mjs +2 -2
  51. package/dist/NumberStepper.d.mts +19 -0
  52. package/dist/NumberStepper.d.ts +19 -0
  53. package/dist/NumberStepper.js +410 -0
  54. package/dist/NumberStepper.mjs +9 -0
  55. package/dist/PagerDots.js +1 -1
  56. package/dist/PagerDots.mjs +2 -2
  57. package/dist/PricingCard.js +1 -1
  58. package/dist/PricingCard.mjs +4 -4
  59. package/dist/SelectableGrid.js +1 -1
  60. package/dist/SelectableGrid.mjs +2 -2
  61. package/dist/SheetSelect.js +1 -1
  62. package/dist/SheetSelect.mjs +2 -2
  63. package/dist/TabBar.js +1 -1
  64. package/dist/TabBar.mjs +2 -2
  65. package/dist/Textarea.js +1 -1
  66. package/dist/Textarea.mjs +2 -2
  67. package/dist/Toggle.js +1 -1
  68. package/dist/Toggle.mjs +2 -2
  69. package/dist/chunk-53Z3NYGE.mjs +742 -0
  70. package/dist/{chunk-VQ57HWPL.mjs → chunk-6L4G6PBT.mjs} +1 -1
  71. package/dist/{chunk-6OAZJ577.mjs → chunk-6SECQ2ZF.mjs} +2 -2
  72. package/dist/{chunk-KIHCWCWL.mjs → chunk-7LWRKMF5.mjs} +1 -1
  73. package/dist/{chunk-4I7D47FH.mjs → chunk-AJRVDP2H.mjs} +3 -3
  74. package/dist/{chunk-6MKGPAR2.mjs → chunk-BEMIQXXU.mjs} +1 -1
  75. package/dist/chunk-BUMAMSTZ.mjs +126 -0
  76. package/dist/{chunk-UREA2GYY.mjs → chunk-DYT7BG5I.mjs} +1 -1
  77. package/dist/{chunk-WOEYDUJZ.mjs → chunk-ELXBDILQ.mjs} +2 -2
  78. package/dist/{chunk-A4MDAP7G.mjs → chunk-FCSSQK3L.mjs} +1 -1
  79. package/dist/{chunk-2TFTAWVJ.mjs → chunk-HTHGSXFG.mjs} +1 -1
  80. package/dist/{chunk-VGTDN7SW.mjs → chunk-IX3NYLYQ.mjs} +1 -1
  81. package/dist/{chunk-T7XZ7H7Y.mjs → chunk-KA7LTET3.mjs} +17 -3
  82. package/dist/{chunk-URI2WBIV.mjs → chunk-KOO4WITD.mjs} +1 -1
  83. package/dist/{chunk-JUXSWN54.mjs → chunk-NMU5FMQJ.mjs} +1 -1
  84. package/dist/{chunk-LXJIIOYQ.mjs → chunk-RYZC432S.mjs} +1 -1
  85. package/dist/{chunk-JB67UOB5.mjs → chunk-S2R7UVOE.mjs} +1 -1
  86. package/dist/{chunk-ZUR7AU5R.mjs → chunk-SXLKNTA4.mjs} +1 -1
  87. package/dist/{chunk-3U4SSNWP.mjs → chunk-T2KCAHOS.mjs} +1 -1
  88. package/dist/{chunk-ZJKGQMYH.mjs → chunk-TB6SD2FT.mjs} +1 -1
  89. package/dist/{chunk-AZJF2BLK.mjs → chunk-TBNZHU6C.mjs} +1 -1
  90. package/dist/{chunk-Y4GL2MHX.mjs → chunk-TZDGAP5N.mjs} +28 -10
  91. package/dist/{chunk-CZCQZHG6.mjs → chunk-U2XJFYED.mjs} +1 -1
  92. package/dist/{chunk-TERDKCLE.mjs → chunk-VF2ATYN3.mjs} +1 -1
  93. package/dist/{chunk-OHBNABL5.mjs → chunk-VKID2D2I.mjs} +1 -1
  94. package/dist/{chunk-KZL5VTYK.mjs → chunk-WYEUNUTP.mjs} +1 -1
  95. package/dist/{chunk-DJ7RN37L.mjs → chunk-YJ7I257J.mjs} +1 -1
  96. package/dist/{chunk-NA7PARID.mjs → chunk-Z4VHZ7B5.mjs} +1 -1
  97. package/dist/{chunk-MLF3EZFW.mjs → chunk-Z6SFHN6T.mjs} +1 -1
  98. package/dist/{chunk-4K625MVM.mjs → chunk-ZZ2R6KZ3.mjs} +1 -1
  99. package/dist/index.d.mts +4 -1
  100. package/dist/index.d.ts +4 -1
  101. package/dist/index.js +892 -12
  102. package/dist/index.mjs +33 -31
  103. package/package.json +1 -1
  104. package/src/components/DetailRow/DetailRow.tsx +1 -1
  105. package/src/components/IconPicker/IconPicker.tsx +383 -0
  106. package/src/components/IconPicker/index.ts +1 -0
  107. package/src/components/ImageUpload/ImageUpload.tsx +34 -12
  108. package/src/components/NumberStepper/NumberStepper.tsx +147 -0
  109. package/src/components/NumberStepper/index.ts +1 -0
  110. package/src/index.ts +3 -1
  111. package/src/utils/curatedIcons.ts +286 -0
  112. package/src/utils/icons.ts +20 -2
@@ -0,0 +1,147 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { impactLight } from '../../utils/haptics'
4
+ import { useTheme } from '../../theme'
5
+ import { s, ms, mvs } from '../../utils/scaling'
6
+ import { renderIcon } from '../../utils/icons'
7
+ import { RADIUS } from '../../tokens'
8
+ import { PressableButton } from '../../utils/pressable'
9
+
10
+ export type NumberStepperSize = 'sm' | 'md' | 'lg'
11
+
12
+ export interface NumberStepperProps {
13
+ value: number
14
+ onValueChange: (value: number) => void
15
+ min?: number
16
+ max?: number
17
+ step?: number
18
+ size?: NumberStepperSize
19
+ disabled?: boolean
20
+ style?: ViewStyle
21
+ accessibilityLabel?: string
22
+ }
23
+
24
+ const sizeConfig: Record<NumberStepperSize, { button: number; icon: number; valueFontSize: number; valueLineHeight: number; valueMinWidth: number }> = {
25
+ sm: { button: s(40), icon: 16, valueFontSize: ms(18), valueLineHeight: mvs(24), valueMinWidth: s(32) },
26
+ md: { button: s(44), icon: 18, valueFontSize: ms(22), valueLineHeight: mvs(28), valueMinWidth: s(36) },
27
+ lg: { button: s(52), icon: 22, valueFontSize: ms(26), valueLineHeight: mvs(32), valueMinWidth: s(40) },
28
+ }
29
+
30
+ function NumberStepperBase({
31
+ value,
32
+ onValueChange,
33
+ min = 1,
34
+ max = 99,
35
+ step = 1,
36
+ size = 'md',
37
+ disabled = false,
38
+ style,
39
+ accessibilityLabel,
40
+ }: NumberStepperProps) {
41
+ const { colors } = useTheme()
42
+
43
+ const canDecrement = value > min && !disabled
44
+ const canIncrement = value < max && !disabled
45
+
46
+ const handleDecrement = () => {
47
+ if (!canDecrement) return
48
+ impactLight()
49
+ onValueChange(Math.max(min, value - step))
50
+ }
51
+
52
+ const handleIncrement = () => {
53
+ if (!canIncrement) return
54
+ impactLight()
55
+ onValueChange(Math.min(max, value + step))
56
+ }
57
+
58
+ const { button: buttonSize, icon: iconSize, valueFontSize, valueLineHeight, valueMinWidth } = sizeConfig[size]
59
+
60
+ const displayValue = String(value)
61
+
62
+ return (
63
+ <View style={[styles.container, style]}>
64
+ <PressableButton
65
+ style={[
66
+ styles.button,
67
+ {
68
+ width: buttonSize,
69
+ height: buttonSize,
70
+ backgroundColor: colors.surface,
71
+ borderColor: colors.border,
72
+ },
73
+ !canDecrement && styles.buttonDisabled,
74
+ ]}
75
+ enabled={canDecrement}
76
+ onPress={handleDecrement}
77
+ rippleColor="transparent"
78
+ touchSoundDisabled
79
+ accessibilityRole="button"
80
+ accessibilityLabel={`Decrease, current value ${displayValue}`}
81
+ accessibilityState={{ disabled: !canDecrement }}
82
+ >
83
+ {renderIcon('minus', iconSize, canDecrement ? colors.foreground : colors.foregroundMuted)}
84
+ </PressableButton>
85
+ <Text
86
+ style={[
87
+ styles.value,
88
+ {
89
+ color: colors.foreground,
90
+ fontSize: valueFontSize,
91
+ lineHeight: valueLineHeight,
92
+ minWidth: valueMinWidth,
93
+ },
94
+ ]}
95
+ allowFontScaling={true}
96
+ accessibilityLabel={accessibilityLabel ?? `Quantity: ${displayValue}`}
97
+ accessibilityRole="text"
98
+ >
99
+ {displayValue}
100
+ </Text>
101
+ <PressableButton
102
+ style={[
103
+ styles.button,
104
+ {
105
+ width: buttonSize,
106
+ height: buttonSize,
107
+ backgroundColor: colors.surface,
108
+ borderColor: colors.border,
109
+ },
110
+ !canIncrement && styles.buttonDisabled,
111
+ ]}
112
+ enabled={canIncrement}
113
+ onPress={handleIncrement}
114
+ rippleColor="transparent"
115
+ touchSoundDisabled
116
+ accessibilityRole="button"
117
+ accessibilityLabel={`Increase, current value ${displayValue}`}
118
+ accessibilityState={{ disabled: !canIncrement }}
119
+ >
120
+ {renderIcon('plus', iconSize, canIncrement ? colors.foreground : colors.foregroundMuted)}
121
+ </PressableButton>
122
+ </View>
123
+ )
124
+ }
125
+
126
+ export const NumberStepper = React.memo(NumberStepperBase)
127
+
128
+ const styles = StyleSheet.create({
129
+ container: {
130
+ flexDirection: 'row',
131
+ alignItems: 'center',
132
+ gap: s(12),
133
+ },
134
+ button: {
135
+ borderRadius: RADIUS.md,
136
+ alignItems: 'center',
137
+ justifyContent: 'center',
138
+ borderWidth: 1.5,
139
+ },
140
+ buttonDisabled: {
141
+ opacity: 0.35,
142
+ },
143
+ value: {
144
+ fontFamily: 'Sohne-Medium',
145
+ textAlign: 'center',
146
+ },
147
+ })
@@ -0,0 +1 @@
1
+ export * from './NumberStepper'
package/src/index.ts CHANGED
@@ -55,12 +55,14 @@ export * from './components/TabBar'
55
55
  export * from './components/ImageViewer'
56
56
  export * from './components/SheetSelect'
57
57
  export * from './components/ImageUpload'
58
+ export * from './components/IconPicker'
59
+ export * from './components/NumberStepper'
58
60
  // HolographicCard is intentionally NOT re-exported here — it depends on the
59
61
  // optional peer @shopify/react-native-skia, so it must stay out of the main
60
62
  // barrel's module graph. Deep-import it: '@retray-dev/ui-kit/HolographicCard'.
61
63
 
62
64
  // Icon utility
63
- export { Icon, renderIcon, configureIconFamilies } from './utils/icons'
65
+ export { Icon, renderIcon, configureIconFamilies, getValidIconNames } from './utils/icons'
64
66
 
65
67
  // Typography utilities
66
68
  export { getResponsiveFontSize } from './utils/typography'
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Curated icon set — ~320 most useful icons organized by category.
3
+ * Ordered by usage probability: most-common categories first, niche last.
4
+ * All names resolve across 6 @expo/vector-icons families via renderIcon().
5
+ */
6
+
7
+ export interface IconCategory {
8
+ name: string
9
+ label: string
10
+ labelEs: string
11
+ /** Icon that represents this category in the tab bar. */
12
+ categoryIcon: string
13
+ icons: string[]
14
+ }
15
+
16
+ export const CURATED_ICONS: IconCategory[] = [
17
+ {
18
+ name: 'food',
19
+ label: 'Food',
20
+ labelEs: 'Comida',
21
+ categoryIcon: 'coffee',
22
+ icons: [
23
+ // Bebidas
24
+ 'coffee',
25
+ 'wine-glass',
26
+ 'beer',
27
+ 'cocktail',
28
+ 'mug-hot',
29
+ 'glass-cheers',
30
+ 'wine',
31
+ // Comidas
32
+ 'restaurant',
33
+ 'hamburger',
34
+ 'pizza-slice',
35
+ 'ice-cream',
36
+ 'cake',
37
+ 'egg',
38
+ 'fish',
39
+ 'bread-slice',
40
+ 'cookie',
41
+ 'cheese',
42
+ 'carrot',
43
+ 'hotdog',
44
+ // Cocina / utilidades
45
+ 'utensils',
46
+ // Delivery / pedidos
47
+ 'shopping-bag',
48
+ 'clock',
49
+ 'star',
50
+ 'heart',
51
+ 'map-pin',
52
+ 'phone',
53
+ ],
54
+ },
55
+ {
56
+ name: 'sports',
57
+ label: 'Sports',
58
+ labelEs: 'Deportes',
59
+ categoryIcon: 'activity',
60
+ icons: [
61
+ // Deportes específicos
62
+ 'futbol',
63
+ 'dumbbell',
64
+ 'running',
65
+ 'bicycle',
66
+ 'heartbeat',
67
+ 'fitness-center',
68
+ // Competencia / logros
69
+ 'target',
70
+ 'award',
71
+ 'flag',
72
+ 'star',
73
+ 'zap',
74
+ // Bienestar
75
+ 'heart',
76
+ 'clock',
77
+ 'activity',
78
+ 'sun',
79
+ 'user',
80
+ 'users',
81
+ ],
82
+ },
83
+ {
84
+ name: 'business',
85
+ label: 'Business',
86
+ labelEs: 'Negocios',
87
+ categoryIcon: 'briefcase',
88
+ icons: [
89
+ 'shopping-cart', 'shopping-bag',
90
+ 'credit-card',
91
+ 'dollar-sign', 'percent',
92
+ 'briefcase',
93
+ 'truck', 'package',
94
+ 'gift',
95
+ 'bar-chart', 'bar-chart-2', 'pie-chart',
96
+ 'trending-up', 'trending-down',
97
+ 'activity',
98
+ 'tag',
99
+ 'bookmark',
100
+ 'pocket',
101
+ ],
102
+ },
103
+ {
104
+ name: 'objects',
105
+ label: 'Objects',
106
+ labelEs: 'Objetos',
107
+ categoryIcon: 'folder',
108
+ icons: [
109
+ 'file', 'file-text', 'file-plus', 'file-minus',
110
+ 'folder', 'folder-plus', 'folder-minus',
111
+ 'lock', 'unlock', 'key',
112
+ 'shield', 'shield-off',
113
+ 'settings',
114
+ 'sliders', 'toggle-left', 'toggle-right',
115
+ 'tool',
116
+ 'printer',
117
+ 'database', 'server', 'hard-drive',
118
+ 'cpu',
119
+ 'paperclip',
120
+ 'aperture', 'box',
121
+ 'radio',
122
+ ],
123
+ },
124
+ {
125
+ name: 'status',
126
+ label: 'Status',
127
+ labelEs: 'Estado',
128
+ categoryIcon: 'alert-circle',
129
+ icons: [
130
+ 'alert-circle', 'alert-triangle', 'alert-octagon',
131
+ 'info', 'help-circle',
132
+ 'bell', 'bell-off',
133
+ 'eye', 'eye-off',
134
+ 'flag',
135
+ 'zap', 'zap-off',
136
+ 'loader', 'clock', 'watch',
137
+ ],
138
+ },
139
+ {
140
+ name: 'actions',
141
+ label: 'Actions',
142
+ labelEs: 'Acciones',
143
+ categoryIcon: 'edit-3',
144
+ icons: [
145
+ 'plus', 'plus-circle', 'plus-square',
146
+ 'minus', 'minus-circle', 'minus-square',
147
+ 'x', 'x-circle', 'x-square', 'x-octagon',
148
+ 'check', 'check-circle', 'check-square',
149
+ 'edit', 'edit-2', 'edit-3',
150
+ 'trash', 'trash-2',
151
+ 'copy', 'clipboard', 'scissors',
152
+ 'download', 'download-cloud', 'upload', 'upload-cloud',
153
+ 'share', 'share-2', 'link', 'link-2',
154
+ 'search', 'zoom-in', 'zoom-out',
155
+ 'save', 'archive',
156
+ 'log-in', 'log-out', 'power',
157
+ 'refresh-cw', 'rotate-cw',
158
+ 'slash',
159
+ ],
160
+ },
161
+ {
162
+ name: 'communication',
163
+ label: 'Communication',
164
+ labelEs: 'Comunicación',
165
+ categoryIcon: 'message-circle',
166
+ icons: [
167
+ 'mail',
168
+ 'message-circle', 'message-square',
169
+ 'send',
170
+ 'phone', 'phone-call', 'phone-incoming', 'phone-outgoing',
171
+ 'phone-missed', 'phone-off', 'phone-forwarded',
172
+ 'at-sign', 'inbox',
173
+ 'user', 'user-plus', 'user-minus', 'user-check', 'user-x',
174
+ 'users',
175
+ 'smile', 'frown', 'meh',
176
+ 'heart', 'thumbs-up', 'thumbs-down',
177
+ 'star',
178
+ 'award',
179
+ ],
180
+ },
181
+ {
182
+ name: 'navigation',
183
+ label: 'Navigation',
184
+ labelEs: 'Navegación',
185
+ categoryIcon: 'compass',
186
+ icons: [
187
+ 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right',
188
+ 'arrow-up-left', 'arrow-up-right', 'arrow-down-left', 'arrow-down-right',
189
+ 'chevron-up', 'chevron-down', 'chevron-left', 'chevron-right',
190
+ 'chevrons-up', 'chevrons-down', 'chevrons-left', 'chevrons-right',
191
+ 'corner-up-left', 'corner-up-right', 'corner-down-left', 'corner-down-right',
192
+ 'corner-left-up', 'corner-left-down', 'corner-right-up', 'corner-right-down',
193
+ 'refresh-cw', 'refresh-ccw', 'rotate-cw', 'rotate-ccw',
194
+ 'navigation', 'navigation-2', 'compass', 'map', 'map-pin', 'target', 'crosshair',
195
+ 'home', 'maximize', 'maximize-2', 'minimize', 'minimize-2',
196
+ 'external-link', 'move', 'anchor',
197
+ ],
198
+ },
199
+ {
200
+ name: 'media',
201
+ label: 'Media',
202
+ labelEs: 'Medios',
203
+ categoryIcon: 'image',
204
+ icons: [
205
+ 'image', 'film', 'video', 'video-off',
206
+ 'play', 'play-circle', 'pause', 'pause-circle',
207
+ 'square', 'stop-circle',
208
+ 'music', 'headphones', 'speaker',
209
+ 'camera', 'camera-off',
210
+ 'mic', 'mic-off',
211
+ 'volume', 'volume-1', 'volume-2', 'volume-x',
212
+ 'fast-forward', 'rewind', 'skip-forward', 'skip-back',
213
+ 'repeat', 'shuffle',
214
+ 'disc',
215
+ ],
216
+ },
217
+ {
218
+ name: 'layout',
219
+ label: 'Layout',
220
+ labelEs: 'Diseño',
221
+ categoryIcon: 'grid',
222
+ icons: [
223
+ 'menu', 'more-horizontal', 'more-vertical',
224
+ 'grid', 'columns', 'sidebar', 'layout',
225
+ 'list',
226
+ 'align-left', 'align-center', 'align-right', 'align-justify',
227
+ 'bold', 'italic', 'underline',
228
+ 'type',
229
+ 'filter',
230
+ 'crop',
231
+ 'layers',
232
+ 'hash',
233
+ 'table', 'trello',
234
+ 'circle', 'square', 'triangle', 'hexagon', 'octagon',
235
+ 'feather', 'pen-tool',
236
+ ],
237
+ },
238
+ {
239
+ name: 'nature',
240
+ label: 'Nature',
241
+ labelEs: 'Naturaleza',
242
+ categoryIcon: 'sun',
243
+ icons: [
244
+ 'sun', 'sunrise', 'sunset', 'moon',
245
+ 'cloud', 'cloud-drizzle', 'cloud-lightning',
246
+ 'cloud-off', 'cloud-rain', 'cloud-snow',
247
+ 'wind', 'umbrella', 'thermometer',
248
+ 'droplet',
249
+ 'life-buoy',
250
+ ],
251
+ },
252
+ {
253
+ name: 'brands',
254
+ label: 'Brands',
255
+ labelEs: 'Marcas',
256
+ categoryIcon: 'globe',
257
+ icons: [
258
+ 'github', 'gitlab',
259
+ 'twitter',
260
+ 'facebook', 'instagram',
261
+ 'linkedin',
262
+ 'youtube',
263
+ 'dribbble',
264
+ 'twitch', 'slack', 'figma', 'framer',
265
+ 'chrome', 'codepen', 'codesandbox',
266
+ 'globe', 'rss',
267
+ 'airplay', 'cast',
268
+ 'bluetooth', 'wifi', 'wifi-off',
269
+ 'battery', 'battery-charging',
270
+ 'monitor', 'tablet', 'smartphone', 'tv',
271
+ ],
272
+ },
273
+ ]
274
+
275
+ /** Flat deduplicated array of all curated icon names (for "Todos" / search). */
276
+ export const ALL_CURATED_ICONS: string[] = [
277
+ ...new Set(CURATED_ICONS.flatMap((c) => c.icons)),
278
+ ]
279
+
280
+ /** Resolve icon to its category label, or undefined if not found. */
281
+ export function getIconCategory(name: string): string | undefined {
282
+ for (const cat of CURATED_ICONS) {
283
+ if (cat.icons.includes(name)) return cat.label
284
+ }
285
+ return undefined
286
+ }
@@ -62,9 +62,9 @@ export function configureIconFamilies(families: IconFamily[]): void {
62
62
  resolvedCache = null // invalidate — rebuilt lazily on next resolve
63
63
  }
64
64
 
65
- function buildCache(): Map<string, IconFamilyEntry> {
65
+ function buildCache(families?: IconFamilyEntry[]): Map<string, IconFamilyEntry> {
66
66
  const cache = new Map<string, IconFamilyEntry>()
67
- for (const family of activeFamilies) {
67
+ for (const family of (families ?? activeFamilies)) {
68
68
  const glyphMap = family.getGlyphMap()
69
69
  for (const iconName of Object.keys(glyphMap)) {
70
70
  cache.set(iconName, family)
@@ -80,6 +80,24 @@ function resolveFamily(name: string): IconFamilyEntry | null {
80
80
  return resolvedCache.get(name) ?? null
81
81
  }
82
82
 
83
+ let cachedIconNames: string[] | null = null
84
+
85
+ export function getValidIconNames(families?: IconFamily[]): string[] {
86
+ if (families && families.length > 0) {
87
+ const tempFamilies = families
88
+ .map((n) => ALL_FAMILIES.find((f) => f.name === n))
89
+ .filter((f): f is IconFamilyEntry => f !== undefined)
90
+ if (tempFamilies.length === 0) return []
91
+ const cache = buildCache(tempFamilies)
92
+ return Array.from(cache.keys())
93
+ }
94
+ if (!cachedIconNames) {
95
+ const cache = buildCache()
96
+ cachedIconNames = Array.from(cache.keys())
97
+ }
98
+ return cachedIconNames
99
+ }
100
+
83
101
  export function Icon({ name, size, color, family }: IconProps): React.ReactElement | null {
84
102
  let resolved: IconFamilyEntry | null = null
85
103