@titan-design/react-ui 0.1.0 → 0.1.1

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 (192) hide show
  1. package/package.json +10 -5
  2. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  3. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  4. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  5. package/src/components/custom/DateTime/index.ts +2 -0
  6. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  7. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  8. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  9. package/src/components/custom/EmptyState/index.ts +2 -0
  10. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  11. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  12. package/src/components/custom/Metric/Metric.tsx +92 -0
  13. package/src/components/custom/Metric/index.ts +2 -0
  14. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  15. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  16. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  17. package/src/components/custom/Sidebar/index.ts +18 -0
  18. package/src/components/custom/Table/Table.stories.tsx +481 -0
  19. package/src/components/custom/Table/Table.test.tsx +531 -0
  20. package/src/components/custom/Table/Table.tsx +696 -0
  21. package/src/components/custom/Table/index.ts +22 -0
  22. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  23. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  24. package/src/components/custom/Typography/Typography.tsx +218 -0
  25. package/src/components/custom/Typography/index.ts +19 -0
  26. package/src/components/custom/index.ts +8 -0
  27. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  28. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  29. package/src/components/custom/stepper/Stepper.tsx +250 -0
  30. package/src/components/custom/stepper/index.ts +10 -0
  31. package/src/components/index.ts +5 -0
  32. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  33. package/src/components/ui/alert/Alert.test.tsx +202 -0
  34. package/src/components/ui/alert/Alert.tsx +170 -0
  35. package/src/components/ui/alert/index.ts +8 -0
  36. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  37. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  38. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  39. package/src/components/ui/autocomplete/index.ts +2 -0
  40. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  41. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  42. package/src/components/ui/avatar/Avatar.tsx +168 -0
  43. package/src/components/ui/avatar/index.ts +2 -0
  44. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  45. package/src/components/ui/badge/Badge.test.tsx +86 -0
  46. package/src/components/ui/badge/Badge.tsx +105 -0
  47. package/src/components/ui/badge/index.ts +2 -0
  48. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  49. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  50. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  51. package/src/components/ui/breadcrumbs/index.ts +2 -0
  52. package/src/components/ui/button/Button.stories.tsx +216 -0
  53. package/src/components/ui/button/Button.test.tsx +132 -0
  54. package/src/components/ui/button/Button.tsx +242 -0
  55. package/src/components/ui/button/index.ts +10 -0
  56. package/src/components/ui/card/Card.stories.tsx +443 -0
  57. package/src/components/ui/card/Card.test.tsx +265 -0
  58. package/src/components/ui/card/Card.tsx +380 -0
  59. package/src/components/ui/card/index.ts +20 -0
  60. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  61. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  62. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  63. package/src/components/ui/checkbox/index.ts +2 -0
  64. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  65. package/src/components/ui/chip/Chip.test.tsx +119 -0
  66. package/src/components/ui/chip/Chip.tsx +151 -0
  67. package/src/components/ui/chip/index.ts +2 -0
  68. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  69. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  70. package/src/components/ui/collapse/Collapse.tsx +275 -0
  71. package/src/components/ui/collapse/index.ts +18 -0
  72. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  73. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  74. package/src/components/ui/data-row/DataRow.tsx +29 -0
  75. package/src/components/ui/data-row/index.ts +2 -0
  76. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  77. package/src/components/ui/divider/Divider.test.tsx +55 -0
  78. package/src/components/ui/divider/Divider.tsx +37 -0
  79. package/src/components/ui/divider/index.ts +2 -0
  80. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  81. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  82. package/src/components/ui/drawer/Drawer.tsx +228 -0
  83. package/src/components/ui/drawer/index.ts +9 -0
  84. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  85. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  86. package/src/components/ui/form-field/FormField.tsx +308 -0
  87. package/src/components/ui/form-field/index.ts +2 -0
  88. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  89. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  90. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  91. package/src/components/ui/help-tip/index.ts +2 -0
  92. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  93. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  94. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  95. package/src/components/ui/icon-box/index.ts +1 -0
  96. package/src/components/ui/index.ts +36 -0
  97. package/src/components/ui/input/Input.stories.tsx +192 -0
  98. package/src/components/ui/input/Input.test.tsx +86 -0
  99. package/src/components/ui/input/Input.tsx +273 -0
  100. package/src/components/ui/input/index.ts +2 -0
  101. package/src/components/ui/link/Link.stories.tsx +78 -0
  102. package/src/components/ui/link/Link.test.tsx +94 -0
  103. package/src/components/ui/link/Link.tsx +86 -0
  104. package/src/components/ui/link/index.ts +2 -0
  105. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  106. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  107. package/src/components/ui/list-item/ListItem.tsx +89 -0
  108. package/src/components/ui/list-item/index.ts +14 -0
  109. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  110. package/src/components/ui/menu/Menu.test.tsx +353 -0
  111. package/src/components/ui/menu/Menu.tsx +222 -0
  112. package/src/components/ui/menu/index.ts +2 -0
  113. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  114. package/src/components/ui/modal/Modal.test.tsx +159 -0
  115. package/src/components/ui/modal/Modal.tsx +254 -0
  116. package/src/components/ui/modal/index.ts +19 -0
  117. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  118. package/src/components/ui/popover/Popover.test.tsx +277 -0
  119. package/src/components/ui/popover/Popover.tsx +162 -0
  120. package/src/components/ui/popover/index.ts +2 -0
  121. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  122. package/src/components/ui/progress/Progress.test.tsx +181 -0
  123. package/src/components/ui/progress/Progress.tsx +320 -0
  124. package/src/components/ui/progress/index.ts +9 -0
  125. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  126. package/src/components/ui/radio/Radio.test.tsx +166 -0
  127. package/src/components/ui/radio/Radio.tsx +200 -0
  128. package/src/components/ui/radio/index.ts +2 -0
  129. package/src/components/ui/section/Section.stories.tsx +111 -0
  130. package/src/components/ui/section/Section.test.tsx +121 -0
  131. package/src/components/ui/section/Section.tsx +52 -0
  132. package/src/components/ui/section/index.ts +6 -0
  133. package/src/components/ui/select/Select.stories.tsx +286 -0
  134. package/src/components/ui/select/Select.test.tsx +180 -0
  135. package/src/components/ui/select/Select.tsx +236 -0
  136. package/src/components/ui/select/index.ts +2 -0
  137. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  138. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  139. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  140. package/src/components/ui/skeleton/index.ts +16 -0
  141. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  142. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  143. package/src/components/ui/spinner/Spinner.tsx +72 -0
  144. package/src/components/ui/spinner/index.ts +2 -0
  145. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  146. package/src/components/ui/stack/Stack.test.tsx +184 -0
  147. package/src/components/ui/stack/Stack.tsx +76 -0
  148. package/src/components/ui/stack/index.ts +2 -0
  149. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  150. package/src/components/ui/surface/Surface.test.tsx +63 -0
  151. package/src/components/ui/surface/Surface.tsx +51 -0
  152. package/src/components/ui/surface/index.ts +1 -0
  153. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  154. package/src/components/ui/switch/Switch.test.tsx +112 -0
  155. package/src/components/ui/switch/Switch.tsx +103 -0
  156. package/src/components/ui/switch/index.ts +2 -0
  157. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  158. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  159. package/src/components/ui/tabs/Tabs.tsx +253 -0
  160. package/src/components/ui/tabs/index.ts +16 -0
  161. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  162. package/src/components/ui/toast/Toast.test.tsx +207 -0
  163. package/src/components/ui/toast/Toast.tsx +327 -0
  164. package/src/components/ui/toast/index.ts +8 -0
  165. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  166. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  167. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  168. package/src/components/ui/toolbar-button/index.ts +7 -0
  169. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  170. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  171. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  172. package/src/components/ui/tooltip/index.ts +2 -0
  173. package/src/index.ts +10 -0
  174. package/src/test/setup.ts +19 -0
  175. package/src/theme/Colors.stories.tsx +258 -0
  176. package/src/theme/config.ts +193 -0
  177. package/src/theme/elevation.stories.tsx +346 -0
  178. package/src/theme/elevation.test.ts +27 -0
  179. package/src/theme/elevation.ts +567 -0
  180. package/src/theme/index.ts +39 -0
  181. package/src/theme/shadows.stories.tsx +523 -0
  182. package/src/theme/shadows.ts +439 -0
  183. package/src/theme/tokens/index.ts +2 -0
  184. package/src/theme/tokens/primitives.ts +354 -0
  185. package/src/theme/tokens/semantic.ts +381 -0
  186. package/src/types/jest-axe.d.ts +16 -0
  187. package/src/types/nativewind.d.ts +54 -0
  188. package/src/utils/cn.ts +14 -0
  189. package/src/utils/colors.ts +140 -0
  190. package/src/utils/form.ts +188 -0
  191. package/src/utils/index.ts +29 -0
  192. package/src/utils/useTheme.ts +72 -0
