@idealyst/components 1.1.7 → 1.1.9

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 (129) hide show
  1. package/package.json +3 -3
  2. package/plugin/web.js +280 -532
  3. package/src/Accordion/Accordion.native.tsx +8 -6
  4. package/src/Accordion/Accordion.styles.old.tsx +298 -0
  5. package/src/Accordion/Accordion.styles.tsx +102 -236
  6. package/src/Accordion/Accordion.web.tsx +1 -3
  7. package/src/ActivityIndicator/ActivityIndicator.styles.old.tsx +94 -0
  8. package/src/ActivityIndicator/ActivityIndicator.styles.tsx +44 -74
  9. package/src/Alert/Alert.native.tsx +16 -6
  10. package/src/Alert/Alert.styles.old.tsx +209 -0
  11. package/src/Alert/Alert.styles.tsx +67 -149
  12. package/src/Alert/Alert.web.tsx +3 -4
  13. package/src/Avatar/Avatar.styles.old.tsx +99 -0
  14. package/src/Avatar/Avatar.styles.tsx +35 -80
  15. package/src/Badge/Badge.styles.old.tsx +157 -0
  16. package/src/Badge/Badge.styles.tsx +61 -121
  17. package/src/Badge/Badge.web.tsx +8 -15
  18. package/src/Breadcrumb/Breadcrumb.styles.old.tsx +231 -0
  19. package/src/Breadcrumb/Breadcrumb.styles.tsx +83 -200
  20. package/src/Breadcrumb/Breadcrumb.web.tsx +31 -30
  21. package/src/Button/Button.native.tsx +14 -21
  22. package/src/Button/Button.styles.tsx +103 -140
  23. package/src/Button/Button.web.tsx +9 -19
  24. package/src/Card/Card.native.tsx +7 -11
  25. package/src/Card/Card.styles.old.tsx +160 -0
  26. package/src/Card/Card.styles.tsx +105 -142
  27. package/src/Card/Card.web.tsx +5 -4
  28. package/src/Checkbox/Checkbox.native.tsx +9 -5
  29. package/src/Checkbox/Checkbox.styles.old.tsx +271 -0
  30. package/src/Checkbox/Checkbox.styles.tsx +104 -216
  31. package/src/Checkbox/Checkbox.web.tsx +7 -8
  32. package/src/Chip/Chip.styles.old.tsx +184 -0
  33. package/src/Chip/Chip.styles.tsx +34 -72
  34. package/src/Chip/Chip.web.tsx +3 -5
  35. package/src/Dialog/Dialog.native.tsx +7 -4
  36. package/src/Dialog/Dialog.styles.old.tsx +202 -0
  37. package/src/Dialog/Dialog.styles.tsx +69 -133
  38. package/src/Dialog/Dialog.web.tsx +3 -3
  39. package/src/Dialog/types.ts +1 -1
  40. package/src/Divider/Divider.styles.old.tsx +172 -0
  41. package/src/Divider/Divider.styles.tsx +62 -84
  42. package/src/Icon/Icon.native.tsx +8 -8
  43. package/src/Icon/Icon.styles.old.tsx +81 -0
  44. package/src/Icon/Icon.styles.tsx +52 -66
  45. package/src/Icon/Icon.web.tsx +62 -21
  46. package/src/Icon/IconRegistry.native.ts +41 -0
  47. package/src/Icon/IconRegistry.ts +107 -0
  48. package/src/Icon/IconSvg/IconSvg.web.tsx +28 -5
  49. package/src/Icon/icon-resolver.ts +12 -43
  50. package/src/Icon/index.native.ts +2 -1
  51. package/src/Icon/index.ts +1 -0
  52. package/src/Icon/index.web.ts +1 -0
  53. package/src/Image/Image.styles.old.tsx +69 -0
  54. package/src/Image/Image.styles.tsx +46 -60
  55. package/src/Input/Input.native.tsx +138 -53
  56. package/src/Input/Input.styles.old.tsx +289 -0
  57. package/src/Input/Input.styles.tsx +134 -232
  58. package/src/Input/Input.web.tsx +5 -8
  59. package/src/List/List.native.tsx +5 -2
  60. package/src/List/List.styles.old.tsx +242 -0
  61. package/src/List/List.styles.tsx +179 -215
  62. package/src/List/ListItem.native.tsx +16 -11
  63. package/src/List/ListItem.web.tsx +26 -16
  64. package/src/Menu/Menu.styles.old.tsx +197 -0
  65. package/src/Menu/Menu.styles.tsx +68 -150
  66. package/src/Menu/MenuItem.native.tsx +5 -3
  67. package/src/Menu/MenuItem.styles.old.tsx +114 -0
  68. package/src/Menu/MenuItem.styles.tsx +57 -89
  69. package/src/Menu/MenuItem.web.tsx +10 -7
  70. package/src/Popover/Popover.native.tsx +10 -4
  71. package/src/Popover/Popover.styles.old.tsx +135 -0
  72. package/src/Popover/Popover.styles.tsx +51 -112
  73. package/src/Pressable/Pressable.styles.old.tsx +27 -0
  74. package/src/Pressable/Pressable.styles.tsx +35 -27
  75. package/src/Progress/Progress.styles.old.tsx +200 -0
  76. package/src/Progress/Progress.styles.tsx +75 -164
  77. package/src/RadioButton/RadioButton.native.tsx +4 -3
  78. package/src/RadioButton/RadioButton.styles.old.tsx +175 -0
  79. package/src/RadioButton/RadioButton.styles.tsx +83 -154
  80. package/src/RadioButton/RadioButton.web.tsx +2 -2
  81. package/src/SVGImage/SVGImage.styles.old.tsx +86 -0
  82. package/src/SVGImage/SVGImage.styles.tsx +35 -78
  83. package/src/Screen/Screen.native.tsx +19 -26
  84. package/src/Screen/Screen.styles.old.tsx +87 -0
  85. package/src/Screen/Screen.styles.tsx +103 -68
  86. package/src/Screen/Screen.web.tsx +2 -2
  87. package/src/Select/Select.native.tsx +42 -33
  88. package/src/Select/Select.styles.old.tsx +353 -0
  89. package/src/Select/Select.styles.tsx +214 -300
  90. package/src/Select/Select.web.tsx +45 -33
  91. package/src/Skeleton/Skeleton.styles.old.tsx +67 -0
  92. package/src/Skeleton/Skeleton.styles.tsx +29 -53
  93. package/src/Slider/Slider.styles.old.tsx +259 -0
  94. package/src/Slider/Slider.styles.tsx +153 -234
  95. package/src/Slider/Slider.web.tsx +2 -4
  96. package/src/Switch/Switch.native.tsx +9 -7
  97. package/src/Switch/Switch.styles.old.tsx +203 -0
  98. package/src/Switch/Switch.styles.tsx +101 -174
  99. package/src/Switch/Switch.web.tsx +7 -8
  100. package/src/TabBar/TabBar.native.tsx +3 -2
  101. package/src/TabBar/TabBar.styles.old.tsx +343 -0
  102. package/src/TabBar/TabBar.styles.tsx +145 -279
  103. package/src/Table/Table.native.tsx +180 -68
  104. package/src/Table/Table.styles.old.tsx +311 -0
  105. package/src/Table/Table.styles.tsx +140 -281
  106. package/src/Table/Table.web.tsx +169 -70
  107. package/src/Text/Text.native.tsx +1 -3
  108. package/src/Text/Text.style.demo.tsx +16 -0
  109. package/src/Text/Text.styles.old.tsx +219 -0
  110. package/src/Text/Text.styles.tsx +94 -84
  111. package/src/Text/Text.web.tsx +3 -2
  112. package/src/Text/index.ts +1 -0
  113. package/src/TextArea/TextArea.native.tsx +21 -8
  114. package/src/TextArea/TextArea.styles.old.tsx +213 -0
  115. package/src/TextArea/TextArea.styles.tsx +87 -187
  116. package/src/TextArea/TextArea.web.tsx +17 -6
  117. package/src/Tooltip/Tooltip.styles.old.tsx +82 -0
  118. package/src/Tooltip/Tooltip.styles.tsx +32 -56
  119. package/src/Video/Video.styles.old.tsx +51 -0
  120. package/src/Video/Video.styles.tsx +32 -44
  121. package/src/View/View.native.tsx +41 -13
  122. package/src/View/View.styles.old.tsx +125 -0
  123. package/src/View/View.styles.tsx +76 -106
  124. package/src/View/View.web.tsx +5 -21
  125. package/src/View/types.ts +31 -3
  126. package/src/examples/ButtonExamples.tsx +20 -0
  127. package/src/examples/CardExamples.tsx +0 -6
  128. package/src/extensions/extendComponent.ts +61 -0
  129. package/src/index.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  import { forwardRef, useMemo } from 'react';
