@idealyst/components 1.1.8 → 1.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 (46) hide show
  1. package/package.json +3 -3
  2. package/plugin/web.js +280 -532
  3. package/src/Accordion/Accordion.web.tsx +1 -3
  4. package/src/Alert/Alert.web.tsx +3 -4
  5. package/src/Badge/Badge.web.tsx +8 -15
  6. package/src/Breadcrumb/Breadcrumb.web.tsx +4 -8
  7. package/src/Button/Button.native.tsx +14 -21
  8. package/src/Button/Button.styles.tsx +15 -0
  9. package/src/Button/Button.web.tsx +9 -19
  10. package/src/Checkbox/Checkbox.web.tsx +1 -2
  11. package/src/Chip/Chip.web.tsx +3 -5
  12. package/src/Dialog/Dialog.web.tsx +3 -3
  13. package/src/Dialog/types.ts +1 -1
  14. package/src/Icon/Icon.web.tsx +22 -17
  15. package/src/Icon/IconRegistry.native.ts +41 -0
  16. package/src/Icon/IconRegistry.ts +107 -0
  17. package/src/Icon/IconSvg/IconSvg.web.tsx +26 -5
  18. package/src/Icon/icon-resolver.ts +12 -43
  19. package/src/Icon/index.native.ts +2 -1
  20. package/src/Icon/index.ts +1 -0
  21. package/src/Icon/index.web.ts +1 -0
  22. package/src/Input/Input.styles.tsx +56 -83
  23. package/src/Input/Input.web.tsx +5 -8
  24. package/src/List/ListItem.native.tsx +6 -7
  25. package/src/List/ListItem.web.tsx +3 -3
  26. package/src/Menu/MenuItem.web.tsx +3 -5
  27. package/src/Screen/Screen.native.tsx +1 -1
  28. package/src/Screen/Screen.styles.tsx +3 -6
  29. package/src/Screen/Screen.web.tsx +1 -1
  30. package/src/Select/Select.styles.tsx +31 -48
  31. package/src/Select/Select.web.tsx +45 -33
  32. package/src/Slider/Slider.web.tsx +2 -4
  33. package/src/Switch/Switch.native.tsx +2 -2
  34. package/src/Switch/Switch.web.tsx +2 -3
  35. package/src/Table/Table.native.tsx +168 -65
  36. package/src/Table/Table.styles.tsx +26 -33
  37. package/src/Table/Table.web.tsx +169 -70
  38. package/src/Text/Text.web.tsx +1 -0
  39. package/src/TextArea/TextArea.native.tsx +21 -8
  40. package/src/TextArea/TextArea.styles.tsx +15 -27
  41. package/src/TextArea/TextArea.web.tsx +17 -6
  42. package/src/View/View.native.tsx +33 -3
  43. package/src/View/View.web.tsx +4 -21
  44. package/src/View/types.ts +31 -3
  45. package/src/examples/ButtonExamples.tsx +20 -0
  46. package/src/index.ts +1 -1
@@ -3,7 +3,6 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { accordionStyles } from './Accordion.styles';
4
4
  import type { AccordionProps, AccordionItem as AccordionItemType } from './types';
5
5
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
- import { resolveIconPath } from '../Icon/icon-resolver';
7
6
  import { getWebAriaProps, generateAccessibilityId, ACCORDION_KEYS } from '../utils/accessibility';
8
7
 
