@himakarinv/nativex 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/images.d.ts +14 -0
  4. package/index.js +8 -0
  5. package/nativewind-preset.js +18 -0
  6. package/package.json +120 -0
  7. package/src/assets/branding/logo.jpg +0 -0
  8. package/src/assets/branding/softech-logo.png +0 -0
  9. package/src/assets/icons/2-layers.svg +3 -0
  10. package/src/assets/icons/Forbidden.svg +211 -0
  11. package/src/assets/icons/Internal-server-error.svg +119 -0
  12. package/src/assets/icons/Log-out-nav.svg +4 -0
  13. package/src/assets/icons/Not-Found.svg +282 -0
  14. package/src/assets/icons/Unauthorized.svg +314 -0
  15. package/src/assets/icons/User-Frame.svg +5 -0
  16. package/src/assets/icons/alert.svg +4 -0
  17. package/src/assets/icons/arrow-right.svg +3 -0
  18. package/src/assets/icons/avatar-female.svg +10 -0
  19. package/src/assets/icons/avatar-male.svg +10 -0
  20. package/src/assets/icons/bell.svg +3 -0
  21. package/src/assets/icons/calendar-event.svg +11 -0
  22. package/src/assets/icons/calendar.svg +6 -0
  23. package/src/assets/icons/card.svg +4 -0
  24. package/src/assets/icons/caret-right.svg +3 -0
  25. package/src/assets/icons/chat.svg +11 -0
  26. package/src/assets/icons/check.svg +3 -0
  27. package/src/assets/icons/checkbox-minus.svg +3 -0
  28. package/src/assets/icons/checkmark.svg +3 -0
  29. package/src/assets/icons/chevron-down.svg +3 -0
  30. package/src/assets/icons/chevron-left.svg +9 -0
  31. package/src/assets/icons/chevron-right.svg +9 -0
  32. package/src/assets/icons/circle-radio-active.svg +10 -0
  33. package/src/assets/icons/circle-radio.svg +10 -0
  34. package/src/assets/icons/clock.svg +3 -0
  35. package/src/assets/icons/close.svg +3 -0
  36. package/src/assets/icons/code.svg +3 -0
  37. package/src/assets/icons/country.svg +20 -0
  38. package/src/assets/icons/dot.svg +3 -0
  39. package/src/assets/icons/download.svg +1 -0
  40. package/src/assets/icons/dropdown-arrow.svg +3 -0
  41. package/src/assets/icons/dropdown-avatar.svg +3 -0
  42. package/src/assets/icons/email.svg +3 -0
  43. package/src/assets/icons/error.svg +3 -0
  44. package/src/assets/icons/eye-off.svg +1 -0
  45. package/src/assets/icons/eye.svg +1 -0
  46. package/src/assets/icons/hashtag-icon.svg +3 -0
  47. package/src/assets/icons/help-circle.svg +10 -0
  48. package/src/assets/icons/help.svg +4 -0
  49. package/src/assets/icons/hide-checkbox.svg +3 -0
  50. package/src/assets/icons/home-smile.svg +3 -0
  51. package/src/assets/icons/home.svg +3 -0
  52. package/src/assets/icons/info.svg +5 -0
  53. package/src/assets/icons/left-arrow.svg +3 -0
  54. package/src/assets/icons/log-out.svg +3 -0
  55. package/src/assets/icons/notification-box.svg +4 -0
  56. package/src/assets/icons/pencil.svg +1 -0
  57. package/src/assets/icons/plus.svg +3 -0
  58. package/src/assets/icons/radio-dot.svg +3 -0
  59. package/src/assets/icons/right-arrow.svg +3 -0
  60. package/src/assets/icons/search.svg +3 -0
  61. package/src/assets/icons/settings.svg +11 -0
  62. package/src/assets/icons/slack.svg +17 -0
  63. package/src/assets/icons/slash.svg +3 -0
  64. package/src/assets/icons/success.svg +3 -0
  65. package/src/assets/icons/successicon.svg +5 -0
  66. package/src/assets/icons/three-dot-vertical.svg +5 -0
  67. package/src/assets/icons/trash.svg +1 -0
  68. package/src/assets/icons/trend-down-01.svg +3 -0
  69. package/src/assets/icons/trend-up-01.svg +3 -0
  70. package/src/assets/icons/up-arrow.svg +3 -0
  71. package/src/assets/icons/user-plus.svg +10 -0
  72. package/src/assets/icons/user.svg +3 -0
  73. package/src/assets/icons/users.svg +3 -0
  74. package/src/assets/icons/vertical-dots-true-icon.svg +5 -0
  75. package/src/assets/icons/vertical-line.svg +3 -0
  76. package/src/assets/icons/vertical-three-dots-icon.svg +5 -0
  77. package/src/assets/icons/warning.svg +3 -0
  78. package/src/assets/icons/zap.svg +3 -0
  79. package/src/components/ActionsMenu/ActionsMenu.tsx +314 -0
  80. package/src/components/ActionsMenu/index.ts +2 -0
  81. package/src/components/Alert/Alert.tsx +291 -0
  82. package/src/components/Alert/index.ts +2 -0
  83. package/src/components/Badge/Badge.tsx +230 -0
  84. package/src/components/Badge/index.ts +2 -0
  85. package/src/components/Breadcrumb/Breadcrumb.tsx +364 -0
  86. package/src/components/Breadcrumb/index.ts +7 -0
  87. package/src/components/Button/Button.tsx +584 -0
  88. package/src/components/Button/index.ts +9 -0
  89. package/src/components/Checkbox/Checkbox.tsx +336 -0
  90. package/src/components/Checkbox/index.ts +2 -0
  91. package/src/components/CreatePassword/CreatePassword.tsx +161 -0
  92. package/src/components/CreatePassword/index.ts +2 -0
  93. package/src/components/DataTable/DataTable.tsx +822 -0
  94. package/src/components/DataTable/index.ts +10 -0
  95. package/src/components/Datepicker/Datepicker.tsx +797 -0
  96. package/src/components/Datepicker/index.ts +2 -0
  97. package/src/components/Dropdown/Dropdown.tsx +277 -0
  98. package/src/components/Dropdown/index.ts +7 -0
  99. package/src/components/DropdownMenu/DropdownMenu.tsx +471 -0
  100. package/src/components/DropdownMenu/index.ts +11 -0
  101. package/src/components/DynamicForm/DynamicForm.tsx +741 -0
  102. package/src/components/DynamicForm/index.ts +13 -0
  103. package/src/components/ErrorPages/ErrorPages.tsx +135 -0
  104. package/src/components/ErrorPages/index.ts +2 -0
  105. package/src/components/ForgotPassword/ForgotPassword.tsx +121 -0
  106. package/src/components/ForgotPassword/index.ts +2 -0
  107. package/src/components/HintError/HintError.tsx +37 -0
  108. package/src/components/HintError/index.ts +2 -0
  109. package/src/components/Icon/Icon.tsx +43 -0
  110. package/src/components/Icon/icon-registry.ts +227 -0
  111. package/src/components/Icon/index.ts +3 -0
  112. package/src/components/InputDropdown/InputDropdown.tsx +662 -0
  113. package/src/components/InputDropdown/index.ts +9 -0
  114. package/src/components/InputField/InputField.tsx +527 -0
  115. package/src/components/InputField/index.ts +2 -0
  116. package/src/components/Label/Label.tsx +88 -0
  117. package/src/components/Label/index.ts +2 -0
  118. package/src/components/LoginForm/LoginForm.tsx +158 -0
  119. package/src/components/LoginForm/index.ts +2 -0
  120. package/src/components/LoginShell/LoginShell.tsx +97 -0
  121. package/src/components/LoginShell/RoleCard.tsx +24 -0
  122. package/src/components/LoginShell/SoftechLogo.tsx +24 -0
  123. package/src/components/LoginShell/branding-assets.ts +2 -0
  124. package/src/components/LoginShell/index.ts +20 -0
  125. package/src/components/LoginShell/login-copy.ts +45 -0
  126. package/src/components/LoginShell/loginPageStyles.ts +210 -0
  127. package/src/components/Modals/Modals.tsx +313 -0
  128. package/src/components/Modals/index.ts +2 -0
  129. package/src/components/Offcanvas/Offcanvas.tsx +404 -0
  130. package/src/components/Offcanvas/index.ts +13 -0
  131. package/src/components/PageHeader/PageHeader.tsx +128 -0
  132. package/src/components/PageHeader/index.ts +2 -0
  133. package/src/components/Pagination/Pagination.tsx +540 -0
  134. package/src/components/Pagination/index.ts +8 -0
  135. package/src/components/ResetPassword/ResetPassword.tsx +172 -0
  136. package/src/components/ResetPassword/index.ts +2 -0
  137. package/src/components/RoleSelection/RoleSelection.tsx +111 -0
  138. package/src/components/RoleSelection/index.ts +2 -0
  139. package/src/components/SendOtp/SendOtp.tsx +149 -0
  140. package/src/components/SendOtp/index.ts +2 -0
  141. package/src/components/Sidenavbar/Sidenavbar.tsx +358 -0
  142. package/src/components/Sidenavbar/index.ts +2 -0
  143. package/src/components/StatCard/StatCard.tsx +393 -0
  144. package/src/components/StatCard/index.ts +6 -0
  145. package/src/components/Tabs/Tabs.tsx +239 -0
  146. package/src/components/Tabs/index.ts +2 -0
  147. package/src/components/Textarea/Textarea.tsx +217 -0
  148. package/src/components/Textarea/index.ts +2 -0
  149. package/src/components/Timepicker/Timepicker.tsx +384 -0
  150. package/src/components/Timepicker/index.ts +2 -0
  151. package/src/components/Toast/Toast.tsx +278 -0
  152. package/src/components/Toast/index.ts +2 -0
  153. package/src/components/Toggle/Toggle.tsx +296 -0
  154. package/src/components/Toggle/index.ts +2 -0
  155. package/src/components/Typeahead/Typeahead.tsx +521 -0
  156. package/src/components/Typeahead/index.ts +2 -0
  157. package/src/components/Verification/Verification.tsx +374 -0
  158. package/src/components/Verification/index.ts +2 -0
  159. package/src/index.ts +39 -0
  160. package/src/theme/borders.ts +19 -0
  161. package/src/theme/button.ts +41 -0
  162. package/src/theme/colors.ts +273 -0
  163. package/src/theme/index.ts +14 -0
  164. package/src/theme/nativewind-theme.js +182 -0
  165. package/src/theme/radius.ts +12 -0
  166. package/src/theme/shadows.ts +40 -0
  167. package/src/theme/spacing.ts +18 -0
  168. package/src/theme/typography.ts +35 -0
  169. package/src/utils/a11y.ts +21 -0
  170. package/svg.d.ts +7 -0