2
2
  import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
3
3
  import { IconProps } from './types';
4
- import { iconStyles, buildIconSize } from './Icon.styles';
4
+ import { iconStyles } from './Icon.styles';
5
5
  import { useUnistyles } from 'react-native-unistyles';
6
6
 
7
7
  const Icon = forwardRef<any, IconProps>(({
@@ -14,18 +14,17 @@ const Icon = forwardRef<any, IconProps>(({
14
14
  accessibilityLabel,
15
15
  id,
16
16
  }: IconProps, ref) => {
17
+ const { theme } = useUnistyles();
17
18
 
18
-
19
- // Call dynamic style with variants
19
+ // Call dynamic style with variants - includes theme-reactive color
20
20
  const iconStyle = (iconStyles.icon as any)({ color, intent, size });
21
21
 
22
- const { theme } = useUnistyles();
23
-
24
22
  const iconSize = useMemo(() => {
25
- return buildIconSize(theme, size).width;
26
- }, [theme, size]);
23
+ return iconStyle.width;
24
+ }, [iconStyle]);
27
25
 
28
- // Get fontSize from styles for numeric size prop
26
+ // Extract color from iconStyle for explicit color prop (RN vector icons need this)
27
+ const iconColor = iconStyle.color;
29
28
 
30
29
  return (
31
30
  <MaterialCommunityIcons
@@ -33,6 +32,7 @@ const Icon = forwardRef<any, IconProps>(({
33
32
  nativeID={id}
34
33
  size={iconSize}
35
34
  name={name}
35
+ color={iconColor}
36
36
  style={[iconStyle, style]}
37
37
  testID={testID}
38
38
  accessibilityLabel={accessibilityLabel}
@@ -0,0 +1,81 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+ import { Theme, StylesheetStyles, Intent, Color, getColorFromString } from '@idealyst/theme';
3
+ import { buildSizeVariants } from '../utils/buildSizeVariants';
4
+ import { IconSizeVariant } from './types';
5
+ import { applyExtensions } from '../extensions/applyExtension';
6
+
7
+ type IconVariants = {
8
+ size: IconSizeVariant;
9
+ intent?: Intent;
10
+ color?: Color;
11
+ }
12
+
13
+ export type ExpandedIconStyles = StylesheetStyles<keyof IconVariants>;
14
+
15
+ export type IconStylesheet = {
16
+ icon: ExpandedIconStyles;
17
+ }
18
+
19
+ /**
20
+ * Create color variants for icon
21
+ */
22
+ function getIconColor(theme: Theme, color?: Color, intent?: Intent): string {
23
+ if (intent) {
24
+ return theme.intents[intent]?.primary
25
+ } else if (color) {
26
+ return getColorFromString(theme, color);
27
+ }
28
+ return theme.colors.text.primary;
29
+ }
30
+
31
+ export function buildIconSize(theme: Theme, size?: IconSizeVariant) {
32
+ // Handle direct numeric sizes
33
+ if (typeof size === 'number') {
34
+ return {
35
+ width: size,
36
+ height: size,
37
+ };
38
+ }
39
+
40
+ // Default to 'md' if size is undefined
41
+ const sizeKey = size || 'md';
42
+ const iconSize = theme.sizes.icon[sizeKey];
43
+
44
+ if (typeof iconSize === 'number') {
45
+ return {
46
+ width: iconSize,
47
+ height: iconSize,
48
+ };
49
+ }
50
+
51
+ return buildSizeVariants(theme, 'icon', (size) => ({
52
+ width: size.width,
53
+ height: size.height,
54
+ }))[sizeKey];
55
+ }
56
+
57
+ function createIconStyles(theme: Theme) {
58
+ return ({ color, intent, size }: Partial<IconVariants>) => {
59
+ const iconSize = buildIconSize(theme, size);
60
+ return {
61
+ width: iconSize.width,
62
+ height: iconSize.height,
63
+ color: getIconColor(theme, color, intent),
64
+ _web: {
65
+ display: 'inline-block',
66
+ verticalAlign: 'middle',
67
+ flexShrink: 0,
68
+ lineHeight: 0,
69
+ },
70
+ } as const;
71
+ }
72
+ }
73
+
74
+ // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel
75
+ // transform on native cannot resolve function calls to extract variant structures.
76
+ export const iconStyles = StyleSheet.create((theme: Theme) => {
77
+ // Apply extensions to main visual elements
78
+ return applyExtensions('Icon', theme, {
79
+ icon: createIconStyles(theme),
80
+ });
81
+ });
@@ -1,85 +1,71 @@
1
+ /**
2
+ * Icon styles using defineStyle with dynamic functions.
3
+ */
1
4
  import { StyleSheet } from 'react-native-unistyles';
2
- import { Theme, StylesheetStyles, Intent, Color, getColorFromString } from '@idealyst/theme';
3
- import { buildSizeVariants } from '../utils/buildSizeVariants';
5
+ import { defineStyle, ThemeStyleWrapper, getColorFromString } from '@idealyst/theme';
6
+ import type { Theme as BaseTheme, Intent, Color } from '@idealyst/theme';
4
7
  import { IconSizeVariant } from './types';
5
- import { applyExtensions } from '../extensions/applyExtension';
6
8
 
7
- type IconVariants = {
9
+ // Required: Unistyles must see StyleSheet usage in original source to process this file
10
+ void StyleSheet;
11
+
12
+ // Wrap theme for $iterator support
13
+ type Theme = ThemeStyleWrapper<BaseTheme>;
14
+
15
+ export type IconVariants = {
8
16
  size: IconSizeVariant;
9
17
  intent?: Intent;
10
18
  color?: Color;
11
- }
12
-
13
- export type ExpandedIconStyles = StylesheetStyles<keyof IconVariants>;
19
+ };
14
20
 
15
- export type IconStylesheet = {
16
- icon: ExpandedIconStyles;
17
- }
21
+ export type IconDynamicProps = Partial<IconVariants>;
18
22
 
19
23
  /**
20
- * Create color variants for icon
24
+ * Icon styles with dynamic color/size handling.
21
25
  */
22
- function getIconColor(theme: Theme, color?: Color, intent?: Intent): string {
23
- if (intent) {
24
- return theme.intents[intent]?.primary
25
- } else if (color) {
26
- return getColorFromString(theme, color);
27
- }
28
- return theme.colors.text.primary;
29
- }
26
+ export const iconStyles = defineStyle('Icon', (theme: Theme) => ({
27
+ icon: ({ color, intent, size = 'md' }: IconDynamicProps) => {
28
+ // Handle size - can be a named size or number
29
+ let iconWidth: number;
30
+ let iconHeight: number;
30
31
 
31
- export function buildIconSize(theme: Theme, size?: IconSizeVariant) {
32
- // Handle direct numeric sizes
33
- if (typeof size === 'number') {
34
- return {
35
- width: size,
36
- height: size,
37
- };
38
- }
32
+ if (typeof size === 'number') {
33
+ iconWidth = size;
34
+ iconHeight = size;
35
+ } else {
36
+ const sizeKey = size || 'md';
37
+ const iconSize = theme.sizes.icon[sizeKey];
38
+ if (typeof iconSize === 'number') {
39
+ iconWidth = iconSize;
40
+ iconHeight = iconSize;
41
+ } else {
42
+ iconWidth = (iconSize?.width as number) ?? 24;
43
+ iconHeight = (iconSize?.height as number) ?? 24;
44
+ }
45
+ }
39
46
 
40
- // Default to 'md' if size is undefined
41
- const sizeKey = size || 'md';
42
- const iconSize = theme.sizes.icon[sizeKey];
47
+ // Get color - intent takes priority, then color prop, then default
48
+ const iconColor = intent
49
+ ? theme.intents[intent]?.primary
50
+ : color
51
+ ? getColorFromString(theme as unknown as BaseTheme, color)
52
+ : theme.colors.text.primary;
43
53
 
44
- if (typeof iconSize === 'number') {
45
54
  return {
46
- width: iconSize,
47
- height: iconSize,
48
- };
49
- }
50
-
51
- return buildSizeVariants(theme, 'icon', (size) => ({
52
- width: size.width,
53
- height: size.height,
54
- }))[sizeKey];
55
- }
56
-
57
- function createIconStyles(theme: Theme) {
58
- return ({ color, intent, size }: Partial<IconVariants>) => {
59
- const iconSize = buildIconSize(theme, size);
60
- return {
61
- width: iconSize.width,
62
- height: iconSize.height,
63
- color: getIconColor(theme, color, intent),
55
+ width: iconWidth,
56
+ height: iconHeight,
57
+ color: iconColor,
64
58
  _web: {
65
- display: 'inline-block',
59
+ fontSize: iconWidth,
60
+ width: '1em',
61
+ height: '1em',
62
+ display: 'inline-flex',
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
66
65
  verticalAlign: 'middle',
67
66
  flexShrink: 0,
68
- lineHeight: 0,
67
+ lineHeight: 1,
69
68
  },
70
69
  } as const;
71
- }
72
- }
73
-
74
- // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel
75
- // transform on native cannot resolve function calls to extract variant structures.
76
- export const iconStyles = StyleSheet.create((theme: Theme) => {
77
- // Apply extensions to main visual elements
78
- const extended = applyExtensions('Icon', theme, {
79
- icon: createIconStyles(theme),
80
- });
81
-
82
- return {
83
- ...extended,
84
- };
85
- });
70
+ },
71
+ }));
@@ -3,14 +3,12 @@ import MdiIcon from '@mdi/react';
3
3
  import { IconProps } from './types';
4
4
  import { iconStyles } from './Icon.styles';
5
5
  import { getWebProps } from 'react-native-unistyles/web';
6
+ import { useUnistyles } from 'react-native-unistyles';
6
7
  import useMergeRefs from '../hooks/useMergeRefs';
8
+ import { getColorFromString, Intent, Color } from '@idealyst/theme';
9
+ import { IconRegistry } from './IconRegistry';
7
10
 
8
- // Internal props that include the transformed path from Babel plugin
9
- interface InternalIconProps extends IconProps {
10
- path?: string; // Added by Babel plugin transformation
11
- }
12
-
13
- const Icon = forwardRef<HTMLDivElement, IconProps>((props: InternalIconProps, ref) => {
11
+ const Icon = forwardRef<HTMLSpanElement, IconProps>((props, ref) => {
14
12
  const {
15
13
  name,
16
14
  size = 'md',
@@ -23,29 +21,72 @@ const Icon = forwardRef<HTMLDivElement, IconProps>((props: InternalIconProps, re
23
21
  ...restProps
24
22
  } = props;
25
23
 
26
- // Check if we have a path prop (from Babel plugin transformation)
27
- const { path } = restProps as { path?: string };
28
- const iconProps = getWebProps(iconStyles.icon({ intent, color, size }));
24
+ const { theme } = useUnistyles();
25
+
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
+ }
36
+
37
+ // Compute size from theme
38
+ let iconSize: number;
39
+ if (typeof size === 'number') {
40
+ iconSize = size;
41
+ } else {
42
+ const themeSize = theme.sizes.icon[size as keyof typeof theme.sizes.icon];
43
+ iconSize = typeof themeSize === 'number' ? themeSize : (themeSize?.width ?? 24);
44
+ }
45
+
46
+ // Compute color from intent or color prop or default
47
+ const iconColor = intent
48
+ ? theme.intents[intent as Intent]?.primary
49
+ : color
50
+ ? getColorFromString(theme, color as Color)
51
+ : theme.colors.text.primary;
52
+
53
+ // Use getWebProps for className generation but override with computed values
54
+ const iconStyle = (iconStyles.icon as any)({ intent, color, size });
55
+ const iconProps = getWebProps([iconStyle, style]);
29
56
 
30
57
  const mergedRef = useMergeRefs(ref, iconProps.ref);
31
58
 
32
- // Use MDI React icon when path is provided (transformed by Babel plugin)
33
59
  return (
34
- <div
60
+ <span
35
61
  {...iconProps}
36
62
  ref={mergedRef}
37
- id={id}>
38
- <MdiIcon
39
- path={path}
40
- size={'100%'}
41
- color={'currentColor'}
42
- data-testid={testID}
43
- aria-label={accessibilityLabel || name}
44
- />
45
- </div>
63
+ id={id}
64
+ style={{
65
+ ...iconProps.style,
66
+ fontSize: iconSize,
67
+ width: '1em',
68
+ height: '1em',
69
+ display: 'inline-flex',
70
+ alignItems: 'center',
71
+ justifyContent: 'center',
72
+ flexShrink: 0,
73
+ lineHeight: 1,
74
+ color: iconColor,
75
+ }}
76
+ >
77
+ {path && (
78
+ <MdiIcon
79
+ path={path}
80
+ size="1em"
81
+ color="currentColor"
82
+ data-testid={testID}
83
+ aria-label={accessibilityLabel || name}
84
+ />
85
+ )}
86
+ </span>
46
87
  );
47
88
  });
48
89
 
49
90
  Icon.displayName = 'Icon';
50
91
 
51
- 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 };
@@ -1,36 +1,59 @@
1
1
  import React from 'react';
2
2
  import MdiIcon from '@mdi/react';
3
+ import { IconRegistry } from '../IconRegistry';
3
4
 
4
5
  /**
5
- * Internal component for rendering SVG icons directly from MDI paths.
6
+ * Internal component for rendering SVG icons from the icon registry.
6
7
  * This is used internally by components like Button, Badge, etc. to render icons
7
8
  * without going through the full Icon component.
8
9
  *
9
- * The path prop should be provided by the Babel plugin transformation.
10
+ * Icons are looked up from the registry by name. The registry is populated
11
+ * at build time by the Babel plugin.
10
12
  */
11
13
  interface IconSvgProps {
12
- path?: string; // MDI icon path, provided by Babel plugin
14
+ /** Icon name in canonical format (e.g., "home", "account-circle") */
15
+ name: string;
13
16
  size?: string | number;
14
17
  color?: string;
15
18
  style?: React.CSSProperties;
19
+ className?: string;
16
20
  'aria-label'?: string;
17
21
  'data-testid'?: string;
18
22
  }
19
23
 
20
24
  export const IconSvg: React.FC<IconSvgProps> = ({
21
- path,
25
+ name,
26
+ size = '1em',
22
27
  color = 'currentColor',
23
28
  style,
29
+ className,
24
30
  'aria-label': ariaLabel,
25
31
  'data-testid': testID,
26
32
  ...rest
27
33
  }) => {
34
+ // Look up path from registry
35
+ const path = IconRegistry.get(name);
36
+
37
+ // Warn in development if icon is not registered
38
+ if (!path && process.env.NODE_ENV !== 'production') {
39
+ console.warn(
40
+ `[IconSvg] Icon "${name}" is not registered. ` +
41
+ `Add it to the 'icons' array in your babel config.`
42
+ );
43
+ }
44
+
45
+ if (!path) {
46
+ return null;
47
+ }
48
+
28
49
  return (
29
50
  <MdiIcon
30
51
  style={style}
52
+ className={className}
31
53
  path={path}
54
+ size={size}
32
55
  color={color}
33
- aria-label={ariaLabel}
56
+ aria-label={ariaLabel || name}
34
57
  data-testid={testID}
35
58
  {...rest}
36
59
  />
@@ -1,62 +1,31 @@
1
1
  /**
2
2
  * Runtime utility for resolving MDI icon names to their SVG paths.
3
- * This is used when icon names are passed dynamically (e.g., in arrays)
4
- * and cannot be transformed by the Babel plugin at build time.
5
- */
6
-
7
- import * as mdiIcons from '@mdi/js';
8
-
9
- /**
10
- * Formats an icon name from kebab-case to the MDI export name format.
11
- * Examples:
12
- * "home" -> "mdiHome"
13
- * "account-circle" -> "mdiAccountCircle"
14
- * "star-outline" -> "mdiStarOutline"
3
+ *
4
+ * Icons are looked up from the IconRegistry, which is populated at build time
5
+ * by the Babel plugin. This replaces the previous approach of importing all
6
+ * 7,447 icons from @mdi/js.
15
7
  */
16
- function formatIconName(name: string): string {
17
- if (!name || typeof name !== 'string') {
18
- return 'mdiHelpCircle';
19
- }
20
-
21
- // Remove mdi: prefix if present
22
- const cleanName = name.startsWith('mdi:') ? name.substring(4) : name;
23
8
 
24
- // Check if the name contains only valid characters
25
- if (!/^[a-zA-Z0-9-_]+$/.test(cleanName)) {
26
- console.warn(
27
- `[icon-resolver] Invalid icon name "${name}" (contains special characters), using "help-circle" as fallback`
28
- );
29
- return 'mdiHelpCircle';
30
- }
31
-
32
- // Convert kebab-case to PascalCase
33
- const pascalCase = cleanName
34
- .split('-')
35
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
36
- .join('');
37
-
38
- return `mdi${pascalCase}`;
39
- }
9
+ import { IconRegistry } from './IconRegistry';
40
10
 
41
11
  /**
42
- * Resolves an icon name to its SVG path data.
43
- * Returns undefined if the icon is not found.
12
+ * Resolves an icon name to its SVG path data from the registry.
13
+ * Returns undefined if the icon is not registered.
44
14
  *
45
15
  * @param iconName - The icon name in kebab-case (e.g., "home", "account-circle")
46
16
  * @returns The SVG path string or undefined if not found
47
17
  */
48
18
  export function resolveIconPath(iconName: string): string | undefined {
49
- const mdiIconName = formatIconName(iconName);
50
- const iconPath = (mdiIcons as any)[mdiIconName];
19
+ const path = IconRegistry.get(iconName);
51
20
 
52
- if (!iconPath) {
21
+ if (!path && process.env.NODE_ENV !== 'production') {
53
22
  console.warn(
54
- `[icon-resolver] Icon "${iconName}" (${mdiIconName}) not found in @mdi/js, using help-circle as fallback`
23
+ `[icon-resolver] Icon "${iconName}" is not registered. ` +
24
+ `Add it to the 'icons' array in your babel config.`
55
25
  );
56
- return (mdiIcons as any).mdiHelpCircle;
57
26
  }
58
27
 
59
- return iconPath;
28
+ return path;
60
29
  }
61
30
 
62
31
  /**
@@ -1,3 +1,4 @@
1
1
  // React Native-specific Icon export
2
2
  export { default } from './Icon.native';
3
- export * from './types';
3
+ export * from './types';
4
+ export { IconRegistry } from './IconRegistry.native';
package/src/Icon/index.ts CHANGED
@@ -3,3 +3,4 @@ import IconComponent from './Icon.web';
3
3
  export default IconComponent;
4
4
  export { IconComponent as Icon };
5
5
  export * from './types';
6
+ export { IconRegistry } from './IconRegistry';
@@ -3,3 +3,4 @@ import IconComponent from './Icon.web';
3
3
  export default IconComponent;
4
4
  export { IconComponent as Icon };
5
5
  export * from './types';
6
+ export { IconRegistry } from './IconRegistry';