@idealyst/components 1.3.4 → 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.4",
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.4",
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.4",
115
- "@idealyst/tooling": "^1.3.4",
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",
@@ -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;