@idealyst/components 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/components",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Shared component library for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
6
6
  "readme": "README.md",
@@ -56,7 +56,7 @@
56
56
  "publish:npm": "npm publish"
57
57
  },
58
58
  "peerDependencies": {
59
- "@idealyst/theme": "^1.3.3",
59
+ "@idealyst/theme": "^1.3.5",
60
60
  "@mdi/js": ">=7.0.0",
61
61
  "@mdi/react": ">=1.0.0",
62
62
  "@react-native-vector-icons/common": ">=12.0.0",
@@ -111,8 +111,8 @@
111
111
  },
112
112
  "devDependencies": {
113
113
  "@idealyst/blur": "^1.2.40",
114
- "@idealyst/theme": "^1.3.3",
115
- "@idealyst/tooling": "^1.3.3",
114
+ "@idealyst/theme": "^1.3.5",
115
+ "@idealyst/tooling": "^1.3.5",
116
116
  "@mdi/react": "^1.6.1",
117
117
  "@types/react": "^19.1.0",
118
118
  "react": "^19.1.0",
@@ -17,34 +17,26 @@ interface BreadcrumbItemProps {
17
17
  }
18
18
 
19
19
  const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, intent, itemStyle }) => {
20
- const isClickable = !!item.onPress && !item.disabled;
21
20
  const isDisabled = item.disabled || false;
22
21
 
23
- // Apply size variant
24
22
  breadcrumbStyles.useVariants({
25
23
  size,
26
- });
27
-
28
- // Get dynamic item text style
29
- const itemTextStyle = (breadcrumbStyles.itemText as any)({
30
24
  intent,
31
- isLast,
25
+ active: isLast,
32
26
  disabled: isDisabled,
33
- clickable: isClickable,
34
27
  });
35
28
 
36
- const iconStyle = (breadcrumbStyles.icon as any)({});
29
+ const iconSize = (breadcrumbStyles.icon as any).width || 16;
37
30
 
38
31
  const renderIcon = () => {
39
32
  if (!item.icon) return null;
40
33
 
41
34
  if (typeof item.icon === 'string') {
42
- const iconSize = iconStyle.width || 16;
43
35
  return (
44
36
  <Icon
45
37
  name={item.icon as IconName}
46
38
  size={iconSize}
47
- style={iconStyle}
39
+ style={breadcrumbStyles.icon}
48
40
  />
49
41
  );
50
42
  } else if (isValidElement(item.icon)) {
@@ -54,16 +46,14 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
54
46
  return null;
55
47
  };
56
48
 
57
- const itemContainerStyle = (breadcrumbStyles.item as any)({});
58
-
59
49
  const content = (
60
- <View style={[itemContainerStyle, itemStyle]}>
61
- {item.icon && <View style={iconStyle}>{renderIcon()}</View>}
62
- <Text style={itemTextStyle}>{item.label}</Text>
50
+ <View style={[breadcrumbStyles.item, itemStyle]}>
51
+ {item.icon && <View style={breadcrumbStyles.icon}>{renderIcon()}</View>}
52
+ <Text style={breadcrumbStyles.itemText}>{item.label}</Text>
63
53
  </View>
64
54
  );
65
55
 
66
- if (isClickable) {
56
+ if (!!item.onPress && !item.disabled) {
67
57
  return (
68
58
  <Pressable
69
59
  onPress={item.onPress}
@@ -91,12 +81,10 @@ const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = ({ separator, si
91
81
  breadcrumbStyles.useVariants({ size });
92
82
 
93
83
  if (typeof separator === 'string') {
94
- const sepStyle = (breadcrumbStyles.separator as any)({});
95
- return <Text style={[sepStyle, separatorStyle]}>{separator}</Text>;
84
+ return <Text style={[breadcrumbStyles.separator, separatorStyle]}>{separator}</Text>;
96
85
  }
97
86
 
98
- const sepIconStyle = (breadcrumbStyles.separatorIcon as any)({});
99
- return <View style={[sepIconStyle, separatorStyle]}>{separator}</View>;
87
+ return <View style={[breadcrumbStyles.separatorIcon, separatorStyle]}>{separator}</View>;
100
88
  };
101
89
 
102
90
  interface BreadcrumbEllipsisProps {
@@ -105,13 +93,11 @@ interface BreadcrumbEllipsisProps {
105
93
  }
106
94
 
107
95
  const BreadcrumbEllipsis: React.FC<BreadcrumbEllipsisProps> = ({ size, intent }) => {
108
- breadcrumbStyles.useVariants({ size });
109
- const ellipsisStyle = (breadcrumbStyles.ellipsis as any)({});
110
- const iconStyle = (breadcrumbStyles.ellipsisIcon as any)({ intent });
96
+ breadcrumbStyles.useVariants({ size, intent });
111
97
 
112
98
  return (
113
- <View style={ellipsisStyle}>
114
- <Icon name="dots-horizontal" style={iconStyle} />
99
+ <View style={breadcrumbStyles.ellipsis}>
100
+ <Icon name="dots-horizontal" style={breadcrumbStyles.ellipsisIcon} />
115
101
  </View>
116
102
  );
117
103
  };
@@ -132,11 +118,7 @@ const Breadcrumb = forwardRef<IdealystElement, BreadcrumbProps>(({
132
118
  }, ref) => {
133
119
  const [menuOpen, setMenuOpen] = useState(false);
134
120
 
135
- // Apply variants
136
- breadcrumbStyles.useVariants({ size });
137
- const containerStyle = (breadcrumbStyles.container as any)({});
138
- const menuButtonStyle = (breadcrumbStyles.menuButton as any)({});
139
- const menuIconStyle = (breadcrumbStyles.menuButtonIcon as any)({ intent });
121
+ breadcrumbStyles.useVariants({ size, intent });
140
122
 
141
123
  // Handle responsive collapsing
142
124
  let displayItems = items;
@@ -174,7 +156,7 @@ const Breadcrumb = forwardRef<IdealystElement, BreadcrumbProps>(({
174
156
  <View
175
157
  ref={ref as any}
176
158
  nativeID={id}
177
- style={[containerStyle, style]}
159
+ style={[breadcrumbStyles.container, style]}
178
160
  testID={testID}
179
161
  accessibilityLabel="Breadcrumb"
180
162
  >
@@ -202,11 +184,11 @@ const Breadcrumb = forwardRef<IdealystElement, BreadcrumbProps>(({
202
184
  size={size}
203
185
  >
204
186
  <Pressable
205
- style={menuButtonStyle}
187
+ style={breadcrumbStyles.menuButton}
206
188
  accessibilityRole="button"
207
189
  accessibilityLabel="Show more breadcrumb items"
208
190
  >
209
- <Icon name="dots-horizontal" style={menuIconStyle} />
191
+ <Icon name="dots-horizontal" style={breadcrumbStyles.menuButtonIcon} />
210
192
  </Pressable>
211
193
  </Menu>
212
194
  <BreadcrumbSeparator separator={separator} size={size} separatorStyle={separatorStyle} />
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Breadcrumb styles using defineStyle with $iterator expansion.
2
+ * Breadcrumb styles using defineStyle with static variants.
3
3
  */
4
4
  import { StyleSheet } from 'react-native-unistyles';
5
5
  import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
6
- import type { Theme as BaseTheme, Size } from '@idealyst/theme';
6
+ import type { Theme as BaseTheme } from '@idealyst/theme';
7
+ import { ViewStyleSize } from '../utils/viewStyleProps';
7
8
 
8
9
  // Required: Unistyles must see StyleSheet usage in original source to process this file
9
10
  void StyleSheet;
@@ -11,65 +12,61 @@ void StyleSheet;
11
12
  // Wrap theme for $iterator support
12
13
  type Theme = ThemeStyleWrapper<BaseTheme>;
13
14
 
14
- type BreadcrumbIntent = 'primary' | 'neutral';
15
-
16
- export type BreadcrumbDynamicProps = {
17
- size?: Size;
18
- intent?: BreadcrumbIntent;
15
+ export type BreadcrumbVariants = {
16
+ size?: ViewStyleSize;
17
+ intent?: 'primary' | 'neutral';
18
+ active?: boolean;
19
19
  disabled?: boolean;
20
- isLast?: boolean;
21
- clickable?: boolean;
22
20
  };
23
21
 
24
22
  /**
25
- * Breadcrumb styles with intent and state handling.
23
+ * Breadcrumb styles with static variants.
26
24
  */
27
25
  export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
28
- container: (_props: BreadcrumbDynamicProps) => ({
26
+ container: {
29
27
  display: 'flex' as const,
30
28
  flexDirection: 'row' as const,
31
29
  alignItems: 'center' as const,
32
30
  flexWrap: 'wrap' as const,
33
31
  gap: 4,
34
- }),
32
+ },
35
33
 
36
- item: (_props: BreadcrumbDynamicProps) => ({
34
+ item: {
37
35
  display: 'flex' as const,
38
36
  flexDirection: 'row' as const,
39
37
  alignItems: 'center' as const,
40
38
  gap: 4,
41
- }),
42
-
43
- itemText: ({ intent = 'primary', isLast = false, disabled = false, clickable = true }: BreadcrumbDynamicProps) => {
44
- // Get color based on state - inline for Unistyles to trace
45
- const color = disabled
46
- ? theme.colors.text.secondary
47
- : isLast
48
- ? theme.colors.text.primary
49
- : clickable
50
- ? (intent === 'primary' ? theme.intents.primary.primary : theme.colors.text.secondary)
51
- : theme.colors.text.secondary;
39
+ opacity: 0.7,
40
+ variants: {
41
+ active: {
42
+ true: { opacity: 1 },
43
+ false: { _web: { _hover: { opacity: 1 } } },
44
+ },
45
+ disabled: {
46
+ true: { opacity: 0.5 },
47
+ false: {},
48
+ },
49
+ },
50
+ _web: {
51
+ transition: 'opacity 0.2s ease',
52
+ },
53
+ },
52
54
 
53
- return {
54
- color,
55
- opacity: disabled ? 0.5 : 1,
56
- variants: {
57
- // $iterator expands for each breadcrumb size
58
- size: {
59
- fontSize: theme.sizes.$breadcrumb.fontSize,
60
- lineHeight: theme.sizes.$breadcrumb.lineHeight,
61
- },
55
+ itemText: {
56
+ color: theme.colors.text.primary,
57
+ variants: {
58
+ size: {
59
+ fontSize: theme.sizes.$breadcrumb.fontSize,
60
+ lineHeight: theme.sizes.$breadcrumb.lineHeight,
61
+ },
62
+ disabled: {
63
+ true: { color: theme.colors.text.secondary },
64
+ false: {},
62
65
  },
63
- _web: clickable && !isLast && !disabled ? {
64
- _hover: {
65
- textDecoration: 'underline',
66
- opacity: 0.8,
67
- },
68
- } : {},
69
- } as const;
66
+ },
70
67
  },
71
68
 
72
- icon: (_props: BreadcrumbDynamicProps) => ({
69
+ icon: {
73
70
  variants: {
74
71
  size: {
75
72
  width: theme.sizes.$breadcrumb.iconSize,
@@ -77,23 +74,25 @@ export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
77
74
  fontSize: theme.sizes.$breadcrumb.iconSize,
78
75
  },
79
76
  },
80
- }),
77
+ },
81
78
 
82
- separator: (_props: BreadcrumbDynamicProps) => ({
79
+ separator: {
83
80
  color: theme.colors.text.tertiary,
81
+ opacity: 0.9,
84
82
  variants: {
85
83
  size: {
86
84
  fontSize: theme.sizes.$breadcrumb.fontSize,
87
85
  lineHeight: theme.sizes.$breadcrumb.lineHeight,
88
86
  },
89
87
  },
90
- }),
88
+ },
91
89
 
92
- separatorIcon: (_props: BreadcrumbDynamicProps) => ({
90
+ separatorIcon: {
93
91
  display: 'flex' as const,
94
92
  alignItems: 'center' as const,
95
93
  justifyContent: 'center' as const,
96
94
  color: theme.colors.text.tertiary,
95
+ opacity: 0.9,
97
96
  variants: {
98
97
  size: {
99
98
  width: theme.sizes.$breadcrumb.iconSize,
@@ -101,38 +100,46 @@ export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
101
100
  fontSize: theme.sizes.$breadcrumb.iconSize,
102
101
  },
103
102
  },
104
- }),
103
+ },
105
104
 
106
- ellipsis: (_props: BreadcrumbDynamicProps) => ({
105
+ ellipsis: {
107
106
  display: 'flex' as const,
108
107
  alignItems: 'center' as const,
109
108
  justifyContent: 'center' as const,
110
- }),
109
+ },
111
110
 
112
- ellipsisIcon: ({ intent = 'primary' }: BreadcrumbDynamicProps) => ({
113
- color: intent === 'primary' ? theme.intents.primary.primary : theme.colors.text.secondary,
111
+ ellipsisIcon: {
112
+ color: theme.colors.text.secondary,
114
113
  variants: {
115
114
  size: {
116
115
  width: theme.sizes.$breadcrumb.iconSize,
117
116
  height: theme.sizes.$breadcrumb.iconSize,
118
117
  fontSize: theme.sizes.$breadcrumb.iconSize,
119
118
  },
119
+ intent: {
120
+ primary: { color: theme.intents.primary.primary },
121
+ neutral: { color: theme.colors.text.secondary },
122
+ },
120
123
  },
121
- }),
124
+ },
122
125
 
123
- menuButton: (_props: BreadcrumbDynamicProps) => ({
126
+ menuButton: {
124
127
  paddingVertical: 4,
125
128
  paddingHorizontal: 8,
126
- }),
129
+ },
127
130
 
128
- menuButtonIcon: ({ intent = 'primary' }: BreadcrumbDynamicProps) => ({
129
- color: intent === 'primary' ? theme.intents.primary.primary : theme.colors.text.secondary,
131
+ menuButtonIcon: {
132
+ color: theme.colors.text.secondary,
130
133
  variants: {
131
134
  size: {
132
135
  width: theme.sizes.$breadcrumb.iconSize,
133
136
  height: theme.sizes.$breadcrumb.iconSize,
134
137
  fontSize: theme.sizes.$breadcrumb.iconSize,
135
138
  },
139
+ intent: {
140
+ primary: { color: theme.intents.primary.primary },
141
+ neutral: { color: theme.colors.text.secondary },
142
+ },
136
143
  },
137
- }),
144
+ },
138
145
  }));
@@ -19,24 +19,16 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
19
19
  const isClickable = !!item.onPress && !item.disabled;
20
20
  const isDisabled = item.disabled || false;
21
21
 
22
- // Apply size variant
23
22
  breadcrumbStyles.useVariants({
24
23
  size,
25
- });
26
-
27
- // Get dynamic styles - call as functions for theme reactivity
28
- const itemStyle_ = (breadcrumbStyles.item as any)({});
29
- const itemTextStyle = (breadcrumbStyles.itemText as any)({
30
24
  intent,
31
- isLast,
25
+ active: isLast,
32
26
  disabled: isDisabled,
33
- clickable: isClickable,
34
27
  });
35
- const iconStyle = (breadcrumbStyles.icon as any)({});
36
28
 
37
- const itemProps = getWebProps([itemStyle_]);
38
- const itemTextProps = getWebProps([itemTextStyle, itemStyle]);
39
- const iconProps = getWebProps([iconStyle]);
29
+ const itemProps = getWebProps([breadcrumbStyles.item]);
30
+ const itemTextProps = getWebProps([breadcrumbStyles.itemText, itemStyle]);
31
+ const iconProps = getWebProps([breadcrumbStyles.icon]);
40
32
 
41
33
  const handleClick = () => {
42
34
  if (!item.disabled && item.onPress) {
@@ -61,30 +53,13 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
61
53
  return null;
62
54
  };
63
55
 
64
- const content = (
65
- <div {...itemProps}>
66
- {item.icon && (
67
- <span
68
- {...iconProps}
69
- style={{
70
- display: 'inline-flex',
71
- alignItems: 'center',
72
- justifyContent: 'center',
73
- }}
74
- >
75
- {renderIcon()}
76
- </span>
77
- )}
78
- <span {...itemTextProps}>
79
- {item.label}
80
- </span>
81
- </div>
82
- );
83
-
84
56
  if (isClickable) {
85
57
  return (
86
58
  <button
59
+ {...itemProps}
87
60
  onClick={handleClick}
61
+ disabled={isDisabled}
62
+ aria-current={isLast ? 'page' : undefined}
88
63
  style={{
89
64
  background: 'none',
90
65
  border: 'none',
@@ -95,17 +70,43 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
95
70
  font: 'inherit',
96
71
  color: 'inherit',
97
72
  }}
98
- disabled={isDisabled}
99
- aria-current={isLast ? 'page' : undefined}
100
73
  >
101
- {content}
74
+ {item.icon && (
75
+ <span
76
+ {...iconProps}
77
+ style={{
78
+ display: 'inline-flex',
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ }}
82
+ >
83
+ {renderIcon()}
84
+ </span>
85
+ )}
86
+ <span {...itemTextProps}>
87
+ {item.label}
88
+ </span>
102
89
  </button>
103
90
  );
104
91
  }
105
92
 
106
93
  return (
107
- <div aria-current={isLast ? 'page' : undefined}>
108
- {content}
94
+ <div {...itemProps} aria-current={isLast ? 'page' : undefined}>
95
+ {item.icon && (
96
+ <span
97
+ {...iconProps}
98
+ style={{
99
+ display: 'inline-flex',
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ }}
103
+ >
104
+ {renderIcon()}
105
+ </span>
106
+ )}
107
+ <span {...itemTextProps}>
108
+ {item.label}
109
+ </span>
109
110
  </div>
110
111
  );
111
112
  };
@@ -121,8 +122,7 @@ const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = ({ separator, si
121
122
  const isTextSeparator = typeof separator === 'string';
122
123
 
123
124
  if (isTextSeparator) {
124
- const separatorStyle_ = (breadcrumbStyles.separator as any)({});
125
- const separatorProps = getWebProps([separatorStyle_, separatorStyle]);
125
+ const separatorProps = getWebProps([breadcrumbStyles.separator, separatorStyle]);
126
126
  return (
127
127
  <span {...separatorProps} aria-hidden="true">
128
128
  {separator}
@@ -130,8 +130,7 @@ const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = ({ separator, si
130
130
  );
131
131
  }
132
132
 
133
- const separatorIconStyle = (breadcrumbStyles.separatorIcon as any)({});
134
- const separatorIconProps = getWebProps([separatorIconStyle, separatorStyle]);
133
+ const separatorIconProps = getWebProps([breadcrumbStyles.separatorIcon, separatorStyle]);
135
134
  return (
136
135
  <span {...separatorIconProps} aria-hidden="true">
137
136
  {separator}
@@ -145,11 +144,9 @@ interface BreadcrumbEllipsisProps {
145
144
  }
146
145
 
147
146
  const BreadcrumbEllipsis: React.FC<BreadcrumbEllipsisProps> = ({ size, intent }) => {
148
- breadcrumbStyles.useVariants({ size });
149
- const ellipsisStyle = (breadcrumbStyles.ellipsis as any)({});
150
- const ellipsisIconStyle = (breadcrumbStyles.ellipsisIcon as any)({ intent });
151
- const ellipsisProps = getWebProps([ellipsisStyle]);
152
- const iconProps = getWebProps([ellipsisIconStyle]);
147
+ breadcrumbStyles.useVariants({ size, intent });
148
+ const ellipsisProps = getWebProps([breadcrumbStyles.ellipsis]);
149
+ const iconProps = getWebProps([breadcrumbStyles.ellipsisIcon]);
153
150
 
154
151
  return (
155
152
  <span {...ellipsisProps}>
@@ -182,17 +179,10 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
182
179
  }) => {
183
180
  const [menuOpen, setMenuOpen] = useState(false);
184
181
 
185
- // Get dynamic styles - call as functions for theme reactivity
186
- const containerStyle = (breadcrumbStyles.container as any)({});
187
- const menuButtonStyle = (breadcrumbStyles.menuButton as any)({});
188
- const menuButtonIconStyle = (breadcrumbStyles.menuButtonIcon as any)({ intent });
189
-
190
- const containerProps = getWebProps([containerStyle, style as any]);
191
-
192
- // Apply variants for menu button
193
- breadcrumbStyles.useVariants({ size });
194
- const menuButtonProps = getWebProps([menuButtonStyle]);
195
- const menuIconProps = getWebProps([menuButtonIconStyle]);
182
+ breadcrumbStyles.useVariants({ size, intent });
183
+ const containerProps = getWebProps([breadcrumbStyles.container, style as any]);
184
+ const menuButtonProps = getWebProps([breadcrumbStyles.menuButton]);
185
+ const menuIconProps = getWebProps([breadcrumbStyles.menuButtonIcon]);
196
186
 
197
187
  // Handle responsive collapsing
198
188
  let displayItems = items;
@@ -57,12 +57,12 @@ export const menuStyles = defineStyle('Menu', (theme: Theme) => ({
57
57
  flexDirection: 'row' as const,
58
58
  alignItems: 'center' as const,
59
59
  backgroundColor: 'transparent' as const,
60
- borderRadius: 4,
61
- minHeight: 44,
62
60
  variants: {
63
61
  size: {
64
62
  paddingVertical: theme.sizes.$menu.paddingVertical,
65
63
  paddingHorizontal: theme.sizes.$menu.paddingHorizontal,
64
+ minHeight: theme.sizes.$menu.minHeight,
65
+ borderRadius: theme.sizes.$menu.borderRadius,
66
66
  },
67
67
  intent: {
68
68
  neutral: {
@@ -147,13 +147,13 @@ export const menuStyles = defineStyle('Menu', (theme: Theme) => ({
147
147
  alignItems: 'center' as const,
148
148
  justifyContent: 'center' as const,
149
149
  flexShrink: 0,
150
- marginRight: 8,
151
150
  color: theme.colors.text.primary,
152
151
  variants: {
153
152
  size: {
154
153
  width: theme.sizes.$menu.iconSize,
155
154
  height: theme.sizes.$menu.iconSize,
156
155
  fontSize: theme.sizes.$menu.iconSize,
156
+ marginRight: theme.sizes.$menu.iconGap,
157
157
  },
158
158
  intent: {
159
159
  neutral: {},
@@ -25,12 +25,12 @@ export const menuItemStyles = defineStyle('MenuItem', (theme: Theme) => ({
25
25
  flexDirection: 'row' as const,
26
26
  alignItems: 'center' as const,
27
27
  backgroundColor: 'transparent' as const,
28
- borderRadius: 4,
29
- minHeight: 44,
30
28
  variants: {
31
29
  size: {
32
30
  paddingVertical: theme.sizes.$menu.paddingVertical,
33
31
  paddingHorizontal: theme.sizes.$menu.paddingHorizontal,
32
+ minHeight: theme.sizes.$menu.minHeight,
33
+ borderRadius: theme.sizes.$menu.borderRadius,
34
34
  },
35
35
  intent: {
36
36
  neutral: {
@@ -110,13 +110,13 @@ export const menuItemStyles = defineStyle('MenuItem', (theme: Theme) => ({
110
110
  alignItems: 'center' as const,
111
111
  justifyContent: 'center' as const,
112
112
  flexShrink: 0,
113
- marginRight: 12,
114
113
  color: theme.colors.text.primary,
115
114
  variants: {
116
115
  size: {
117
116
  width: theme.sizes.$menu.iconSize,
118
117
  height: theme.sizes.$menu.iconSize,
119
118
  fontSize: theme.sizes.$menu.iconSize,
119
+ marginRight: theme.sizes.$menu.iconGap,
120
120
  },
121
121
  intent: {
122
122
  neutral: {},
@@ -135,6 +135,9 @@ export const menuItemStyles = defineStyle('MenuItem', (theme: Theme) => ({
135
135
  label: (_props: MenuItemDynamicProps) => ({
136
136
  flex: 1,
137
137
  color: theme.colors.text.primary,
138
+ _web: {
139
+ whiteSpace: 'nowrap',
140
+ },
138
141
  variants: {
139
142
  size: {
140
143
  fontSize: theme.sizes.$menu.labelFontSize,
@@ -18,6 +18,7 @@ const MenuItem = forwardRef<IdealystElement, MenuItemProps>(({ item, onPress, si
18
18
  // Initialize styles with useVariants (for size and disabled)
19
19
  menuItemStyles.useVariants({
20
20
  size,
21
+ intent: item.intent || 'neutral',
21
22
  disabled: Boolean(item.disabled),
22
23
  });
23
24
 
@@ -54,22 +55,10 @@ const MenuItem = forwardRef<IdealystElement, MenuItemProps>(({ item, onPress, si
54
55
  // Merge refs
55
56
  const mergedRef = useMergeRefs(ref, itemProps.ref);
56
57
 
57
- // Button reset styles that must be applied directly
58
- const buttonResetStyles: React.CSSProperties = {
59
- display: 'flex',
60
- width: '100%',
61
- border: 'none',
62
- outline: 'none',
63
- cursor: item.disabled ? 'not-allowed' : 'pointer',
64
- background: 'transparent',
65
- textAlign: 'left',
66
- };
67
-
68
58
  return (
69
59
  <button
70
60
  {...itemProps}
71
61
  ref={mergedRef}
72
- style={buttonResetStyles}
73
62
  onClick={(e: React.MouseEvent) => {
74
63
  e.preventDefault();
75
64
  e.stopPropagation();
@@ -1,8 +1,11 @@
1
- import React, { forwardRef, useMemo, ReactNode } from 'react';
2
- import { View, ScrollView, Text, TouchableOpacity } from 'react-native';
1
+ import React, { forwardRef, useMemo, useState, useCallback, ReactNode } from 'react';
2
+ import { View, ScrollView, Text, TouchableOpacity, Pressable } from 'react-native';
3
3
  import { tableStyles } from './Table.styles';
4
- import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant } from './types';
4
+ import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant, SortDirection } from './types';
5
+ import type { MenuItem } from '../Menu/types';
5
6
  import { getNativeAccessibilityProps } from '../utils/accessibility';
7
+ import Icon from '../Icon/Icon.native';
8
+ import Menu from '../Menu/Menu.native';
6
9
 
7
10
  // ============================================================================
8
11
  // Sub-component Props
@@ -25,6 +28,10 @@ interface THProps {
25
28
  type?: TableType;
26
29
  align?: TableAlignVariant;
27
30
  width?: number | string;
31
+ sortable?: boolean;
32
+ sortDirection?: SortDirection;
33
+ onSort?: () => void;
34
+ options?: MenuItem[];
28
35
  }
29
36
 
30
37
  interface TDProps {
@@ -83,29 +90,72 @@ function TH({
83
90
  type = 'standard',
84
91
  align = 'left',
85
92
  width,
93
+ sortable,
94
+ sortDirection,
95
+ onSort,
96
+ options,
86
97
  }: THProps) {
98
+ const [menuOpen, setMenuOpen] = useState(false);
99
+
87
100
  tableStyles.useVariants({
88
101
  size,
89
102
  type,
90
103
  align,
104
+ sortable: !!sortable,
105
+ sortActive: sortDirection != null,
91
106
  });
92
107
 
93
108
  const headerCellStyle = (tableStyles.headerCell as any)({});
109
+ const sortIndicatorStyle = (tableStyles.sortIndicator as any)({ sortActive: sortDirection != null });
110
+ const optionsButtonStyle = (tableStyles.optionsButton as any)({});
94
111
 
95
- return (
112
+ const sortIconName = sortDirection === 'asc' ? 'arrow-up' :
113
+ sortDirection === 'desc' ? 'arrow-down' : 'arrow-up-down';
114
+
115
+ const content = (
96
116
  <View
97
117
  style={[
98
118
  headerCellStyle,
99
119
  { width, flex: width ? undefined : 1 },
100
120
  ]}
101
121
  >
102
- {typeof children === 'string' ? (
103
- <Text style={headerCellStyle}>{children}</Text>
104
- ) : (
105
- children
122
+ <View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, gap: 2 }}>
123
+ {typeof children === 'string' ? (
124
+ <Text style={headerCellStyle}>{children}</Text>
125
+ ) : (
126
+ children
127
+ )}
128
+ {sortable && (
129
+ <View style={sortIndicatorStyle}>
130
+ <Icon name={sortIconName} size={size} />
131
+ </View>
132
+ )}
133
+ </View>
134
+ {options && options.length > 0 && (
135
+ <Menu
136
+ items={options}
137
+ open={menuOpen}
138
+ onOpenChange={setMenuOpen}
139
+ placement="bottom-start"
140
+ size={size}
141
+ >
142
+ <Pressable style={optionsButtonStyle}>
143
+ <Icon name="dots-vertical" size={size} />
144
+ </Pressable>
145
+ </Menu>
106
146
  )}
107
147
  </View>
108
148
  );
149
+
150
+ if (sortable) {
151
+ return (
152
+ <Pressable onPress={onSort}>
153
+ {content}
154
+ </Pressable>
155
+ );
156
+ }
157
+
158
+ return content;
109
159
  }
110
160
 
111
161
  // ============================================================================
@@ -193,6 +243,7 @@ function TableInner<T = any>({
193
243
  size = 'md',
194
244
  stickyHeader: _stickyHeader = false,
195
245
  onRowPress,
246
+ onSort,
196
247
  dividers = false,
197
248
  emptyState,
198
249
  // Spacing variants from ContainerStyleProps
@@ -212,6 +263,26 @@ function TableInner<T = any>({
212
263
  accessibilityRole,
213
264
  accessibilityHidden,
214
265
  }: TableProps<T>, ref: React.Ref<ScrollView>) {
266
+ // Sort state
267
+ const [sortColumn, setSortColumn] = useState<string | null>(null);
268
+ const [sortDirection, setSortDirection] = useState<SortDirection>(null);
269
+
270
+ const handleSort = useCallback((columnKey: string) => {
271
+ let newDir: SortDirection;
272
+ if (sortColumn !== columnKey) {
273
+ newDir = 'asc';
274
+ } else if (sortDirection === 'asc') {
275
+ newDir = 'desc';
276
+ } else {
277
+ setSortColumn(null);
278
+ setSortDirection(null);
279
+ onSort?.(columnKey, null);
280
+ return;
281
+ }
282
+ setSortColumn(columnKey);
283
+ setSortDirection(newDir);
284
+ onSort?.(columnKey, newDir);
285
+ }, [sortColumn, sortDirection, onSort]);
215
286
  // Generate native accessibility props
216
287
  const nativeA11yProps = useMemo(() => {
217
288
  return getNativeAccessibilityProps({
@@ -276,7 +347,17 @@ function TableInner<T = any>({
276
347
  <View style={theadStyle}>
277
348
  <View style={{ flexDirection: 'row' }}>
278
349
  {cols.map((column) => (
279
- <TH key={column.key} size={size} type={type} align={column.align} width={column.width}>
350
+ <TH
351
+ key={column.key}
352
+ size={size}
353
+ type={type}
354
+ align={column.align}
355
+ width={column.width}
356
+ sortable={column.sortable}
357
+ sortDirection={sortColumn === column.key ? sortDirection : undefined}
358
+ onSort={column.sortable ? () => handleSort(column.key) : undefined}
359
+ options={column.options}
360
+ >
280
361
  {column.title}
281
362
  </TH>
282
363
  ))}
@@ -23,6 +23,8 @@ export type TableDynamicProps = {
23
23
  even?: boolean;
24
24
  sticky?: boolean;
25
25
  align?: CellAlign;
26
+ sortable?: boolean;
27
+ sortActive?: boolean;
26
28
  gap?: ViewStyleSize;
27
29
  padding?: ViewStyleSize;
28
30
  paddingVertical?: ViewStyleSize;
@@ -178,6 +180,18 @@ export const tableStyles = defineStyle('Table', (theme: Theme) => ({
178
180
  fontSize: theme.sizes.$table.fontSize,
179
181
  lineHeight: theme.sizes.$table.lineHeight,
180
182
  },
183
+ sortable: {
184
+ true: {
185
+ _web: {
186
+ cursor: 'pointer',
187
+ userSelect: 'none',
188
+ transition: 'background-color 0.15s ease',
189
+ _hover: {
190
+ backgroundColor: theme.colors.surface.hover,
191
+ },
192
+ },
193
+ },
194
+ },
181
195
  },
182
196
  _web: {
183
197
  position: 'relative',
@@ -185,6 +199,53 @@ export const tableStyles = defineStyle('Table', (theme: Theme) => ({
185
199
  },
186
200
  }),
187
201
 
202
+ sortIndicator: ({ sortActive = false }: TableDynamicProps) => ({
203
+ display: 'flex' as const,
204
+ alignItems: 'center' as const,
205
+ justifyContent: 'center' as const,
206
+ marginLeft: 4,
207
+ opacity: sortActive ? 1 : 0.4,
208
+ color: sortActive ? theme.colors.text.primary : theme.colors.text.tertiary,
209
+ flexShrink: 0,
210
+ _web: {
211
+ transition: 'opacity 0.15s ease, color 0.15s ease',
212
+ },
213
+ variants: {
214
+ size: {
215
+ width: theme.sizes.$table.fontSize,
216
+ height: theme.sizes.$table.fontSize,
217
+ },
218
+ },
219
+ }),
220
+
221
+ optionsButton: (_props: TableDynamicProps) => ({
222
+ display: 'flex' as const,
223
+ alignItems: 'center' as const,
224
+ justifyContent: 'center' as const,
225
+ marginLeft: 4,
226
+ borderRadius: 4,
227
+ padding: 2,
228
+ opacity: 0.4,
229
+ flexShrink: 0,
230
+ color: theme.colors.text.tertiary,
231
+ _web: {
232
+ cursor: 'pointer',
233
+ border: 'none',
234
+ background: 'transparent',
235
+ transition: 'opacity 0.15s ease, background-color 0.15s ease',
236
+ _hover: {
237
+ opacity: 1,
238
+ backgroundColor: theme.colors.surface.hover,
239
+ },
240
+ },
241
+ variants: {
242
+ size: {
243
+ width: theme.sizes.$table.fontSize,
244
+ height: theme.sizes.$table.fontSize,
245
+ },
246
+ },
247
+ }),
248
+
188
249
  cell: (_props: TableDynamicProps) => ({
189
250
  flexDirection: 'row' as const,
190
251
  alignItems: 'center' as const,
@@ -1,8 +1,11 @@
1
- import { useMemo, useRef, useCallback, ReactNode } from 'react';
1
+ import { useMemo, useRef, useCallback, useState, ReactNode } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { tableStyles } from './Table.styles';
4
- import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant } from './types';
4
+ import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant, SortDirection } from './types';
5
+ import type { MenuItem } from '../Menu/types';
5
6
  import { getWebAriaProps } from '../utils/accessibility';
7
+ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
8
+ import Menu from '../Menu/Menu.web';
6
9
 
7
10
  // ============================================================================
8
11
  // Helpers
@@ -49,6 +52,10 @@ interface THProps {
49
52
  onResize?: (width: number) => void;
50
53
  minWidth?: number;
51
54
  accessibilitySort?: 'ascending' | 'descending' | 'none' | 'other';
55
+ sortable?: boolean;
56
+ sortDirection?: SortDirection;
57
+ onSort?: () => void;
58
+ options?: MenuItem[];
52
59
  }
53
60
 
54
61
  interface TDProps {
@@ -110,16 +117,37 @@ function TH({
110
117
  onResize,
111
118
  minWidth = 50,
112
119
  accessibilitySort,
120
+ sortable,
121
+ sortDirection,
122
+ onSort,
123
+ options,
113
124
  }: THProps) {
125
+ const [menuOpen, setMenuOpen] = useState(false);
126
+
114
127
  tableStyles.useVariants({
115
128
  size,
116
129
  type,
117
130
  align,
131
+ sortable: !!sortable,
132
+ sortActive: sortDirection != null,
118
133
  });
119
134
 
120
135
  const headerCellProps = getWebProps([(tableStyles.headerCell as any)({})]);
136
+ const sortIndicatorProps = getWebProps([(tableStyles.sortIndicator as any)({ sortActive: sortDirection != null })]);
137
+ const optionsButtonProps = getWebProps([(tableStyles.optionsButton as any)({})]);
121
138
  const thRef = useRef<HTMLTableCellElement>(null);
122
139
 
140
+ // Derive aria-sort from sortDirection
141
+ const derivedAriaSort = accessibilitySort ?? (
142
+ sortDirection === 'asc' ? 'ascending' :
143
+ sortDirection === 'desc' ? 'descending' :
144
+ sortable ? 'none' : undefined
145
+ );
146
+
147
+ // Sort indicator icon name
148
+ const sortIconName = sortDirection === 'asc' ? 'arrow-up' :
149
+ sortDirection === 'desc' ? 'arrow-down' : 'arrow-up-down';
150
+
123
151
  const handlePointerDown = useCallback((e: React.PointerEvent) => {
124
152
  e.preventDefault();
125
153
  e.stopPropagation();
@@ -139,12 +167,10 @@ function TH({
139
167
  document.removeEventListener('pointerup', handlePointerUp);
140
168
  const finalWidth = th.getBoundingClientRect().width;
141
169
  onResize?.(finalWidth);
142
- // Remove inline cursor override
143
170
  document.body.style.cursor = '';
144
171
  document.body.style.userSelect = '';
145
172
  };
146
173
 
147
- // Prevent text selection and set resize cursor globally during drag
148
174
  document.body.style.cursor = 'col-resize';
149
175
  document.body.style.userSelect = 'none';
150
176
  document.addEventListener('pointermove', handlePointerMove);
@@ -156,10 +182,47 @@ function TH({
156
182
  {...headerCellProps}
157
183
  ref={thRef}
158
184
  scope="col"
159
- aria-sort={accessibilitySort}
185
+ aria-sort={derivedAriaSort}
160
186
  style={{ width, ...getStickyStyle(sticky, stickyOffset, 11) }}
187
+ onClick={sortable ? onSort : undefined}
161
188
  >
162
- {children}
189
+ <span style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
190
+ <span style={{ flex: 1, minWidth: 0 }}>
191
+ {children}
192
+ </span>
193
+ {sortable && (
194
+ <span {...sortIndicatorProps} style={{ display: 'inline-flex', flexShrink: 0 }}>
195
+ <IconSvg name={sortIconName} size="0.85em" aria-label={sortIconName} />
196
+ </span>
197
+ )}
198
+ {options && options.length > 0 && (
199
+ <span onClick={(e) => e.stopPropagation()} style={{ display: 'inline-flex', flexShrink: 0 }}>
200
+ <Menu
201
+ items={options}
202
+ open={menuOpen}
203
+ onOpenChange={setMenuOpen}
204
+ placement="bottom-start"
205
+ size={size}
206
+ >
207
+ <button
208
+ {...optionsButtonProps}
209
+ style={{
210
+ background: 'transparent',
211
+ border: 'none',
212
+ cursor: 'pointer',
213
+ display: 'inline-flex',
214
+ alignItems: 'center',
215
+ justifyContent: 'center',
216
+ padding: 2,
217
+ }}
218
+ aria-label="Column options"
219
+ >
220
+ <IconSvg name="dots-vertical" size="0.85em" aria-label="dots-vertical" />
221
+ </button>
222
+ </Menu>
223
+ </span>
224
+ )}
225
+ </span>
163
226
  {resizable && (
164
227
  <span
165
228
  onPointerDown={handlePointerDown}
@@ -266,6 +329,7 @@ function Table<T = any>({
266
329
  stickyHeader = false,
267
330
  onRowPress,
268
331
  onColumnResize,
332
+ onSort,
269
333
  dividers = false,
270
334
  emptyState,
271
335
  // Spacing variants from ContainerStyleProps
@@ -285,6 +349,26 @@ function Table<T = any>({
285
349
  accessibilityRole,
286
350
  accessibilityHidden,
287
351
  }: TableProps<T>) {
352
+ // Sort state
353
+ const [sortColumn, setSortColumn] = useState<string | null>(null);
354
+ const [sortDirection, setSortDirection] = useState<SortDirection>(null);
355
+
356
+ const handleSort = useCallback((columnKey: string) => {
357
+ let newDir: SortDirection;
358
+ if (sortColumn !== columnKey) {
359
+ newDir = 'asc';
360
+ } else if (sortDirection === 'asc') {
361
+ newDir = 'desc';
362
+ } else {
363
+ setSortColumn(null);
364
+ setSortDirection(null);
365
+ onSort?.(columnKey, null);
366
+ return;
367
+ }
368
+ setSortColumn(columnKey);
369
+ setSortDirection(newDir);
370
+ onSort?.(columnKey, newDir);
371
+ }, [sortColumn, sortDirection, onSort]);
288
372
  // Generate ARIA props
289
373
  const ariaProps = useMemo(() => {
290
374
  return getWebAriaProps({
@@ -378,6 +462,10 @@ function Table<T = any>({
378
462
  minWidth={column.minWidth}
379
463
  onResize={onColumnResize ? (w) => onColumnResize(column.key, w) : undefined}
380
464
  accessibilitySort={column.accessibilitySort}
465
+ sortable={column.sortable}
466
+ sortDirection={sortColumn === column.key ? sortDirection : undefined}
467
+ onSort={column.sortable ? () => handleSort(column.key) : undefined}
468
+ options={column.options}
381
469
  >
382
470
  {column.title}
383
471
  </TH>
@@ -3,11 +3,13 @@ import type { ReactNode } from 'react';
3
3
  import { Size } from '@idealyst/theme';
4
4
  import { ContainerStyleProps } from '../utils/viewStyleProps';
5
5
  import { AccessibilityProps, SortableAccessibilityProps } from '../utils/accessibility';
6
+ import type { MenuItem } from '../Menu/types';
6
7
 
7
8
  // Component-specific type aliases for future extensibility
8
9
  export type TableSizeVariant = Size;
9
10
  export type TableType = 'standard' | 'striped';
10
11
  export type TableAlignVariant = 'left' | 'center' | 'right';
12
+ export type SortDirection = 'asc' | 'desc' | null;
11
13
 
12
14
  export interface TableColumn<T = any> extends SortableAccessibilityProps {
13
15
  key: string;
@@ -32,6 +34,16 @@ export interface TableColumn<T = any> extends SortableAccessibilityProps {
32
34
  * Minimum width when resizing (default: 50).
33
35
  */
34
36
  minWidth?: number;
37
+ /**
38
+ * Enables click-to-sort cycling on this column header.
39
+ * Cycles: unsorted → ascending → descending → unsorted.
40
+ */
41
+ sortable?: boolean;
42
+ /**
43
+ * Menu items to show in a column options dropdown.
44
+ * Uses the existing MenuItem type from the Menu component.
45
+ */
46
+ options?: MenuItem[];
35
47
  }
36
48
 
37
49
  /**
@@ -58,6 +70,11 @@ export interface TableProps<T = any> extends ContainerStyleProps, AccessibilityP
58
70
  * Receives the column key and the new width in pixels.
59
71
  */
60
72
  onColumnResize?: (key: string, width: number) => void;
73
+ /**
74
+ * Called when sort state changes via header click.
75
+ * The Table manages sort state internally; the parent handles data ordering.
76
+ */
77
+ onSort?: (columnKey: string, direction: SortDirection) => void;
61
78
  /**
62
79
  * Content to display when `data` is empty.
63
80
  * Renders in place of the table body.
@@ -1,7 +1,7 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
  import { Screen, View, Text, Badge, Button } from '@idealyst/components';
3
3
  import Table from '../Table';
4
- import type { TableColumn } from '../Table/types';
4
+ import type { TableColumn, SortDirection } from '../Table/types';
5
5
 
6
6
  interface User {
7
7
  id: number;
@@ -287,9 +287,102 @@ export const TableExamples: React.FC = () => {
287
287
  No data to display
288
288
  </Text>
289
289
  </View>
290
+
291
+ <SortableTableExample />
292
+ <OptionsTableExample />
290
293
  </View>
291
294
  </Screen>
292
295
  );
293
296
  };
294
297
 
298
+ const SortableTableExample: React.FC = () => {
299
+ const [sortedProducts, setSortedProducts] = useState([
300
+ { id: 1, name: 'Laptop', category: 'Electronics', price: 999.99, stock: 15 },
301
+ { id: 2, name: 'Mouse', category: 'Electronics', price: 29.99, stock: 150 },
302
+ { id: 3, name: 'Keyboard', category: 'Electronics', price: 79.99, stock: 75 },
303
+ { id: 4, name: 'Monitor', category: 'Electronics', price: 299.99, stock: 30 },
304
+ { id: 5, name: 'Desk', category: 'Furniture', price: 399.99, stock: 10 },
305
+ ]);
306
+
307
+ const handleSort = (columnKey: string, direction: SortDirection) => {
308
+ if (!direction) {
309
+ setSortedProducts([...sortedProducts]);
310
+ return;
311
+ }
312
+ const sorted = [...sortedProducts].sort((a, b) => {
313
+ const aVal = (a as any)[columnKey];
314
+ const bVal = (b as any)[columnKey];
315
+ if (typeof aVal === 'number') return direction === 'asc' ? aVal - bVal : bVal - aVal;
316
+ return direction === 'asc'
317
+ ? String(aVal).localeCompare(String(bVal))
318
+ : String(bVal).localeCompare(String(aVal));
319
+ });
320
+ setSortedProducts(sorted);
321
+ };
322
+
323
+ const columns: TableColumn<typeof sortedProducts[0]>[] = [
324
+ { key: 'name', title: 'Product', dataIndex: 'name', sortable: true },
325
+ { key: 'category', title: 'Category', dataIndex: 'category', width: '150px', sortable: true },
326
+ { key: 'price', title: 'Price', dataIndex: 'price', width: '120px', align: 'right', sortable: true, render: (price: number) => `$${price.toFixed(2)}` },
327
+ { key: 'stock', title: 'Stock', dataIndex: 'stock', width: '100px', align: 'center', sortable: true },
328
+ ];
329
+
330
+ return (
331
+ <View gap="md">
332
+ <Text typography="h5">Sortable Table</Text>
333
+ <Table columns={columns} data={sortedProducts} onSort={handleSort} dividers />
334
+ <Text typography="caption" color="secondary">
335
+ Click column headers to cycle sort: unsorted, ascending, descending
336
+ </Text>
337
+ </View>
338
+ );
339
+ };
340
+
341
+ const OptionsTableExample: React.FC = () => {
342
+ const data = [
343
+ { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
344
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
345
+ { id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'User' },
346
+ ];
347
+
348
+ const columns: TableColumn<typeof data[0]>[] = [
349
+ {
350
+ key: 'name',
351
+ title: 'Name',
352
+ dataIndex: 'name',
353
+ sortable: true,
354
+ options: [
355
+ { id: 'sort-asc', label: 'Sort A-Z', icon: 'sort-ascending', onClick: () => console.log('Sort A-Z') },
356
+ { id: 'sort-desc', label: 'Sort Z-A', icon: 'sort-descending', onClick: () => console.log('Sort Z-A') },
357
+ { id: 'sep', label: '', separator: true },
358
+ { id: 'hide', label: 'Hide Column', icon: 'eye-off', onClick: () => console.log('Hide column') },
359
+ ],
360
+ },
361
+ {
362
+ key: 'email',
363
+ title: 'Email',
364
+ dataIndex: 'email',
365
+ options: [
366
+ { id: 'copy', label: 'Copy All Emails', icon: 'content-copy', onClick: () => console.log('Copy emails') },
367
+ ],
368
+ },
369
+ {
370
+ key: 'role',
371
+ title: 'Role',
372
+ dataIndex: 'role',
373
+ width: '120px',
374
+ },
375
+ ];
376
+
377
+ return (
378
+ <View gap="md">
379
+ <Text typography="h5">Column Options Menu</Text>
380
+ <Table columns={columns} data={data} dividers />
381
+ <Text typography="caption" color="secondary">
382
+ Columns with options show a kebab menu icon in the header
383
+ </Text>
384
+ </View>
385
+ );
386
+ };
387
+
295
388
  export default TableExamples;