@@ -0,0 +1,822 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ Pressable,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewStyle,
10
+ } from 'react-native';
11
+
12
+ import { borders, colors, radius, spacing, typography } from '../../theme';
13
+ import { ActionsMenu, type ActionMenuItem } from '../ActionsMenu';
14
+ import { Badge, BadgeColor, BadgeSize } from '../Badge';
15
+ import { Button, ButtonSize, ButtonVariant, IconPosition } from '../Button';
16
+ import { Checkbox, CheckboxSize } from '../Checkbox';
17
+ import { Icon, IconName, isIconName } from '../Icon';
18
+ import { InputDropdown, InputDropdownSize, type InputDropdownItem } from '../InputDropdown';
19
+ import { InputField } from '../InputField';
20
+ import {
21
+ Pagination,
22
+ PaginationVariant,
23
+ SimpleLayout,
24
+ PageNumberShape,
25
+ } from '../Pagination';
26
+ import { Timepicker, TimepickerSize, type TimepickerTime } from '../Timepicker';
27
+
28
+ export type DataTableRow = Record<string, unknown> & { id?: string };
29
+
30
+ export type DataTableRowAction = {
31
+ id: string;
32
+ label: string;
33
+ icon?: string;
34
+ variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
35
+ dividerBefore?: boolean;
36
+ };
37
+
38
+ export type DataTableCellEditorKind = 'text' | 'dropdown' | 'time';
39
+
40
+ export type DataTableColumnCellEditorMeta = {
41
+ showHeaderHelpIcon?: boolean;
42
+ headerHelpIconAlt?: string;
43
+ cellEditor?: DataTableCellEditorKind;
44
+ cellEditorField?: string;
45
+ cellEditorPlaceholder?: string;
46
+ cellEditorDropdownOptions?: InputDropdownItem[];
47
+ cellEditorDropdownLabel?: string;
48
+ cellEditorDropdownSuffix?: boolean;
49
+ cellEditorTimeLabel?: string;
50
+ cellEditorTimeMeridian?: boolean;
51
+ cellEditorTimeSize?: 'small' | 'medium' | 'large';
52
+ };
53
+
54
+ export type DataTableColumn = {
55
+ id: string;
56
+ header?: string;
57
+ accessor?: string;
58
+ enableSorting?: boolean;
59
+ meta?: DataTableColumnCellEditorMeta;
60
+ };
61
+
62
+ export type DataTableCellValueChangeEvent = {
63
+ columnId: string;
64
+ row: DataTableRow;
65
+ field: string;
66
+ value: unknown;
67
+ };
68
+
69
+ export type DataTableProps = {
70
+ data: DataTableRow[];
71
+ columns: DataTableColumn[];
72
+ searchPlaceholder?: string;
73
+ searchInputId?: string;
74
+ searchFieldLabel?: string;
75
+ filterToolbarLabel?: string;
76
+ statusFilterGroupAriaLabel?: string;
77
+ statusFilterPlaceholder?: string;
78
+ statusFilterOptions?: InputDropdownItem[];
79
+ statusFilterAllValue?: string;
80
+ maxTeamTagsVisible?: number;
81
+ showToolbar?: boolean;
82
+ showPagination?: boolean;
83
+ pageSize?: number;
84
+ paginationMaxPages?: number;
85
+ selectAllRowsAriaLabel?: string;
86
+ actionsColumnHeaderLabel?: string;
87
+ selectRowAriaLabelPattern?: string;
88
+ rowActionsTriggerAriaLabel?: string;
89
+ rowActionsMenuAriaLabel?: string;
90
+ rowActions?: DataTableRowAction[];
91
+ rowActionsDisabled?: (row: DataTableRow) => boolean;
92
+ rowActionsVisibleLimit?: number | null;
93
+ viewColumnId?: string;
94
+ viewColumnHeaderLabel?: string;
95
+ viewColumnIcon?: string;
96
+ viewColumnAriaLabel?: string;
97
+ viewColumnActionId?: string;
98
+ personColumnId?: string;
99
+ personAvatarUrlField?: string;
100
+ personAvatarColorField?: string;
101
+ personNameField?: string;
102
+ personHandleField?: string;
103
+ personInitialsField?: string;
104
+ statusColumnId?: string;
105
+ statusValueField?: string;
106
+ teamsColumnId?: string;
107
+ teamsValueField?: string;
108
+ emptyMessage?: string;
109
+ onRowAction?: (event: { action: string; row: DataTableRow }) => void;
110
+ onSelectionChange?: (rows: DataTableRow[]) => void;
111
+ onCellValueChange?: (event: DataTableCellValueChangeEvent) => void;
112
+ style?: StyleProp<ViewStyle>;
113
+ };
114
+
115
+ type SortDir = 'asc' | 'desc';
116
+
117
+ const COL_WIDTH: Record<string, number> = {
118
+ select: spacing['7xl'],
119
+ name: spacing['7xl'] * 4,
120
+ status: spacing['7xl'] * 2,
121
+ role: spacing['7xl'] * 3 + spacing.lg,
122
+ email: spacing['7xl'] * 4 + spacing.lg,
123
+ teams: spacing['7xl'] * 3,
124
+ view: spacing['7xl'] + spacing.base,
125
+ shiftType: spacing['7xl'] * 3,
126
+ workStart: spacing['7xl'] * 4,
127
+ workEnd: spacing['7xl'] * 4,
128
+ extraHours: spacing['7xl'] * 2,
129
+ notes: spacing['7xl'] * 3,
130
+ };
131
+
132
+ function actionColumnWidth(count: number) {
133
+ const buttons = Math.max(1, count);
134
+ if (buttons === 1) {
135
+ return spacing['7xl'] + spacing.xl;
136
+ }
137
+ return buttons * spacing['5xl'] + (buttons - 1) * spacing.md + spacing.sm * 2;
138
+ }
139
+
140
+ function columnWidth(id: string, actionsWidth: number) {
141
+ if (id === 'actions') {
142
+ return actionsWidth;
143
+ }
144
+ return COL_WIDTH[id] ?? spacing['7xl'] * 3;
145
+ }
146
+
147
+ function rowId(row: DataTableRow, index: number) {
148
+ return String(row.id ?? index);
149
+ }
150
+
151
+ function fieldOf(row: DataTableRow, key: string) {
152
+ return row[key];
153
+ }
154
+
155
+ function cellValue(row: DataTableRow, column: DataTableColumn) {
156
+ return fieldOf(row, column.accessor ?? column.id);
157
+ }
158
+
159
+ function avatarIconName(value: unknown): string | null {
160
+ return typeof value === 'string' && isIconName(value) ? value : null;
161
+ }
162
+
163
+ function initials(row: DataTableRow, nameKey: string, initialsKey: string) {
164
+ const preset = fieldOf(row, initialsKey);
165
+ if (typeof preset === 'string' && preset.trim()) {
166
+ return preset.trim();
167
+ }
168
+ const parts = String(fieldOf(row, nameKey) ?? '')
169
+ .split(/\s+/)
170
+ .filter(Boolean);
171
+ if (parts.length >= 2) {
172
+ return `${parts[0]![0]}${parts[1]![0]}`.toUpperCase();
173
+ }
174
+ return (parts[0]?.slice(0, 2) ?? '?').toUpperCase();
175
+ }
176
+
177
+ function teamsLine(teams: unknown, max: number) {
178
+ const list = Array.isArray(teams) ? teams.map(String) : [];
179
+ if (list.length <= max) {
180
+ return list.join(', ');
181
+ }
182
+ return `${list.slice(0, max).join(', ')}, +${list.length - max}`;
183
+ }
184
+
185
+ function statusBadgeColor(status: unknown): BadgeColor {
186
+ const key = String(status ?? '')
187
+ .trim()
188
+ .toLowerCase();
189
+ if (key === 'active') {
190
+ return BadgeColor.Success;
191
+ }
192
+ if (key === 'inactive') {
193
+ return BadgeColor.Gray;
194
+ }
195
+ if (key === 'pending') {
196
+ return BadgeColor.Warning;
197
+ }
198
+ return BadgeColor.BlueGray;
199
+ }
200
+
201
+ function titleCase(value: unknown) {
202
+ const text = String(value ?? '');
203
+ return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
204
+ }
205
+
206
+ function matchesSearch(row: DataTableRow, query: string) {
207
+ const q = query.trim().toLowerCase();
208
+ if (!q) {
209
+ return true;
210
+ }
211
+ const parts: string[] = [];
212
+ const push = (value: unknown) => {
213
+ if (value == null) {
214
+ return;
215
+ }
216
+ if (Array.isArray(value)) {
217
+ value.forEach(push);
218
+ } else {
219
+ parts.push(String(value));
220
+ }
221
+ };
222
+ push(row.name);
223
+ push(row.handle);
224
+ push(row.email);
225
+ push(row.role);
226
+ push(row.status);
227
+ push(row.teams);
228
+ return parts.join(' ').toLowerCase().includes(q);
229
+ }
230
+
231
+ function parseTime(value: unknown): TimepickerTime | null {
232
+ if (value == null || typeof value !== 'object') {
233
+ return null;
234
+ }
235
+ const record = value as Record<string, unknown>;
236
+ const hour = Number(record.hour);
237
+ const minute = Number(record.minute);
238
+ if (!Number.isFinite(hour) || !Number.isFinite(minute)) {
239
+ return null;
240
+ }
241
+ const second = Number(record.second ?? 0);
242
+ return { hour, minute, second: Number.isFinite(second) ? second : 0 };
243
+ }
244
+
245
+ function visibleActions(actions: DataTableRowAction[], limit: number | null) {
246
+ if (limit != null) {
247
+ if (limit <= 0) {
248
+ return [];
249
+ }
250
+ const withIcons = actions.filter((action) => action.icon);
251
+ return (withIcons.length > 0 ? withIcons : actions).slice(0, limit);
252
+ }
253
+ const withIcons = actions.filter((action) => action.icon);
254
+ if (withIcons.length > 0) {
255
+ return withIcons.slice(0, actions.length > 3 ? 2 : 3);
256
+ }
257
+ return actions.slice(0, 1);
258
+ }
259
+
260
+ function overflowActions(actions: DataTableRowAction[], limit: number | null) {
261
+ const visibleIds = new Set(visibleActions(actions, limit).map((action) => action.id));
262
+ return actions.filter((action) => !visibleIds.has(action.id));
263
+ }
264
+
265
+ function editorField(column: DataTableColumn) {
266
+ return column.meta?.cellEditorField?.trim() || column.id;
267
+ }
268
+
269
+ export function DataTable({
270
+ data,
271
+ columns,
272
+ searchPlaceholder = '',
273
+ searchFieldLabel = '',
274
+ filterToolbarLabel = '',
275
+ statusFilterGroupAriaLabel = '',
276
+ statusFilterPlaceholder = '',
277
+ statusFilterOptions = [],
278
+ statusFilterAllValue = 'all',
279
+ maxTeamTagsVisible = 2,
280
+ showToolbar = true,
281
+ showPagination = true,
282
+ pageSize = 10,
283
+ paginationMaxPages = 5,
284
+ selectAllRowsAriaLabel = '',
285
+ actionsColumnHeaderLabel = '',
286
+ selectRowAriaLabelPattern = '',
287
+ rowActionsTriggerAriaLabel = '',
288
+ rowActionsMenuAriaLabel = '',
289
+ rowActions = [],
290
+ rowActionsDisabled,
291
+ rowActionsVisibleLimit = null,
292
+ viewColumnId = 'view',
293
+ viewColumnHeaderLabel = 'View',
294
+ viewColumnIcon = IconName.Eye,
295
+ viewColumnAriaLabel = 'View',
296
+ viewColumnActionId = 'view',
297
+ personColumnId = 'name',
298
+ personAvatarUrlField = 'avatarUrl',
299
+ personAvatarColorField = 'avatarColor',
300
+ personNameField = 'name',
301
+ personHandleField = 'handle',
302
+ personInitialsField = 'initials',
303
+ statusColumnId = 'status',
304
+ statusValueField = 'status',
305
+ teamsColumnId = 'teams',
306
+ teamsValueField = 'teams',
307
+ emptyMessage = 'No data available to display.',
308
+ onRowAction,
309
+ onSelectionChange,
310
+ onCellValueChange,
311
+ style,
312
+ }: DataTableProps) {
313
+ const [search, setSearch] = useState('');
314
+ const [statusFilter, setStatusFilter] = useState(statusFilterAllValue);
315
+ const [sort, setSort] = useState<{ id: string; dir: SortDir } | null>(null);
316
+ const [selected, setSelected] = useState<string[]>([]);
317
+ const [page, setPage] = useState(1);
318
+
319
+ useEffect(() => {
320
+ setPage(1);
321
+ }, [search, statusFilter, pageSize, data.length]);
322
+
323
+ const shownActions = visibleActions(rowActions, rowActionsVisibleLimit);
324
+ const menuActions = overflowActions(rowActions, rowActionsVisibleLimit);
325
+ const actionsWidth = actionColumnWidth(shownActions.length + (menuActions.length > 0 ? 1 : 0));
326
+
327
+ const filtered = useMemo(() => {
328
+ return data.filter((row) => {
329
+ if (!matchesSearch(row, search)) {
330
+ return false;
331
+ }
332
+ if (statusFilter && statusFilter !== statusFilterAllValue) {
333
+ return String(fieldOf(row, statusColumnId)) === statusFilter;
334
+ }
335
+ return true;
336
+ });
337
+ }, [data, search, statusFilter, statusFilterAllValue, statusColumnId]);
338
+
339
+ const sorted = useMemo(() => {
340
+ if (!sort) {
341
+ return filtered;
342
+ }
343
+ const column = columns.find((item) => item.id === sort.id);
344
+ const copy = [...filtered];
345
+ copy.sort((left, right) => {
346
+ const a = String(cellValue(left, column ?? { id: sort.id }) ?? '');
347
+ const b = String(cellValue(right, column ?? { id: sort.id }) ?? '');
348
+ const cmp = a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
349
+ return sort.dir === 'asc' ? cmp : -cmp;
350
+ });
351
+ return copy;
352
+ }, [filtered, sort, columns]);
353
+
354
+ const size = Math.max(1, pageSize);
355
+ const pageCount = Math.max(1, Math.ceil(sorted.length / size));
356
+ const safePage = Math.min(page, pageCount);
357
+ const paged = sorted.slice((safePage - 1) * size, safePage * size);
358
+ const allFilteredSelected = filtered.length > 0 && filtered.every((row, index) => selected.includes(rowId(row, index)));
359
+
360
+ function emitSelection(next: string[]) {
361
+ setSelected(next);
362
+ onSelectionChange?.(data.filter((row, index) => next.includes(rowId(row, index))));
363
+ }
364
+
365
+ function toggleSort(column: DataTableColumn) {
366
+ if (column.enableSorting === false || column.id === 'select' || column.id === 'actions' || column.id === viewColumnId) {
367
+ return;
368
+ }
369
+ setSort((current) => {
370
+ if (current?.id !== column.id) {
371
+ return { id: column.id, dir: 'asc' };
372
+ }
373
+ if (current.dir === 'asc') {
374
+ return { id: column.id, dir: 'desc' };
375
+ }
376
+ return null;
377
+ });
378
+ }
379
+
380
+ function selectAria(id: string) {
381
+ return selectRowAriaLabelPattern.trim() ? selectRowAriaLabelPattern.replace(/\{\{\s*id\s*\}\}/g, id) : `Select row ${id}`;
382
+ }
383
+
384
+ function isDisabled(row: DataTableRow) {
385
+ return rowActionsDisabled ? Boolean(rowActionsDisabled(row)) : false;
386
+ }
387
+
388
+ function menuItems(row: DataTableRow): ActionMenuItem<DataTableRow>[] {
389
+ return menuActions.map((action) => ({
390
+ label: action.label,
391
+ icon: action.icon,
392
+ variant: action.variant,
393
+ dividerBefore: action.dividerBefore,
394
+ action: (target) => onRowAction?.({ action: action.id, row: target }),
395
+ disabled: () => isDisabled(row),
396
+ }));
397
+ }
398
+
399
+ return (
400
+ <View style={[styles.root, style]}>
401
+ {showToolbar ? (
402
+ <View style={styles.toolbar}>
403
+ <View style={styles.searchField}>
404
+ <InputField
405
+ fullWidth
406
+ label={searchFieldLabel || 'Search table'}
407
+ placeholder={searchPlaceholder}
408
+ leftIconName={IconName.Search}
409
+ accessibilityLabel={searchFieldLabel || searchPlaceholder || 'Search table'}
410
+ value={search}
411
+ onValueChange={setSearch}
412
+ />
413
+ </View>
414
+ {filterToolbarLabel || statusFilterOptions.length > 0 ? (
415
+ <View style={styles.filterRow} accessibilityLabel={statusFilterGroupAriaLabel || undefined}>
416
+ {filterToolbarLabel ? <Text style={styles.filterLabel}>{filterToolbarLabel}</Text> : null}
417
+ {statusFilterOptions.length > 0 ? (
418
+ <View style={styles.filterField}>
419
+ <InputDropdown
420
+ label=""
421
+ suffix={false}
422
+ size={InputDropdownSize.Small}
423
+ options={statusFilterOptions}
424
+ placeholder={statusFilterPlaceholder}
425
+ selectedValue={statusFilter}
426
+ onSelectionChange={(item) => setStatusFilter(item?.value ?? statusFilterAllValue)}
427
+ />
428
+ </View>
429
+ ) : null}
430
+ </View>
431
+ ) : null}
432
+ </View>
433
+ ) : null}
434
+
435
+ <ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator>
436
+ <View style={styles.grid}>
437
+ <View style={styles.headerRow}>
438
+ {columns.map((column) => {
439
+ const sortable = column.enableSorting !== false && column.id !== 'select' && column.id !== 'actions' && column.id !== viewColumnId;
440
+ const dir = sort?.id === column.id ? sort.dir : null;
441
+ return (
442
+ <Pressable
443
+ key={column.id}
444
+ disabled={!sortable && column.id !== 'select'}
445
+ onPress={() => toggleSort(column)}
446
+ style={[
447
+ styles.th,
448
+ { width: columnWidth(column.id, actionsWidth) },
449
+ (column.id === 'actions' || column.id === viewColumnId) && styles.thCenter,
450
+ column.id === 'actions' && styles.thActions,
451
+ ]}>
452
+ {column.id === 'select' ? (
453
+ <Checkbox
454
+ size={CheckboxSize.Small}
455
+ checked={allFilteredSelected}
456
+ accessibilityLabel={selectAllRowsAriaLabel || 'Select all rows'}
457
+ onCheckedChange={(checked) =>
458
+ emitSelection(checked ? filtered.map((row, index) => rowId(row, index)) : [])
459
+ }
460
+ />
461
+ ) : (
462
+ <View style={styles.thInner}>
463
+ <Text style={styles.thLabel} numberOfLines={1}>
464
+ {column.id === 'actions'
465
+ ? actionsColumnHeaderLabel
466
+ : column.id === viewColumnId
467
+ ? viewColumnHeaderLabel
468
+ : column.header ?? ''}
469
+ </Text>
470
+ {column.meta?.showHeaderHelpIcon ? (
471
+ <Icon name={IconName.HelpCircle} size={spacing.base} color={colors.dataTable.muted} decorative />
472
+ ) : null}
473
+ {sortable ? (
474
+ <View style={dir === 'asc' ? styles.sortAsc : undefined}>
475
+ <Icon
476
+ name={IconName.ChevronDown}
477
+ size={14}
478
+ color={dir ? colors.dataTable.text : colors.dataTable.muted}
479
+ decorative
480
+ />
481
+ </View>
482
+ ) : null}
483
+ </View>
484
+ )}
485
+ </Pressable>
486
+ );
487
+ })}
488
+ </View>
489
+
490
+ {paged.length === 0 ? (
491
+ <View style={[styles.empty, { width: columns.reduce((sum, column) => sum + columnWidth(column.id, actionsWidth), 0) }]}>
492
+ <Text style={styles.emptyText}>{emptyMessage}</Text>
493
+ </View>
494
+ ) : (
495
+ paged.map((row, index) => {
496
+ const id = rowId(row, data.indexOf(row) === -1 ? index : data.indexOf(row));
497
+ const checked = selected.includes(id);
498
+ const avatarName = avatarIconName(fieldOf(row, personAvatarUrlField));
499
+ return (
500
+ <View key={id} style={[styles.bodyRow, index % 2 === 1 && styles.bodyRowStripe, checked && styles.bodyRowSelected]}>
501
+ {columns.map((column) => (
502
+ <View
503
+ key={`${id}-${column.id}`}
504
+ style={[
505
+ styles.td,
506
+ { width: columnWidth(column.id, actionsWidth) },
507
+ (column.id === 'actions' || column.id === viewColumnId) && styles.tdCenter,
508
+ column.id === 'actions' && styles.tdActions,
509
+ ]}>
510
+ {column.id === 'select' ? (
511
+ <Checkbox
512
+ size={CheckboxSize.Small}
513
+ checked={checked}
514
+ accessibilityLabel={selectAria(id)}
515
+ onCheckedChange={(next) =>
516
+ emitSelection(next ? [...selected, id] : selected.filter((item) => item !== id))
517
+ }
518
+ />
519
+ ) : column.meta?.cellEditor === 'text' ? (
520
+ <InputField
521
+ fullWidth
522
+ label=""
523
+ suppressLabel
524
+ value={String(fieldOf(row, editorField(column)) ?? '')}
525
+ placeholder={column.meta.cellEditorPlaceholder ?? ''}
526
+ onValueChange={(value) =>
527
+ onCellValueChange?.({ columnId: column.id, row, field: editorField(column), value })
528
+ }
529
+ />
530
+ ) : column.meta?.cellEditor === 'dropdown' ? (
531
+ <InputDropdown
532
+ label={column.meta.cellEditorDropdownLabel ?? ''}
533
+ suffix={column.meta.cellEditorDropdownSuffix ?? false}
534
+ size={InputDropdownSize.Small}
535
+ options={column.meta.cellEditorDropdownOptions ?? []}
536
+ placeholder={column.meta.cellEditorPlaceholder ?? ''}
537
+ selectedValue={String(fieldOf(row, editorField(column)) ?? '')}
538
+ onSelectionChange={(item) =>
539
+ onCellValueChange?.({
540
+ columnId: column.id,
541
+ row,
542
+ field: editorField(column),
543
+ value: item?.value ?? null,
544
+ })
545
+ }
546
+ />
547
+ ) : column.meta?.cellEditor === 'time' ? (
548
+ <Timepicker
549
+ label={column.meta.cellEditorTimeLabel ?? ''}
550
+ time={parseTime(fieldOf(row, editorField(column)))}
551
+ meridian={column.meta.cellEditorTimeMeridian ?? false}
552
+ size={column.meta.cellEditorTimeSize ?? TimepickerSize.Small}
553
+ onTimeChange={(value) =>
554
+ onCellValueChange?.({ columnId: column.id, row, field: editorField(column), value })
555
+ }
556
+ />
557
+ ) : column.id === personColumnId ? (
558
+ <View style={styles.person}>
559
+ {avatarName ? (
560
+ <Icon
561
+ name={avatarName}
562
+ size={spacing['3xl']}
563
+ accessibilityLabel={String(fieldOf(row, personNameField) ?? 'Avatar')}
564
+ />
565
+ ) : (
566
+ <View
567
+ style={[
568
+ styles.avatar,
569
+ { backgroundColor: String(fieldOf(row, personAvatarColorField) || colors.gray.medium) },
570
+ ]}>
571
+ <Text style={styles.avatarText}>{initials(row, personNameField, personInitialsField)}</Text>
572
+ </View>
573
+ )}
574
+ <View style={styles.personText}>
575
+ <Text style={styles.name} numberOfLines={1}>
576
+ {String(fieldOf(row, personNameField) ?? '')}
577
+ </Text>
578
+ <Text style={styles.handle} numberOfLines={1}>
579
+ {String(fieldOf(row, personHandleField) ?? '')}
580
+ </Text>
581
+ </View>
582
+ </View>
583
+ ) : column.id === statusColumnId ? (
584
+ <Badge
585
+ label={titleCase(fieldOf(row, statusValueField))}
586
+ color={statusBadgeColor(fieldOf(row, statusValueField))}
587
+ size={BadgeSize.Small}
588
+ />
589
+ ) : column.id === teamsColumnId ? (
590
+ <Text style={styles.cellText} numberOfLines={2}>
591
+ {teamsLine(fieldOf(row, teamsValueField), maxTeamTagsVisible)}
592
+ </Text>
593
+ ) : column.id === viewColumnId ? (
594
+ <Button
595
+ label=""
596
+ variant={ButtonVariant.TertiaryGray}
597
+ size={ButtonSize.Small}
598
+ iconPosition={IconPosition.Only}
599
+ iconName={viewColumnIcon}
600
+ iconDecorative
601
+ accessibilityLabel={viewColumnAriaLabel}
602
+ onPress={() => onRowAction?.({ action: viewColumnActionId, row })}
603
+ />
604
+ ) : column.id === 'actions' ? (
605
+ <View style={styles.actions}>
606
+ {shownActions.map((action) => (
607
+ <View key={action.id} style={styles.actionSlot}>
608
+ <Button
609
+ label={action.icon ? '' : action.label}
610
+ variant={ButtonVariant.TertiaryGray}
611
+ size={ButtonSize.Small}
612
+ iconPosition={action.icon ? IconPosition.Only : IconPosition.None}
613
+ iconName={action.icon}
614
+ iconDecorative
615
+ disabled={isDisabled(row)}
616
+ accessibilityLabel={action.label}
617
+ onPress={() => onRowAction?.({ action: action.id, row })}
618
+ />
619
+ </View>
620
+ ))}
621
+ {menuActions.length > 0 ? (
622
+ <View style={styles.actionSlot}>
623
+ <ActionsMenu
624
+ rowData={row}
625
+ actions={menuItems(row)}
626
+ disabled={isDisabled(row)}
627
+ menuAriaLabel={rowActionsTriggerAriaLabel}
628
+ menuPanelAriaLabel={rowActionsMenuAriaLabel}
629
+ />
630
+ </View>
631
+ ) : null}
632
+ </View>
633
+ ) : (
634
+ <Text style={styles.cellText} numberOfLines={2}>
635
+ {String(cellValue(row, column) ?? '')}
636
+ </Text>
637
+ )}
638
+ </View>
639
+ ))}
640
+ </View>
641
+ );
642
+ })
643
+ )}
644
+ </View>
645
+ </ScrollView>
646
+
647
+ {showPagination && pageCount > 1 ? (
648
+ <View style={styles.pager}>
649
+ <Pagination
650
+ page={safePage}
651
+ pageSize={size}
652
+ collectionSize={sorted.length}
653
+ maxPages={paginationMaxPages}
654
+ variant={PaginationVariant.NumberedLink}
655
+ simpleLayout={SimpleLayout.InfoCenter}
656
+ pageNumberShape={PageNumberShape.Square}
657
+ onPageChange={setPage}
658
+ />
659
+ </View>
660
+ ) : null}
661
+ </View>
662
+ );
663
+ }
664
+
665
+ const styles = StyleSheet.create({
666
+ root: {
667
+ width: '100%',
668
+ gap: spacing.md,
669
+ },
670
+ toolbar: {
671
+ width: '100%',
672
+ maxWidth: '100%',
673
+ flexDirection: 'row',
674
+ alignItems: 'flex-end',
675
+ gap: spacing.sm,
676
+ paddingTop: spacing.base,
677
+ },
678
+ searchField: {
679
+ flex: 1,
680
+ minWidth: spacing.none,
681
+ },
682
+ filterRow: {
683
+ flexDirection: 'row',
684
+ alignItems: 'center',
685
+ flexShrink: 0,
686
+ gap: spacing.sm,
687
+ paddingBottom: spacing.xs,
688
+ },
689
+ filterLabel: {
690
+ fontFamily: typography.fontFamily.base,
691
+ fontSize: typography.fontSize.sm,
692
+ color: colors.text.muted,
693
+ },
694
+ filterField: {
695
+ width: spacing['7xl'] * 3,
696
+ },
697
+ grid: {
698
+ borderWidth: borders.width.thin,
699
+ borderColor: colors.dataTable.border,
700
+ borderRadius: radius.lg,
701
+ overflow: 'hidden',
702
+ backgroundColor: colors.white,
703
+ },
704
+ headerRow: {
705
+ flexDirection: 'row',
706
+ borderBottomWidth: borders.width.thin,
707
+ borderBottomColor: colors.dataTable.border,
708
+ backgroundColor: colors.white,
709
+ },
710
+ th: {
711
+ paddingHorizontal: spacing.md,
712
+ paddingVertical: spacing.md,
713
+ justifyContent: 'center',
714
+ },
715
+ thCenter: {
716
+ alignItems: 'center',
717
+ },
718
+ thActions: {
719
+ paddingHorizontal: spacing.sm,
720
+ },
721
+ thInner: {
722
+ flexDirection: 'row',
723
+ alignItems: 'center',
724
+ gap: spacing.xs,
725
+ },
726
+ thLabel: {
727
+ fontFamily: typography.fontFamily.base,
728
+ fontSize: typography.fontSize.sm,
729
+ fontWeight: typography.fontWeight.medium,
730
+ color: colors.dataTable.text,
731
+ },
732
+ sortAsc: {
733
+ transform: [{ rotate: '180deg' }],
734
+ },
735
+ bodyRow: {
736
+ flexDirection: 'row',
737
+ backgroundColor: colors.dataTable.rowBg,
738
+ borderBottomWidth: borders.width.thin,
739
+ borderBottomColor: colors.dataTable.border,
740
+ },
741
+ bodyRowStripe: {
742
+ backgroundColor: colors.dataTable.rowStripeBg,
743
+ },
744
+ bodyRowSelected: {
745
+ backgroundColor: colors.dataTable.inactiveBg,
746
+ },
747
+ td: {
748
+ paddingHorizontal: spacing.md,
749
+ paddingVertical: spacing.sm,
750
+ justifyContent: 'center',
751
+ },
752
+ tdCenter: {
753
+ alignItems: 'center',
754
+ },
755
+ tdActions: {
756
+ paddingHorizontal: spacing.sm,
757
+ },
758
+ cellText: {
759
+ fontFamily: typography.fontFamily.base,
760
+ fontSize: typography.fontSize.sm,
761
+ color: colors.dataTable.text,
762
+ },
763
+ person: {
764
+ flexDirection: 'row',
765
+ alignItems: 'center',
766
+ gap: spacing.md,
767
+ minWidth: spacing.none,
768
+ },
769
+ avatar: {
770
+ width: spacing['3xl'],
771
+ height: spacing['3xl'],
772
+ borderRadius: radius.full,
773
+ alignItems: 'center',
774
+ justifyContent: 'center',
775
+ },
776
+ avatarText: {
777
+ fontFamily: typography.fontFamily.base,
778
+ fontSize: typography.fontSize.sm,
779
+ color: colors.white,
780
+ fontWeight: typography.fontWeight.medium,
781
+ },
782
+ personText: {
783
+ flex: 1,
784
+ minWidth: spacing.none,
785
+ },
786
+ name: {
787
+ fontFamily: typography.fontFamily.base,
788
+ fontSize: typography.fontSize.sm,
789
+ fontWeight: typography.fontWeight.medium,
790
+ color: colors.dataTable.namePrimary,
791
+ },
792
+ handle: {
793
+ fontFamily: typography.fontFamily.base,
794
+ fontSize: typography.fontSize.sm,
795
+ color: colors.dataTable.muted,
796
+ },
797
+ actions: {
798
+ flexDirection: 'row',
799
+ alignItems: 'center',
800
+ justifyContent: 'flex-end',
801
+ gap: spacing.md,
802
+ },
803
+ actionSlot: {
804
+ alignItems: 'center',
805
+ justifyContent: 'center',
806
+ },
807
+ empty: {
808
+ paddingVertical: spacing['3xl'],
809
+ paddingHorizontal: spacing.xl,
810
+ alignItems: 'center',
811
+ backgroundColor: colors.dataTable.rowBg,
812
+ },
813
+ emptyText: {
814
+ fontFamily: typography.fontFamily.base,
815
+ fontSize: typography.fontSize.sm,
816
+ color: colors.text.muted,
817
+ textAlign: 'center',
818
+ },
819
+ pager: {
820
+ width: '100%',
821
+ },
822
+ });