@titan-design/react-ui 0.0.2 → 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.
- package/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
- package/dist/chunk-MGW37MPO.mjs.map +1 -0
- package/dist/index.d.mts +115 -2
- package/dist/index.d.ts +115 -2
- package/dist/index.js +262 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -3
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +124 -109
- package/dist/theme/index.d.ts +124 -109
- package/dist/theme/index.js +24 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +12 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/index.ts +8 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +242 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +265 -0
- package/src/components/ui/card/Card.tsx +380 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +36 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +180 -0
- package/src/components/ui/select/Select.tsx +236 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/index.ts +39 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
- package/dist/chunk-NA3EES23.mjs.map +0 -1
|
@@ -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'
|