@@ -0,0 +1,696 @@
1
+ import React, { createContext, useContext, useState, useMemo } from 'react'
2
+ import { View, Text, Pressable, ScrollView, type ViewProps, type PressableProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type SortDirection = 'asc' | 'desc' | null
6
+
7
+ interface TableContextType {
8
+ sortColumn?: string
9
+ sortDirection: SortDirection
10
+ onSort?: (column: string) => void
11
+ selectedRows: Set<string>
12
+ onSelectRow?: (id: string, selected: boolean) => void
13
+ onSelectAll?: (selected: boolean) => void
14
+ selectable: boolean
15
+ allRowIds: string[]
16
+ }
17
+
18
+ const TableContext = createContext<TableContextType>({
19
+ sortDirection: null,
20
+ selectedRows: new Set(),
21
+ selectable: false,
22
+ allRowIds: [],
23
+ })
24
+
25
+ export interface TableProps extends ViewProps {
26
+ /** Currently sorted column */
27
+ sortColumn?: string
28
+ /** Sort direction */
29
+ sortDirection?: SortDirection
30
+ /** Callback when sort changes */
31
+ onSort?: (column: string) => void
32
+ /** Selected row IDs (controlled) */
33
+ selectedRows?: Set<string>
34
+ /** Callback when row selection changes */
35
+ onSelectRow?: (id: string, selected: boolean) => void
36
+ /** Callback when select all changes */
37
+ onSelectAll?: (selected: boolean) => void
38
+ /** Enable row selection */
39
+ selectable?: boolean
40
+ /** All row IDs for select all functionality */
41
+ rowIds?: string[]
42
+ /** Whether the table is loading */
43
+ isLoading?: boolean
44
+ /** Number of skeleton rows to show when loading */
45
+ loadingRowCount?: number
46
+ /** Additional className */
47
+ className?: string
48
+ children?: React.ReactNode
49
+ }
50
+
51
+ /**
52
+ * Table component with sorting support.
53
+ *
54
+ * @example
55
+ * <Table sortColumn={sortCol} sortDirection={sortDir} onSort={handleSort}>
56
+ * <TableHeader>
57
+ * <TableRow>
58
+ * <TableHeaderCell sortKey="name">Name</TableHeaderCell>
59
+ * <TableHeaderCell sortKey="email">Email</TableHeaderCell>
60
+ * <TableHeaderCell sortKey="status">Status</TableHeaderCell>
61
+ * </TableRow>
62
+ * </TableHeader>
63
+ * <TableBody>
64
+ * {data.map(row => (
65
+ * <TableRow key={row.id}>
66
+ * <TableCell>{row.name}</TableCell>
67
+ * <TableCell>{row.email}</TableCell>
68
+ * <TableCell>{row.status}</TableCell>
69
+ * </TableRow>
70
+ * ))}
71
+ * </TableBody>
72
+ * </Table>
73
+ */
74
+ export function Table({
75
+ sortColumn,
76
+ sortDirection = null,
77
+ onSort,
78
+ selectedRows = new Set(),
79
+ onSelectRow,
80
+ onSelectAll,
81
+ selectable = false,
82
+ rowIds = [],
83
+ isLoading = false,
84
+ loadingRowCount = 5,
85
+ className,
86
+ children,
87
+ ...props
88
+ }: TableProps) {
89
+ return (
90
+ <TableContext.Provider
91
+ value={{
92
+ sortColumn,
93
+ sortDirection,
94
+ onSort,
95
+ selectedRows,
96
+ onSelectRow,
97
+ onSelectAll,
98
+ selectable,
99
+ allRowIds: rowIds,
100
+ }}
101
+ >
102
+ <View className={cn('w-full', className)} {...props}>
103
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
104
+ <View className="w-full min-w-full">
105
+ {isLoading ? (
106
+ <TableLoadingSkeleton rowCount={loadingRowCount} />
107
+ ) : (
108
+ children
109
+ )}
110
+ </View>
111
+ </ScrollView>
112
+ </View>
113
+ </TableContext.Provider>
114
+ )
115
+ }
116
+
117
+ export interface TableHeaderProps {
118
+ children?: React.ReactNode
119
+ className?: string
120
+ }
121
+
122
+ /**
123
+ * Table header container.
124
+ */
125
+ export function TableHeader({ children, className }: TableHeaderProps) {
126
+ return (
127
+ <View
128
+ accessibilityRole="none"
129
+ className={cn('bg-background-subtle rounded-t-lg', className)}
130
+ >
131
+ {children}
132
+ </View>
133
+ )
134
+ }
135
+
136
+ export interface TableBodyProps {
137
+ children?: React.ReactNode
138
+ className?: string
139
+ }
140
+
141
+ /**
142
+ * Table body container.
143
+ */
144
+ export function TableBody({ children, className }: TableBodyProps) {
145
+ return (
146
+ <View accessibilityRole="none" className={className}>
147
+ {children}
148
+ </View>
149
+ )
150
+ }
151
+
152
+ export interface TableRowProps extends ViewProps {
153
+ /** Whether the row is selected */
154
+ isSelected?: boolean
155
+ /** Whether the row is hoverable */
156
+ isHoverable?: boolean
157
+ /** Additional className */
158
+ className?: string
159
+ children?: React.ReactNode
160
+ }
161
+
162
+ /**
163
+ * Table row.
164
+ */
165
+ export function TableRow({
166
+ isSelected = false,
167
+ isHoverable = true,
168
+ className,
169
+ children,
170
+ ...props
171
+ }: TableRowProps) {
172
+ return (
173
+ <View
174
+ accessibilityRole="none"
175
+ className={cn(
176
+ 'flex-row border-b border-divider',
177
+ isHoverable && 'web:hover:bg-interactive-hover',
178
+ isSelected && 'bg-interactive-selected',
179
+ className
180
+ )}
181
+ {...props}
182
+ >
183
+ {children}
184
+ </View>
185
+ )
186
+ }
187
+
188
+ export interface TableHeaderCellProps extends Omit<PressableProps, 'children'> {
189
+ /** Sort key for this column */
190
+ sortKey?: string
191
+ /** Text alignment */
192
+ align?: 'left' | 'center' | 'right'
193
+ /** Cell width in pixels */
194
+ width?: number
195
+ /** Additional className */
196
+ className?: string
197
+ children?: React.ReactNode
198
+ }
199
+
200
+ /**
201
+ * Table header cell with optional sorting.
202
+ */
203
+ export function TableHeaderCell({
204
+ sortKey,
205
+ align = 'left',
206
+ width,
207
+ className,
208
+ children,
209
+ onPress,
210
+ ...props
211
+ }: TableHeaderCellProps) {
212
+ const { sortColumn, sortDirection, onSort } = useContext(TableContext)
213
+ const isSorted = sortKey && sortColumn === sortKey
214
+ const isSortable = !!sortKey && !!onSort
215
+
216
+ const handlePress = (e: any) => {
217
+ if (isSortable && sortKey) {
218
+ onSort(sortKey)
219
+ }
220
+ onPress?.(e)
221
+ }
222
+
223
+ const alignStyles = {
224
+ left: 'justify-start',
225
+ center: 'justify-center',
226
+ right: 'justify-end',
227
+ }
228
+
229
+ const content = (
230
+ <>
231
+ <Text
232
+ className={cn(
233
+ 'text-xs font-semibold uppercase tracking-wider text-text-secondary',
234
+ isSorted && 'text-text-primary'
235
+ )}
236
+ >
237
+ {children}
238
+ </Text>
239
+ {isSortable && (
240
+ <Text className={cn('ml-1 text-xs', isSorted ? 'text-text-primary' : 'text-text-tertiary')}>
241
+ {isSorted ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}
242
+ </Text>
243
+ )}
244
+ </>
245
+ )
246
+
247
+ if (isSortable) {
248
+ return (
249
+ <Pressable
250
+ accessibilityRole="button"
251
+ accessibilityLabel={`Sort by ${children}`}
252
+ onPress={handlePress}
253
+ style={width ? { width } : { flex: 1 }}
254
+ className={cn(
255
+ 'flex-row items-center px-4 py-3',
256
+ alignStyles[align],
257
+ 'web:hover:bg-interactive-hover active:bg-interactive-active',
258
+ className
259
+ )}
260
+ {...props}
261
+ >
262
+ {content}
263
+ </Pressable>
264
+ )
265
+ }
266
+
267
+ return (
268
+ <View
269
+ style={width ? { width } : { flex: 1 }}
270
+ className={cn(
271
+ 'flex-row items-center px-4 py-3',
272
+ alignStyles[align],
273
+ className
274
+ )}
275
+ >
276
+ {content}
277
+ </View>
278
+ )
279
+ }
280
+
281
+ export interface TableCellProps extends ViewProps {
282
+ /** Text alignment */
283
+ align?: 'left' | 'center' | 'right'
284
+ /** Cell width in pixels */
285
+ width?: number
286
+ /** Additional className */
287
+ className?: string
288
+ children?: React.ReactNode
289
+ }
290
+
291
+ /**
292
+ * Table data cell.
293
+ */
294
+ export function TableCell({
295
+ align = 'left',
296
+ width,
297
+ className,
298
+ children,
299
+ ...props
300
+ }: TableCellProps) {
301
+ const alignStyles = {
302
+ left: 'items-start',
303
+ center: 'items-center',
304
+ right: 'items-end',
305
+ }
306
+
307
+ return (
308
+ <View
309
+ style={width ? { width } : { flex: 1 }}
310
+ className={cn(
311
+ 'justify-center px-4 py-3.5',
312
+ alignStyles[align],
313
+ className
314
+ )}
315
+ {...props}
316
+ >
317
+ {typeof children === 'string' ? (
318
+ <Text className="text-sm text-text-primary">{children}</Text>
319
+ ) : (
320
+ children
321
+ )}
322
+ </View>
323
+ )
324
+ }
325
+
326
+ // Pagination component
327
+
328
+ export interface TablePaginationProps {
329
+ /** Current page (0-indexed) */
330
+ page: number
331
+ /** Items per page */
332
+ pageSize: number
333
+ /** Total number of items */
334
+ totalItems: number
335
+ /** Available page size options */
336
+ pageSizeOptions?: number[]
337
+ /** Callback when page changes */
338
+ onPageChange: (page: number) => void
339
+ /** Callback when page size changes */
340
+ onPageSizeChange?: (pageSize: number) => void
341
+ /** Additional className */
342
+ className?: string
343
+ }
344
+
345
+ /**
346
+ * Pagination controls for table.
347
+ *
348
+ * @example
349
+ * <TablePagination
350
+ * page={page}
351
+ * pageSize={pageSize}
352
+ * totalItems={100}
353
+ * onPageChange={setPage}
354
+ * onPageSizeChange={setPageSize}
355
+ * />
356
+ */
357
+ export function TablePagination({
358
+ page,
359
+ pageSize,
360
+ totalItems,
361
+ pageSizeOptions = [10, 25, 50, 100],
362
+ onPageChange,
363
+ onPageSizeChange,
364
+ className,
365
+ }: TablePaginationProps) {
366
+ const totalPages = Math.ceil(totalItems / pageSize)
367
+ const startItem = page * pageSize + 1
368
+ const endItem = Math.min((page + 1) * pageSize, totalItems)
369
+
370
+ const canGoPrevious = page > 0
371
+ const canGoNext = page < totalPages - 1
372
+
373
+ return (
374
+ <View
375
+ className={cn(
376
+ 'flex-row items-center justify-between px-4 py-3 border-t border-divider',
377
+ className
378
+ )}
379
+ >
380
+ <View className="flex-row items-center gap-2">
381
+ <Text className="text-sm text-text-secondary">
382
+ Rows per page:
383
+ </Text>
384
+ <View className="flex-row gap-1">
385
+ {pageSizeOptions.map((size) => (
386
+ <Pressable
387
+ key={size}
388
+ onPress={() => onPageSizeChange?.(size)}
389
+ className={cn(
390
+ 'px-2 py-1 rounded',
391
+ pageSize === size
392
+ ? 'bg-brand-primary-subtle text-brand-primary'
393
+ : 'text-text-secondary web:hover:bg-interactive-hover'
394
+ )}
395
+ >
396
+ <Text
397
+ className={cn(
398
+ 'text-sm font-medium',
399
+ pageSize === size ? 'text-brand-primary' : 'text-text-secondary'
400
+ )}
401
+ >
402
+ {size}
403
+ </Text>
404
+ </Pressable>
405
+ ))}
406
+ </View>
407
+ </View>
408
+
409
+ <View className="flex-row items-center gap-4">
410
+ <Text className="text-sm text-text-secondary">
411
+ {startItem}-{endItem} of {totalItems}
412
+ </Text>
413
+
414
+ <View className="flex-row gap-1">
415
+ <Pressable
416
+ disabled={!canGoPrevious}
417
+ onPress={() => onPageChange(page - 1)}
418
+ accessibilityLabel="Previous page"
419
+ className={cn(
420
+ 'p-2 rounded',
421
+ canGoPrevious
422
+ ? 'web:hover:bg-interactive-hover active:bg-interactive-active'
423
+ : 'opacity-50'
424
+ )}
425
+ >
426
+ <Text className="text-text-secondary">←</Text>
427
+ </Pressable>
428
+
429
+ <Pressable
430
+ disabled={!canGoNext}
431
+ onPress={() => onPageChange(page + 1)}
432
+ accessibilityLabel="Next page"
433
+ className={cn(
434
+ 'p-2 rounded',
435
+ canGoNext
436
+ ? 'web:hover:bg-interactive-hover active:bg-interactive-active'
437
+ : 'opacity-50'
438
+ )}
439
+ >
440
+ <Text className="text-text-secondary">→</Text>
441
+ </Pressable>
442
+ </View>
443
+ </View>
444
+ </View>
445
+ )
446
+ }
447
+
448
+ // Helper hook for managing table state
449
+
450
+ export interface UseTableOptions<T> {
451
+ data: T[]
452
+ defaultPageSize?: number
453
+ defaultSortColumn?: string
454
+ defaultSortDirection?: SortDirection
455
+ }
456
+
457
+ export interface UseTableReturn<T> {
458
+ sortedData: T[]
459
+ paginatedData: T[]
460
+ page: number
461
+ pageSize: number
462
+ totalItems: number
463
+ sortColumn?: string
464
+ sortDirection: SortDirection
465
+ setPage: (page: number) => void
466
+ setPageSize: (size: number) => void
467
+ handleSort: (column: string) => void
468
+ }
469
+
470
+ /**
471
+ * Hook for managing table sorting and pagination state.
472
+ *
473
+ * @example
474
+ * const {
475
+ * paginatedData,
476
+ * page, pageSize, totalItems,
477
+ * sortColumn, sortDirection,
478
+ * setPage, setPageSize, handleSort
479
+ * } = useTable({
480
+ * data: users,
481
+ * defaultPageSize: 10,
482
+ * })
483
+ */
484
+ export function useTable<T extends Record<string, any>>({
485
+ data,
486
+ defaultPageSize = 10,
487
+ defaultSortColumn,
488
+ defaultSortDirection = null,
489
+ }: UseTableOptions<T>): UseTableReturn<T> {
490
+ const [page, setPage] = useState(0)
491
+ const [pageSize, setPageSize] = useState(defaultPageSize)
492
+ const [sortColumn, setSortColumn] = useState<string | undefined>(defaultSortColumn)
493
+ const [sortDirection, setSortDirection] = useState<SortDirection>(defaultSortDirection)
494
+
495
+ const sortedData = useMemo(() => {
496
+ if (!sortColumn || !sortDirection) return data
497
+
498
+ return [...data].sort((a, b) => {
499
+ const aVal = a[sortColumn]
500
+ const bVal = b[sortColumn]
501
+
502
+ if (aVal === bVal) return 0
503
+ if (aVal === null || aVal === undefined) return 1
504
+ if (bVal === null || bVal === undefined) return -1
505
+
506
+ const comparison = aVal < bVal ? -1 : 1
507
+ return sortDirection === 'asc' ? comparison : -comparison
508
+ })
509
+ }, [data, sortColumn, sortDirection])
510
+
511
+ const paginatedData = useMemo(() => {
512
+ const start = page * pageSize
513
+ return sortedData.slice(start, start + pageSize)
514
+ }, [sortedData, page, pageSize])
515
+
516
+ const handleSort = (column: string) => {
517
+ if (sortColumn === column) {
518
+ // Cycle: asc -> desc -> null
519
+ setSortDirection((prev) =>
520
+ prev === 'asc' ? 'desc' : prev === 'desc' ? null : 'asc'
521
+ )
522
+ } else {
523
+ setSortColumn(column)
524
+ setSortDirection('asc')
525
+ }
526
+ setPage(0) // Reset to first page on sort change
527
+ }
528
+
529
+ const handlePageSizeChange = (newSize: number) => {
530
+ setPageSize(newSize)
531
+ setPage(0) // Reset to first page on page size change
532
+ }
533
+
534
+ return {
535
+ sortedData,
536
+ paginatedData,
537
+ page,
538
+ pageSize,
539
+ totalItems: data.length,
540
+ sortColumn,
541
+ sortDirection,
542
+ setPage,
543
+ setPageSize: handlePageSizeChange,
544
+ handleSort,
545
+ }
546
+ }
547
+
548
+ // Selection components
549
+
550
+ export interface TableSelectAllCellProps {
551
+ className?: string
552
+ }
553
+
554
+ /**
555
+ * Checkbox cell for selecting all rows in header.
556
+ */
557
+ export function TableSelectAllCell({ className }: TableSelectAllCellProps) {
558
+ const { selectedRows, onSelectAll, allRowIds, selectable } = useContext(TableContext)
559
+
560
+ if (!selectable) return null
561
+
562
+ const allSelected = allRowIds.length > 0 && allRowIds.every((id) => selectedRows.has(id))
563
+ const someSelected = allRowIds.some((id) => selectedRows.has(id)) && !allSelected
564
+
565
+ return (
566
+ <View className={cn('w-12 px-4 py-3 items-center justify-center', className)}>
567
+ <Pressable
568
+ accessibilityRole="checkbox"
569
+ accessibilityState={{ checked: allSelected ? true : someSelected ? 'mixed' : false }}
570
+ onPress={() => onSelectAll?.(!allSelected)}
571
+ className={cn(
572
+ 'w-5 h-5 rounded border-2 items-center justify-center',
573
+ allSelected
574
+ ? 'bg-brand-primary border-brand-primary'
575
+ : someSelected
576
+ ? 'bg-brand-primary-subtle border-brand-primary'
577
+ : 'border-border-strong bg-transparent'
578
+ )}
579
+ >
580
+ {(allSelected || someSelected) && (
581
+ <Text className="text-white text-xs font-bold">
582
+ {allSelected ? '✓' : '−'}
583
+ </Text>
584
+ )}
585
+ </Pressable>
586
+ </View>
587
+ )
588
+ }
589
+
590
+ export interface TableSelectCellProps {
591
+ /** Row ID for selection */
592
+ rowId: string
593
+ className?: string
594
+ }
595
+
596
+ /**
597
+ * Checkbox cell for selecting individual rows.
598
+ */
599
+ export function TableSelectCell({ rowId, className }: TableSelectCellProps) {
600
+ const { selectedRows, onSelectRow, selectable } = useContext(TableContext)
601
+
602
+ if (!selectable) return null
603
+
604
+ const isSelected = selectedRows.has(rowId)
605
+
606
+ return (
607
+ <View className={cn('w-12 px-4 py-3.5 items-center justify-center', className)}>
608
+ <Pressable
609
+ accessibilityRole="checkbox"
610
+ accessibilityState={{ checked: isSelected }}
611
+ onPress={() => onSelectRow?.(rowId, !isSelected)}
612
+ className={cn(
613
+ 'w-5 h-5 rounded border-2 items-center justify-center',
614
+ isSelected
615
+ ? 'bg-brand-primary border-brand-primary'
616
+ : 'border-border-strong bg-transparent web:hover:border-brand-primary'
617
+ )}
618
+ >
619
+ {isSelected && (
620
+ <Text className="text-white text-xs font-bold">✓</Text>
621
+ )}
622
+ </Pressable>
623
+ </View>
624
+ )
625
+ }
626
+
627
+ // Loading skeleton
628
+
629
+ interface TableLoadingSkeletonProps {
630
+ rowCount: number
631
+ }
632
+
633
+ function TableLoadingSkeleton({ rowCount }: TableLoadingSkeletonProps) {
634
+ return (
635
+ <>
636
+ <TableHeader>
637
+ <TableRow isHoverable={false}>
638
+ {[1, 2, 3, 4].map((i) => (
639
+ <View key={i} className="flex-1 px-4 py-3">
640
+ <View className="h-4 w-20 bg-interactive-disabled rounded animate-pulse" />
641
+ </View>
642
+ ))}
643
+ </TableRow>
644
+ </TableHeader>
645
+ <TableBody>
646
+ {Array.from({ length: rowCount }).map((_, rowIndex) => (
647
+ <TableRow key={rowIndex} isHoverable={false}>
648
+ {[1, 2, 3, 4].map((colIndex) => (
649
+ <View key={colIndex} className="flex-1 px-4 py-3.5">
650
+ <View
651
+ className="h-4 bg-interactive-disabled rounded animate-pulse"
652
+ style={{ width: `${50 + Math.random() * 40}%` }}
653
+ />
654
+ </View>
655
+ ))}
656
+ </TableRow>
657
+ ))}
658
+ </TableBody>
659
+ </>
660
+ )
661
+ }
662
+
663
+ // Empty state
664
+
665
+ export interface TableEmptyStateProps {
666
+ /** Title for empty state */
667
+ title?: string
668
+ /** Description for empty state */
669
+ description?: string
670
+ /** Action button/element */
671
+ action?: React.ReactNode
672
+ /** Custom icon */
673
+ icon?: React.ReactNode
674
+ /** Additional className */
675
+ className?: string
676
+ }
677
+
678
+ /**
679
+ * Empty state component for tables with no data.
680
+ */
681
+ export function TableEmptyState({
682
+ title = 'No data',
683
+ description = 'There are no items to display.',
684
+ action,
685
+ icon,
686
+ className,
687
+ }: TableEmptyStateProps) {
688
+ return (
689
+ <View className={cn('items-center justify-center py-12 px-4', className)}>
690
+ {icon && <View className="mb-4">{icon}</View>}
691
+ <Text className="text-lg font-semibold text-text-primary mb-1">{title}</Text>
692
+ <Text className="text-sm text-text-secondary text-center mb-4">{description}</Text>
693
+ {action}
694
+ </View>
695
+ )
696
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ Table,
3
+ TableHeader,
4
+ TableBody,
5
+ TableRow,
6
+ TableHeaderCell,
7
+ TableCell,
8
+ TablePagination,
9
+ useTable,
10
+ } from './Table'
11
+ export type {
12
+ TableProps,
13
+ TableHeaderProps,
14
+ TableBodyProps,
15
+ TableRowProps,
16
+ TableHeaderCellProps,
17
+ TableCellProps,
18
+ TablePaginationProps,
19
+ UseTableOptions,
20
+ UseTableReturn,
21
+ SortDirection,
22
+ } from './Table'