9
8
  interface AccordionItemProps {
@@ -33,7 +32,6 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
33
32
  }) => {
34
33
  const contentInnerRef = useRef<HTMLDivElement>(null);
35
34
  const [contentHeight, setContentHeight] = useState(0);
36
- const chevronIconPath = resolveIconPath('chevron-down');
37
35
 
38
36
  // Apply item-specific variants (for size, expanded, disabled)
39
37
  accordionStyles.useVariants({
@@ -86,7 +84,7 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
86
84
  <span {...iconProps}>
87
85
  <IconSvg
88
86
  style={{ width: 12, height: 12 }}
89
- path={chevronIconPath}
87
+ name="chevron-down"
90
88
  aria-label="chevron-down"
91
89
  />
92
90
  </span>
@@ -3,7 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { alertStyles } from './Alert.styles';
4
4
  import type { AlertProps } from './types';
5
5
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
- import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
6
+ import { isIconName } from '../Icon/icon-resolver';
7
7
  import useMergeRefs from '../hooks/useMergeRefs';
8
8
 
9
9
  // Default icons for each intent
@@ -49,10 +49,9 @@ const Alert = forwardRef<HTMLDivElement, AlertProps>(({
49
49
  if (!iconProp) return null;
50
50
 
51
51
  if (isIconName(iconProp)) {
52
- const iconPath = resolveIconPath(iconProp);
53
52
  return (
54
53
  <IconSvg
55
- path={iconPath}
54
+ name={iconProp}
56
55
  {...iconContainerProps}
57
56
  aria-label={iconProp}
58
57
  />
@@ -106,7 +105,7 @@ const Alert = forwardRef<HTMLDivElement, AlertProps>(({
106
105
  type="button"
107
106
  >
108
107
  <IconSvg
109
- path={resolveIconPath('close')}
108
+ name="close"
110
109
  {...closeIconProps}
111
110
  aria-label="close"
112
111
  />
@@ -5,19 +5,13 @@ import { badgeStyles } from './Badge.styles';
5
5
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
6
  import useMergeRefs from '../hooks/useMergeRefs';
7
7
 
8
- // Extended props to include path props added by Babel plugin
9
- interface InternalBadgeProps extends BadgeProps {
10
- iconPath?: string;
11
- }
12
-
13
- const Badge = forwardRef<HTMLSpanElement, BadgeProps>((props: InternalBadgeProps, ref) => {
8
+ const Badge = forwardRef<HTMLSpanElement, BadgeProps>((props, ref) => {
14
9
  const {
15
10
  children,
16
11
  size = 'md',
17
12
  type = 'filled',
18
13
  color = 'blue',
19
14
  icon,
20
- iconPath,
21
15
  style,
22
16
  testID,
23
17
  id,
@@ -27,7 +21,7 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>((props: InternalBadgeProps
27
21
  size,
28
22
  type,
29
23
  });
30
-
24
+
31
25
  const badgeStyle = (badgeStyles.badge as any)({ color });
32
26
  const contentStyle = badgeStyles.content;
33
27
  const textStyle = (badgeStyles.text as any)({ color });
@@ -38,13 +32,12 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>((props: InternalBadgeProps
38
32
  const iconProps = getWebProps([badgeStyles.icon, textStyle]);
39
33
 
40
34
  // Helper to render icon
41
- const renderIcon = (iconProp: typeof icon, path?: string) => {
42
- if (typeof iconProp === 'string' && path) {
43
- // Render IconSvg directly with the path from Babel plugin
44
- // Don't pass size prop - let the style control the dimensions entirely
35
+ const renderIcon = (iconProp: typeof icon) => {
36
+ if (typeof iconProp === 'string') {
37
+ // Render IconSvg with the icon name - registry lookup happens inside
45
38
  return (
46
39
  <IconSvg
47
- path={path}
40
+ name={iconProp}
48
41
  {...iconProps}
49
42
  aria-label={iconProp}
50
43
  />
@@ -85,7 +78,7 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>((props: InternalBadgeProps
85
78
  >
86
79
  {hasIcon ? (
87
80
  <span {...contentProps}>
88
- {renderIcon(icon, iconPath)}
81
+ {renderIcon(icon)}
89
82
  <span {...textProps}>
90
83
  {children}
91
84
  </span>
@@ -101,4 +94,4 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>((props: InternalBadgeProps
101
94
 
102
95
  Badge.displayName = 'Badge';
103
96
 
104
- export default Badge;
97
+ export default Badge;
@@ -3,7 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { breadcrumbStyles } from './Breadcrumb.styles';
4
4
  import type { BreadcrumbProps, BreadcrumbItem as BreadcrumbItemType } from './types';
5
5
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
- import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
6
+ import { isIconName } from '../Icon/icon-resolver';
7
7
  import Menu from '../Menu/Menu.web';
8
8
  import type { MenuItem } from '../Menu/types';
9
9
 
@@ -48,11 +48,9 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
48
48
  if (!item.icon) return null;
49
49
 
50
50
  if (isIconName(item.icon)) {
51
- const iconPath = resolveIconPath(item.icon);
52
- // IconSvg uses size="1em" by default, sized by container's fontSize from styles
53
51
  return (
54
52
  <IconSvg
55
- path={iconPath}
53
+ name={item.icon}
56
54
  aria-label={item.icon}
57
55
  />
58
56
  );
@@ -138,12 +136,11 @@ const BreadcrumbEllipsis: React.FC<BreadcrumbEllipsisProps> = ({ size, intent })
138
136
  const ellipsisIconStyle = (breadcrumbStyles.ellipsisIcon as any)({ intent });
139
137
  const ellipsisProps = getWebProps([ellipsisStyle]);
140
138
  const iconProps = getWebProps([ellipsisIconStyle]);
141
- const ellipsisIconPath = resolveIconPath('dots-horizontal');
142
139
 
143
140
  return (
144
141
  <span {...ellipsisProps}>
145
142
  <IconSvg
146
- path={ellipsisIconPath}
143
+ name="dots-horizontal"
147
144
  {...iconProps}
148
145
  aria-label="more items"
149
146
  />
@@ -173,7 +170,6 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
173
170
  const menuButtonIconStyle = (breadcrumbStyles.menuButtonIcon as any)({ intent });
174
171
 
175
172
  const containerProps = getWebProps([containerStyle, style as any]);
176
- const menuIconPath = resolveIconPath('dots-horizontal');
177
173
 
178
174
  // Apply variants for menu button
179
175
  breadcrumbStyles.useVariants({ size });
@@ -250,7 +246,7 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
250
246
  aria-label="Show more breadcrumb items"
251
247
  >
252
248
  <IconSvg
253
- path={menuIconPath}
249
+ name="dots-horizontal"
254
250
  {...menuIconProps}
255
251
  aria-label="dots-horizontal"
256
252
  />
@@ -5,6 +5,7 @@ import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
5
5
  import { buttonStyles } from './Button.styles';
6
6
  import { ButtonProps } from './types';
7
7
  import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
8
+ import { useUnistyles } from 'react-native-unistyles';
8
9
 
9
10
  const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((props, ref) => {
10
11
  const {
@@ -71,24 +72,6 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
71
72
  } as const;
72
73
  const iconSize = iconSizeMap[size];
73
74
 
74
- // Helper to render icon - uses the icon styles from buttonStyles
75
- const renderIcon = (icon: string | React.ReactNode) => {
76
- if (typeof icon === 'string') {
77
- // Render MaterialCommunityIcons with explicit size prop
78
- // The icon styles provide the correct color based on dynamic styles
79
- return (
80
- <MaterialCommunityIcons
81
- name={icon}
82
- size={iconSize}
83
- style={iconStyle}
84
- />
85
- );
86
- } else if (isValidElement(icon)) {
87
- // Render custom component as-is
88
- return icon;
89
- }
90
- return null;
91
- };
92
75
 
93
76
  // Use children if available, otherwise use title
94
77
  const buttonContent = children || title;
@@ -199,12 +182,22 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
199
182
  <TouchableOpacity {...touchableProps as any}>
200
183
  {renderGradientLayer()}
201
184
  {hasIcons ? (
202
- <View style={iconContainerStyle}>
203
- {leftIcon && renderIcon(leftIcon)}
185
+ <View style={iconContainerStyle}>
186
+ {leftIcon &&
187
+ <MaterialCommunityIcons
188
+ name={leftIcon}
189
+ size={iconSize}
190
+ style={iconStyle}
191
+ />}
204
192
  <Text style={textStyle}>
205
193
  {buttonContent}
206
194
  </Text>
207
- {rightIcon && renderIcon(rightIcon)}
195
+ {rightIcon &&
196
+ <MaterialCommunityIcons
197
+ name={rightIcon}
198
+ size={iconSize}
199
+ style={iconStyle}
200
+ />}
208
201
  </View>
209
202
  ) : (
210
203
  <Text style={textStyle}>
@@ -68,6 +68,21 @@ export const buttonStyles = defineStyle('Button', (theme: Theme) => ({
68
68
  },
69
69
  variants: {
70
70
  // $iterator expands for each button size
71
+ type: {
72
+ contained: {
73
+ backgroundColor: theme.intents[intent].primary,
74
+ borderColor: 'transparent',
75
+ },
76
+ outlined: {
77
+ backgroundColor: 'transparent',
78
+ borderColor: theme.intents[intent].primary,
79
+ },
80
+ text: {
81
+ backgroundColor: 'transparent',
82
+ borderColor: 'transparent',
83
+ borderWidth: 0,
84
+ }
85
+ },
71
86
  size: {
72
87
  paddingVertical: theme.sizes.$button.paddingVertical,
73
88
  paddingHorizontal: theme.sizes.$button.paddingHorizontal,
@@ -6,14 +6,7 @@ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
6
  import useMergeRefs from '../hooks/useMergeRefs';
7
7
  import { getWebInteractiveAriaProps, generateAccessibilityId } from '../utils/accessibility';
8
8
 
9
- // Extended props to include path props added by Babel plugin
10
- interface InternalButtonProps extends ButtonProps {
11
- leftIconPath?: string;
12
- rightIconPath?: string;
13
- }
14
-
15
- const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButtonProps, ref) => {
16
-
9
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
17
10
  const {
18
11
  title,
19
12
  children,
@@ -25,8 +18,6 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
25
18
  gradient,
26
19
  leftIcon,
27
20
  rightIcon,
28
- leftIconPath,
29
- rightIconPath,
30
21
  style,
31
22
  testID,
32
23
  id,
@@ -124,14 +115,13 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
124
115
  const iconStyleArray = [(buttonStyles.icon as any)(dynamicProps)];
125
116
  const iconProps = getWebProps(iconStyleArray);
126
117
 
127
- // Helper to render icon
128
- const renderIcon = (icon: string | React.ReactNode, iconPath?: string) => {
129
- if (typeof icon === 'string' && iconPath) {
130
- // Render IconSvg directly with the path from Babel plugin
131
- // Don't pass size - let the style control the dimensions
118
+ // Helper to render icon - now uses icon name directly
119
+ const renderIcon = (icon: string | React.ReactNode) => {
120
+ if (typeof icon === 'string') {
121
+ // Render IconSvg with the icon name - registry lookup happens inside
132
122
  return (
133
123
  <IconSvg
134
- path={iconPath}
124
+ name={icon}
135
125
  {...iconProps}
136
126
  aria-label={icon}
137
127
  />
@@ -164,9 +154,9 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
164
154
  >
165
155
  {hasIcons ? (
166
156
  <div {...iconContainerProps}>
167
- {leftIcon && renderIcon(leftIcon, leftIconPath)}
157
+ {leftIcon && renderIcon(leftIcon)}
168
158
  {buttonContent}
169
- {rightIcon && renderIcon(rightIcon, rightIconPath)}
159
+ {rightIcon && renderIcon(rightIcon)}
170
160
  </div>
171
161
  ) : (
172
162
  buttonContent
@@ -177,4 +167,4 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
177
167
 
178
168
  Button.displayName = 'Button';
179
169
 
180
- export default Button;
170
+ export default Button;
@@ -3,7 +3,6 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { CheckboxProps } from './types';
4
4
  import { checkboxStyles } from './Checkbox.styles';
5
5
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
- import { resolveIconPath } from '../Icon/icon-resolver';
7
6
  import useMergeRefs from '../hooks/useMergeRefs';
8
7
  import { getWebSelectionAriaProps, generateAccessibilityId, combineIds } from '../utils/accessibility';
9
8
 
@@ -177,7 +176,7 @@ const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(({
177
176
  <div {...checkboxProps}>
178
177
  {(internalChecked || indeterminate) && (
179
178
  <IconSvg
180
- path={resolveIconPath(indeterminate ? 'minus' : 'check')}
179
+ name={indeterminate ? 'minus' : 'check'}
181
180
  {...checkmarkProps}
182
181
  aria-label={indeterminate ? 'minus' : 'check'}
183
182
  />
@@ -3,7 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { chipStyles } from './Chip.styles';
4
4
  import type { ChipProps } from './types';
5
5
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
- import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
6
+ import { isIconName } from '../Icon/icon-resolver';
7
7
  import useMergeRefs from '../hooks/useMergeRefs';
8
8
 
9
9
  const Chip = forwardRef<HTMLDivElement, ChipProps>(({
@@ -56,10 +56,9 @@ const Chip = forwardRef<HTMLDivElement, ChipProps>(({
56
56
  if (!icon) return null;
57
57
 
58
58
  if (isIconName(icon)) {
59
- const iconPath = resolveIconPath(icon);
60
59
  return (
61
60
  <IconSvg
62
- path={iconPath}
61
+ name={icon}
63
62
  {...iconProps}
64
63
  aria-label={icon}
65
64
  />
@@ -74,10 +73,9 @@ const Chip = forwardRef<HTMLDivElement, ChipProps>(({
74
73
  // Helper to render delete icon
75
74
  const renderDeleteIcon = () => {
76
75
  if (isIconName(deleteIcon)) {
77
- const iconPath = resolveIconPath(deleteIcon);
78
76
  return (
79
77
  <IconSvg
80
- path={iconPath}
78
+ name={deleteIcon}
81
79
  {...deleteIconProps}
82
80
  aria-label={deleteIcon}
83
81
  />
@@ -13,7 +13,7 @@ const Dialog = forwardRef<HTMLDivElement, DialogProps>(({
13
13
  title,
14
14
  children,
15
15
  size = 'md',
16
- type = 'standard',
16
+ type = 'default',
17
17
  showCloseButton = true,
18
18
  closeOnBackdropClick = true,
19
19
  closeOnEscapeKey = true,
@@ -144,8 +144,8 @@ const Dialog = forwardRef<HTMLDivElement, DialogProps>(({
144
144
  : { opacity: 0, transform: 'scale(0.96) translateY(-4px)' }
145
145
  ]);
146
146
  const headerProps = getWebProps([(dialogStyles.header as any)({})]);
147
- const titleProps = getWebProps([dialogStyles.title]);
148
- const closeButtonProps = getWebProps([dialogStyles.closeButton]);
147
+ const titleProps = getWebProps([(dialogStyles.title as any)({})]);
148
+ const closeButtonProps = getWebProps([(dialogStyles.closeButton as any)({})]);
149
149
  const contentProps = getWebProps([(dialogStyles.content as any)({})]);
150
150
 
151
151
  const mergedBackdropRef = useMergeRefs(ref, backdropProps.ref);
@@ -5,7 +5,7 @@ import { InteractiveAccessibilityProps } from '../utils/accessibility';
5
5
 
6
6
  // Component-specific type aliases for future extensibility
7
7
  export type DialogSizeVariant = 'sm' | 'md' | 'lg' | 'fullscreen';
8
- export type DialogType = 'standard' | 'alert' | 'confirmation';
8
+ export type DialogType = 'default' | 'alert' | 'confirmation';
9
9
  export type DialogAnimationType = 'slide' | 'fade' | 'none';
10
10
 
11
11
  export interface DialogProps extends BaseProps, InteractiveAccessibilityProps {
@@ -6,13 +6,9 @@ import { getWebProps } from 'react-native-unistyles/web';
6
6
  import { useUnistyles } from 'react-native-unistyles';
7
7
  import useMergeRefs from '../hooks/useMergeRefs';
8
8
  import { getColorFromString, Intent, Color } from '@idealyst/theme';
9
+ import { IconRegistry } from './IconRegistry';
9
10
 
10
- // Internal props that include the transformed path from Babel plugin
11
- interface InternalIconProps extends IconProps {
12
- path?: string; // Added by Babel plugin transformation
13
- }
14
-
15
- const Icon = forwardRef<HTMLSpanElement, IconProps>((props: InternalIconProps, ref) => {
11
+ const Icon = forwardRef<HTMLSpanElement, IconProps>((props, ref) => {
16
12
  const {
17
13
  name,
18
14
  size = 'md',
@@ -27,8 +23,16 @@ const Icon = forwardRef<HTMLSpanElement, IconProps>((props: InternalIconProps, r
27
23
 
28
24
  const { theme } = useUnistyles();
29
25
 
30
- // Check if we have a path prop (from Babel plugin transformation)
31
- const { path } = restProps as { path?: string };
26
+ // Look up the icon path from the registry
27
+ const path = IconRegistry.get(name);
28
+
29
+ // Warn in development if icon is not registered
30
+ if (!path && process.env.NODE_ENV !== 'production') {
31
+ console.warn(
32
+ `[Icon] Icon "${name}" is not registered. ` +
33
+ `Add it to the 'icons' array in your babel config, or ensure it's used in a way that static analysis can detect.`
34
+ );
35
+ }
32
36
 
33
37
  // Compute size from theme
34
38
  let iconSize: number;
@@ -52,7 +56,6 @@ const Icon = forwardRef<HTMLSpanElement, IconProps>((props: InternalIconProps, r
52
56
 
53
57
  const mergedRef = useMergeRefs(ref, iconProps.ref);
54
58
 
55
- // Use MDI React icon when path is provided (transformed by Babel plugin)
56
59
  return (
57
60
  <span
58
61
  {...iconProps}
@@ -71,17 +74,19 @@ const Icon = forwardRef<HTMLSpanElement, IconProps>((props: InternalIconProps, r
71
74
  color: iconColor,
72
75
  }}
73
76
  >
74
- <MdiIcon
75
- path={path}
76
- size="1em"
77
- color="currentColor"
78
- data-testid={testID}
79
- aria-label={accessibilityLabel || name}
80
- />
77
+ {path && (
78
+ <MdiIcon
79
+ path={path}
80
+ size="1em"
81
+ color="currentColor"
82
+ data-testid={testID}
83
+ aria-label={accessibilityLabel || name}
84
+ />
85
+ )}
81
86
  </span>
82
87
  );
83
88
  });
84
89
 
85
90
  Icon.displayName = 'Icon';
86
91
 
87
- export default Icon;
92
+ export default Icon;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Icon Registry stub for native platform
3
+ *
4
+ * On native, icons are handled by react-native-vector-icons which uses
5
+ * icon names directly. The registry is only used on web for SVG path lookup.
6
+ *
7
+ * This stub exists to prevent import errors when code is shared between platforms.
8
+ */
9
+
10
+ class IconRegistryStub {
11
+ register(_name: string, _path: string): void {
12
+ // No-op on native
13
+ }
14
+
15
+ registerMany(_icons: Record<string, string>): void {
16
+ // No-op on native
17
+ }
18
+
19
+ get(_name: string): string | undefined {
20
+ return undefined;
21
+ }
22
+
23
+ has(_name: string): boolean {
24
+ return false;
25
+ }
26
+
27
+ getRegisteredNames(): string[] {
28
+ return [];
29
+ }
30
+
31
+ get size(): number {
32
+ return 0;
33
+ }
34
+
35
+ get isInitialized(): boolean {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ export const IconRegistry = new IconRegistryStub();
41
+ export { IconRegistryStub as IconRegistryClass };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Icon Registry for web platform
3
+ *
4
+ * This registry stores SVG paths for icons that are populated at build time
5
+ * by the Babel plugin. At runtime, components look up icon paths by their
6
+ * canonical name (e.g., "home", "account-circle").
7
+ *
8
+ * The registry is populated by:
9
+ * 1. Static analysis - Babel plugin scans for icon names in JSX
10
+ * 2. Config icons - User specifies additional icons in babel config
11
+ *
12
+ * This approach:
13
+ * - Enables dynamic/variable icon names (if registered)
14
+ * - Tree-shakes unused icons (only registered icons are bundled)
15
+ * - Provides a single source of truth for icon resolution
16
+ */
17
+
18
+ type IconPath = string;
19
+
20
+ class IconRegistryClass {
21
+ private icons = new Map<string, IconPath>();
22
+ private initialized = false;
23
+
24
+ /**
25
+ * Register a single icon
26
+ * @internal Called by generated registration code
27
+ */
28
+ register(name: string, path: IconPath): void {
29
+ // Normalize the name (strip mdi: prefix, lowercase)
30
+ const normalizedName = this.normalizeName(name);
31
+ this.icons.set(normalizedName, path);
32
+ }
33
+
34
+ /**
35
+ * Register multiple icons at once
36
+ * @internal Called by generated registration code
37
+ */
38
+ registerMany(icons: Record<string, IconPath>): void {
39
+ Object.entries(icons).forEach(([name, path]) => {
40
+ this.register(name, path);
41
+ });
42
+ this.initialized = true;
43
+ }
44
+
45
+ /**
46
+ * Get an icon path by name
47
+ * Returns undefined if the icon is not registered
48
+ */
49
+ get(name: string): IconPath | undefined {
50
+ const normalizedName = this.normalizeName(name);
51
+ return this.icons.get(normalizedName);
52
+ }
53
+
54
+ /**
55
+ * Check if an icon is registered
56
+ */
57
+ has(name: string): boolean {
58
+ const normalizedName = this.normalizeName(name);
59
+ return this.icons.has(normalizedName);
60
+ }
61
+
62
+ /**
63
+ * Get all registered icon names
64
+ */
65
+ getRegisteredNames(): string[] {
66
+ return Array.from(this.icons.keys());
67
+ }
68
+
69
+ /**
70
+ * Get the count of registered icons
71
+ */
72
+ get size(): number {
73
+ return this.icons.size;
74
+ }
75
+
76
+ /**
77
+ * Check if the registry has been initialized
78
+ */
79
+ get isInitialized(): boolean {
80
+ return this.initialized;
81
+ }
82
+
83
+ /**
84
+ * Normalize icon name for consistent lookup
85
+ * - Strips "mdi:" prefix
86
+ * - Converts to lowercase for case-insensitive matching
87
+ */
88
+ private normalizeName(name: string): string {
89
+ if (!name || typeof name !== 'string') {
90
+ return '';
91
+ }
92
+
93
+ // Strip mdi: prefix if present
94
+ let normalized = name.startsWith('mdi:') ? name.slice(4) : name;
95
+
96
+ // Lowercase for consistent lookup
97
+ normalized = normalized.toLowerCase();
98
+
99
+ return normalized;
100
+ }
101
+ }
102
+
103
+ // Singleton instance
104
+ export const IconRegistry = new IconRegistryClass();
105
+
106
+ // Also export the class for testing purposes
107
+ export { IconRegistryClass };