@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
@@ -1,38 +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,
22
26
  size = '1em',
23
27
  color = 'currentColor',
24
28
  style,
29
+ className,
25
30
  'aria-label': ariaLabel,
26
31
  'data-testid': testID,
27
32
  ...rest
28
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
+
29
49
  return (
30
50
  <MdiIcon
31
51
  style={style}
52
+ className={className}
32
53
  path={path}
33
54
  size={size}
34
55
  color={color}
35
- aria-label={ariaLabel}
56
+ aria-label={ariaLabel || name}
36
57
  data-testid={testID}
37
58
  {...rest}
38
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';
@@ -28,95 +28,68 @@ export type InputDynamicProps = {
28
28
  * Input styles with type/state handling.
29
29
  */
30
30
  export const inputStyles = defineStyle('Input', (theme: Theme) => ({
31
- container: ({ type = 'outlined', focused = false, hasError = false, disabled = false }: InputDynamicProps) => {
32
- const focusColor = theme.intents.primary.primary;
33
- const errorColor = theme.intents.error.primary;
34
-
35
- // Base styles by type
36
- let backgroundColor = 'transparent';
37
- let borderWidth = 1;
38
- let borderColor = theme.colors.border.primary;
39
-
40
- if (type === 'filled') {
41
- backgroundColor = theme.colors.surface.secondary;
42
- borderWidth = 0;
43
- } else if (type === 'bare') {
44
- backgroundColor = 'transparent';
45
- borderWidth = 0;
46
- }
47
-
48
- // Error state takes precedence
49
- if (hasError) {
50
- borderColor = errorColor;
51
- borderWidth = 1;
52
- }
53
-
54
- // Focus state (error still takes precedence for color)
55
- if (focused && !hasError) {
56
- borderColor = focusColor;
57
- borderWidth = 1;
58
- }
59
-
60
- // Disabled state
61
- if (disabled) {
62
- backgroundColor = theme.colors.surface.secondary;
63
- }
64
-
65
- // Web-specific border and shadow
66
- let webBorder = `1px solid ${borderColor}`;
67
- let webBoxShadow = 'none';
68
-
69
- if (type === 'filled' || type === 'bare') {
70
- webBorder = 'none';
71
- }
72
-
73
- if (hasError) {
74
- webBorder = `1px solid ${errorColor}`;
75
- if (focused) {
76
- webBoxShadow = `0 0 0 2px ${errorColor}20`;
77
- }
78
- } else if (focused) {
79
- webBorder = `1px solid ${focusColor}`;
80
- webBoxShadow = `0 0 0 2px ${focusColor}20`;
81
- }
82
-
83
- return {
84
- display: 'flex' as const,
85
- flexDirection: 'row' as const,
86
- alignItems: 'center' as const,
87
- width: '100%',
88
- minWidth: 0,
89
- borderRadius: 8,
90
- backgroundColor,
91
- borderWidth,
92
- borderColor,
93
- borderStyle: 'solid' as const,
94
- opacity: disabled ? 0.6 : 1,
95
- variants: {
96
- // $iterator expands for each input size
97
- size: {
98
- height: theme.sizes.$input.height,
99
- paddingHorizontal: theme.sizes.$input.paddingHorizontal,
31
+ container: ({ type = 'outlined', focused = false, hasError = false, disabled = false }: InputDynamicProps) => ({
32
+ display: 'flex' as const,
33
+ flexDirection: 'row' as const,
34
+ alignItems: 'center' as const,
35
+ width: '100%',
36
+ minWidth: 0,
37
+ borderRadius: theme.radii.md,
38
+ borderWidth: 1,
39
+ borderStyle: 'solid' as const,
40
+ borderColor: theme.colors.border.primary,
41
+ variants: {
42
+ type: {
43
+ outlined: {
44
+ backgroundColor: theme.colors.surface.primary,
45
+ borderColor: theme.colors.border.primary,
46
+ },
47
+ filled: {
48
+ backgroundColor: theme.colors.surface.secondary,
49
+ borderColor: 'transparent',
100
50
  },
101
- margin: {
102
- margin: theme.sizes.$view.padding,
51
+ bare: {
52
+ backgroundColor: 'transparent',
53
+ borderWidth: 0,
54
+ borderColor: 'transparent',
103
55
  },
104
- marginVertical: {
105
- marginVertical: theme.sizes.$view.padding,
56
+ },
57
+ focused: {
58
+ true: {
59
+ borderColor: theme.intents.primary.primary,
106
60
  },
107
- marginHorizontal: {
108
- marginHorizontal: theme.sizes.$view.padding,
61
+ false: {
62
+
109
63
  },
110
64
  },
111
- _web: {
112
- boxSizing: 'border-box',
113
- transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
114
- border: webBorder,
115
- boxShadow: webBoxShadow,
116
- cursor: disabled ? 'not-allowed' : 'text',
65
+ disabled: {
66
+ true: { opacity: 0.8, _web: { cursor: 'not-allowed' } },
67
+ false: { opacity: 1 }
68
+ },
69
+ hasError: {
70
+ true: { borderColor: theme.intents.error.primary },
71
+ false: { borderColor: theme.colors.border.primary },
72
+ },
73
+ // $iterator expands for each input size
74
+ size: {
75
+ height: theme.sizes.$input.height,
76
+ paddingHorizontal: theme.sizes.$input.paddingHorizontal,
117
77
  },
118
- } as const;
119
- },
78
+ margin: {
79
+ margin: theme.sizes.$view.padding,
80
+ },
81
+ marginVertical: {
82
+ marginVertical: theme.sizes.$view.padding,
83
+ },
84
+ marginHorizontal: {
85
+ marginHorizontal: theme.sizes.$view.padding,
86
+ },
87
+ },
88
+ _web: {
89
+ boxSizing: 'border-box',
90
+ transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
91
+ }
92
+ }),
120
93
 
121
94
  input: (_props: InputDynamicProps) => ({
122
95
  flex: 1,
@@ -1,7 +1,7 @@
1
1
  import React, { isValidElement, useState, useMemo, useRef } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
4
- import { isIconName, resolveIconPath } from '../Icon/icon-resolver';
4
+ import { isIconName } from '../Icon/icon-resolver';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
6
  import { inputStyles } from './Input.styles';
7
7
  import { InputProps } from './types';
@@ -184,10 +184,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
184
184
  if (!leftIcon) return null;
185
185
 
186
186
  if (isIconName(leftIcon)) {
187
- const iconPath = resolveIconPath(leftIcon);
188
187
  return (
189
188
  <IconSvg
190
- path={iconPath}
189
+ name={leftIcon}
191
190
  {...leftIconProps}
192
191
  aria-label={leftIcon}
193
192
  />
@@ -204,10 +203,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
204
203
  if (!rightIcon) return null;
205
204
 
206
205
  if (isIconName(rightIcon)) {
207
- const iconPath = resolveIconPath(rightIcon);
208
206
  return (
209
207
  <IconSvg
210
- path={iconPath}
208
+ name={rightIcon}
211
209
  {...rightIconProps}
212
210
  aria-label={rightIcon}
213
211
  />
@@ -222,10 +220,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
222
220
  // Helper to render password toggle icon
223
221
  const renderPasswordToggleIcon = () => {
224
222
  const iconName = isPasswordVisible ? 'eye-off' : 'eye';
225
- const iconPath = resolveIconPath(iconName);
226
223
  return (
227
224
  <IconSvg
228
- path={iconPath}
225
+ name={iconName}
229
226
  {...passwordToggleIconProps}
230
227
  aria-label={iconName}
231
228
  />
@@ -290,4 +287,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
290
287
 
291
288
  Input.displayName = 'Input';
292
289
 
293
- export default Input;
290
+ export default Input;
@@ -66,18 +66,17 @@ const ListItem = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pres
66
66
  const trailingStyle = (listStyles.trailing as any)({});
67
67
  const trailingIconStyle = (listStyles.trailingIcon as any)({});
68
68
  const labelContainerStyle = (listStyles.labelContainer as any)({});
69
- const itemContentStyle = (listStyles.itemContent as any)({});
70
69
 
71
70
  // Resolve icon color - check intents first, then color palette
72
- const resolvedIconColor = (() => {
73
- if (!iconColor) return undefined;
71
+ const resolvedIconColor = useMemo(() => {
72
+ if (!iconColor) return trailingIconStyle.color || leadingStyle.color;
74
73
  // Check if it's an intent name
75
74
  if (iconColor in theme.intents) {
76
75
  return theme.intents[iconColor as Intent]?.primary;
77
76
  }
78
77
  // Otherwise try color palette
79
78
  return getColorFromString(theme, iconColor as Color);
80
- })();
79
+ }, [iconColor, theme, trailingIconStyle.color, leadingStyle.color]);
81
80
 
82
81
  // Helper to render leading/trailing icons
83
82
  const renderElement = (element: typeof leading | typeof trailing, styleKey: 'leading' | 'trailingIcon') => {
@@ -85,12 +84,12 @@ const ListItem = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pres
85
84
 
86
85
  // If it's a string, treat it as an icon name
87
86
  if (typeof element === 'string') {
88
- const iconStyle = styleKey === 'leading' ? leadingStyle : trailingIconStyle;
87
+ const iconSize = styleKey === 'leading' ? leadingStyle.width : trailingIconStyle.width;
89
88
  return (
90
89
  <MaterialCommunityIcons
91
90
  name={element}
92
- size={iconStyle.width}
93
- color={resolvedIconColor ?? iconStyle.color}
91
+ size={iconSize}
92
+ color={resolvedIconColor}
94
93
  />
95
94
  );
96
95
  } else if (isValidElement(element)) {
@@ -5,7 +5,7 @@ import { getColorFromString, Intent, Theme, Color } from '@idealyst/theme';
5
5
  import { listStyles } from './List.styles';
6
6
  import type { ListItemProps } from './types';
7
7
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
8
- import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
8
+ import { isIconName } from '../Icon/icon-resolver';
9
9
  import { useListContext } from './ListContext';
10
10
 
11
11
  const ListItem: React.FC<ListItemProps> = ({
@@ -77,10 +77,10 @@ const ListItem: React.FC<ListItemProps> = ({
77
77
  if (!element) return null;
78
78
 
79
79
  if (isIconName(element)) {
80
- const iconPath = resolveIconPath(element);
80
+ // Use IconSvg with name - registry lookup happens inside
81
81
  return (
82
82
  <IconSvg
83
- path={iconPath}
83
+ name={element}
84
84
  color={resolvedIconColor || 'currentColor'}
85
85
  aria-label={element}
86
86
  />
@@ -3,7 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { menuItemStyles } from './MenuItem.styles';
4
4
  import type { MenuItem as MenuItemType, MenuSizeVariant } 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
  interface MenuItemProps {
@@ -33,12 +33,10 @@ const MenuItem = forwardRef<HTMLButtonElement, MenuItemProps>(({ item, onPress,
33
33
  if (!item.icon) return null;
34
34
 
35
35
  if (isIconName(item.icon)) {
36
- // Resolve icon name to path and render with IconSvg
37
- const iconPath = resolveIconPath(item.icon);
38
- // IconSvg uses size="1em" by default, sized by container's fontSize from styles
36
+ // Use IconSvg with name - registry lookup happens inside
39
37
  return (
40
38
  <IconSvg
41
- path={iconPath}
39
+ name={item.icon}
42
40
  color="currentColor"
43
41
  aria-label={item.icon}
44
42
  />
@@ -6,7 +6,7 @@ import { screenStyles } from './Screen.styles';
6
6
 
7
7
  const Screen = forwardRef<RNView | RNScrollView, ScreenProps>(({
8
8
  children,
9
- background = 'primary',
9
+ background = 'screen',
10
10
  safeArea = true,
11
11
  scrollable = true,
12
12
  contentInset,
@@ -12,7 +12,7 @@ void StyleSheet;
12
12
  // Wrap theme for $iterator support
13
13
  type Theme = ThemeStyleWrapper<BaseTheme>;
14
14
 
15
- type ScreenBackground = 'primary' | 'secondary' | 'tertiary' | 'inverse' | 'inverse-secondary' | 'inverse-tertiary' | 'transparent';
15
+ type ScreenBackground = 'screen' | 'primary' | 'secondary' | 'tertiary' | 'inverse' | 'inverse-secondary' | 'inverse-tertiary' | 'transparent';
16
16
 
17
17
  export type ScreenDynamicProps = {
18
18
  background?: ScreenBackground;
@@ -34,10 +34,9 @@ export type ScreenDynamicProps = {
34
34
  export const screenStyles = defineStyle('Screen', (theme: Theme) => ({
35
35
  screen: (_props: ScreenDynamicProps) => ({
36
36
  flex: 1,
37
- // Theme marker for Unistyles reactivity
38
- backgroundColor: theme.colors.surface.primary,
39
37
  variants: {
40
38
  background: {
39
+ screen: { backgroundColor: theme.colors.surface.screen },
41
40
  primary: { backgroundColor: theme.colors.surface.primary },
42
41
  secondary: { backgroundColor: theme.colors.surface.secondary },
43
42
  tertiary: { backgroundColor: theme.colors.surface.tertiary },
@@ -74,7 +73,6 @@ export const screenStyles = defineStyle('Screen', (theme: Theme) => ({
74
73
  },
75
74
  },
76
75
  _web: {
77
- overflow: 'auto',
78
76
  display: 'flex',
79
77
  flexDirection: 'column',
80
78
  minHeight: '100%',
@@ -83,10 +81,9 @@ export const screenStyles = defineStyle('Screen', (theme: Theme) => ({
83
81
  }),
84
82
 
85
83
  screenContent: (_props: ScreenDynamicProps) => ({
86
- // Theme marker for Unistyles reactivity
87
- backgroundColor: theme.colors.surface.primary,
88
84
  variants: {
89
85
  background: {
86
+ screen: { backgroundColor: theme.colors.surface.screen },
90
87
  primary: { backgroundColor: theme.colors.surface.primary },
91
88
  secondary: { backgroundColor: theme.colors.surface.secondary },
92
89
  tertiary: { backgroundColor: theme.colors.surface.tertiary },
@@ -6,7 +6,7 @@ import useMergeRefs from '../hooks/useMergeRefs';
6
6
 
7
7
  const Screen = forwardRef<HTMLDivElement, ScreenProps>(({
8
8
  children,
9
- background = 'primary',
9
+ background = 'screen',
10
10
  safeArea = false,
11
11
  // Spacing variants from ContainerStyleProps
12
12
  gap,
@@ -52,56 +52,39 @@ export const selectStyles = defineStyle('Select', (theme: Theme) => ({
52
52
  marginBottom: 4,
53
53
  }),
54
54
 
55
- trigger: ({ type = 'outlined', intent = 'neutral', disabled = false, error = false, focused = false }: SelectDynamicProps) => {
56
- const intentValue = theme.intents[intent];
57
- const primaryIntent = theme.intents.primary;
58
- const errorColor = theme.intents.error.primary;
59
-
60
- // Background based on type
61
- const backgroundColor = type === 'filled'
62
- ? theme.colors.surface.secondary
63
- : theme.colors.surface.primary;
64
-
65
- // Border color based on state
66
- let borderColor = type === 'filled' ? 'transparent' : theme.colors.border.primary;
67
- if (error) {
68
- borderColor = errorColor;
69
- } else if (focused) {
70
- borderColor = primaryIntent.primary;
71
- } else if (intent !== 'neutral') {
72
- borderColor = intentValue.primary;
73
- }
74
-
75
- return {
76
- position: 'relative' as const,
77
- flexDirection: 'row' as const,
78
- alignItems: 'center' as const,
79
- justifyContent: 'space-between' as const,
80
- borderRadius: 8,
81
- borderWidth: 1,
82
- borderStyle: 'solid' as const,
83
- backgroundColor,
84
- borderColor,
85
- opacity: disabled ? 0.6 : 1,
86
- variants: {
87
- size: {
88
- paddingHorizontal: theme.sizes.$select.paddingHorizontal,
89
- minHeight: theme.sizes.$select.minHeight,
55
+ trigger: ({ type = 'outlined', intent = 'neutral', disabled = false, error = false, focused = false }: SelectDynamicProps) => ({
56
+ position: 'relative' as const,
57
+ flexDirection: 'row' as const,
58
+ alignItems: 'center' as const,
59
+ justifyContent: 'space-between' as const,
60
+ borderWidth: 1,
61
+ borderStyle: 'solid' as const,
62
+ opacity: disabled ? 0.6 : 1,
63
+ variants: {
64
+ type: {
65
+ filled: {
66
+ backgroundColor: theme.colors.surface.secondary,
67
+ borderColor: 'transparent',
90
68
  },
69
+ outlined: {
70
+ backgroundColor: theme.colors.surface.primary,
71
+ borderWidth: 1,
72
+ borderColor: theme.colors.border.primary,
73
+ }
91
74
  },
92
- _web: {
93
- display: 'flex',
94
- boxSizing: 'border-box',
95
- cursor: disabled ? 'not-allowed' : 'pointer',
96
- border: `1px solid ${borderColor}`,
97
- boxShadow: focused ? `0 0 0 2px ${primaryIntent.primary}20` : 'none',
98
- transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
99
- _hover: disabled ? {} : { opacity: 0.9 },
100
- _active: disabled ? {} : { opacity: 0.8 },
101
- _focus: { outline: 'none' },
102
- },
103
- } as const;
104
- },
75
+ size: theme.sizes.$select,
76
+ },
77
+ _web: {
78
+ display: 'flex',
79
+ boxSizing: 'border-box',
80
+ cursor: disabled ? 'not-allowed' : 'pointer',
81
+ border: `1px solid`,
82
+ transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
83
+ _hover: disabled ? {} : { opacity: 0.9 },
84
+ _active: disabled ? {} : { opacity: 0.8 },
85
+ _focus: { outline: 'none' },
86
+ },
87
+ }),
105
88
 
106
89
  triggerContent: (_props: SelectDynamicProps) => ({
107
90
  flex: 1,