@bacon-pos/ui 0.9.0 → 0.10.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 (123) hide show
  1. package/lib/commonjs/components/EmptyState/EmptyState.js +88 -0
  2. package/lib/commonjs/components/EmptyState/EmptyState.js.map +1 -0
  3. package/lib/commonjs/components/EmptyState/EmptyState.stories.js +46 -0
  4. package/lib/commonjs/components/EmptyState/EmptyState.stories.js.map +1 -0
  5. package/lib/commonjs/components/EmptyState/EmptyState.test.js +40 -0
  6. package/lib/commonjs/components/EmptyState/EmptyState.test.js.map +1 -0
  7. package/lib/commonjs/components/EmptyState/index.js +13 -0
  8. package/lib/commonjs/components/EmptyState/index.js.map +1 -0
  9. package/lib/commonjs/components/Sidebar/Sidebar.js +264 -0
  10. package/lib/commonjs/components/Sidebar/Sidebar.js.map +1 -0
  11. package/lib/commonjs/components/Sidebar/Sidebar.stories.js +229 -0
  12. package/lib/commonjs/components/Sidebar/Sidebar.stories.js.map +1 -0
  13. package/lib/commonjs/components/Sidebar/Sidebar.test.js +131 -0
  14. package/lib/commonjs/components/Sidebar/Sidebar.test.js.map +1 -0
  15. package/lib/commonjs/components/Sidebar/index.js +13 -0
  16. package/lib/commonjs/components/Sidebar/index.js.map +1 -0
  17. package/lib/commonjs/components/Table/DataTable.js +149 -0
  18. package/lib/commonjs/components/Table/DataTable.js.map +1 -0
  19. package/lib/commonjs/components/Table/DataTable.stories.js +214 -0
  20. package/lib/commonjs/components/Table/DataTable.stories.js.map +1 -0
  21. package/lib/commonjs/components/Table/DataTable.test.js +143 -0
  22. package/lib/commonjs/components/Table/DataTable.test.js.map +1 -0
  23. package/lib/commonjs/components/Table/Table.js +520 -0
  24. package/lib/commonjs/components/Table/Table.js.map +1 -0
  25. package/lib/commonjs/components/Table/Table.stories.js +385 -0
  26. package/lib/commonjs/components/Table/Table.stories.js.map +1 -0
  27. package/lib/commonjs/components/Table/Table.test.js +278 -0
  28. package/lib/commonjs/components/Table/Table.test.js.map +1 -0
  29. package/lib/commonjs/components/Table/index.js +20 -0
  30. package/lib/commonjs/components/Table/index.js.map +1 -0
  31. package/lib/commonjs/components/brand/BaconLogo.js +29 -0
  32. package/lib/commonjs/components/brand/BaconLogo.js.map +1 -0
  33. package/lib/commonjs/components/brand/index.js +7 -0
  34. package/lib/commonjs/components/brand/index.js.map +1 -1
  35. package/lib/commonjs/components/index.js +50 -17
  36. package/lib/commonjs/components/index.js.map +1 -1
  37. package/lib/module/components/EmptyState/EmptyState.js +84 -0
  38. package/lib/module/components/EmptyState/EmptyState.js.map +1 -0
  39. package/lib/module/components/EmptyState/EmptyState.stories.js +42 -0
  40. package/lib/module/components/EmptyState/EmptyState.stories.js.map +1 -0
  41. package/lib/module/components/EmptyState/EmptyState.test.js +40 -0
  42. package/lib/module/components/EmptyState/EmptyState.test.js.map +1 -0
  43. package/lib/module/components/EmptyState/index.js +4 -0
  44. package/lib/module/components/EmptyState/index.js.map +1 -0
  45. package/lib/module/components/Sidebar/Sidebar.js +260 -0
  46. package/lib/module/components/Sidebar/Sidebar.js.map +1 -0
  47. package/lib/module/components/Sidebar/Sidebar.stories.js +225 -0
  48. package/lib/module/components/Sidebar/Sidebar.stories.js.map +1 -0
  49. package/lib/module/components/Sidebar/Sidebar.test.js +131 -0
  50. package/lib/module/components/Sidebar/Sidebar.test.js.map +1 -0
  51. package/lib/module/components/Sidebar/index.js +4 -0
  52. package/lib/module/components/Sidebar/index.js.map +1 -0
  53. package/lib/module/components/Table/DataTable.js +145 -0
  54. package/lib/module/components/Table/DataTable.js.map +1 -0
  55. package/lib/module/components/Table/DataTable.stories.js +210 -0
  56. package/lib/module/components/Table/DataTable.stories.js.map +1 -0
  57. package/lib/module/components/Table/DataTable.test.js +143 -0
  58. package/lib/module/components/Table/DataTable.test.js.map +1 -0
  59. package/lib/module/components/Table/Table.js +516 -0
  60. package/lib/module/components/Table/Table.js.map +1 -0
  61. package/lib/module/components/Table/Table.stories.js +381 -0
  62. package/lib/module/components/Table/Table.stories.js.map +1 -0
  63. package/lib/module/components/Table/Table.test.js +278 -0
  64. package/lib/module/components/Table/Table.test.js.map +1 -0
  65. package/lib/module/components/Table/index.js +5 -0
  66. package/lib/module/components/Table/index.js.map +1 -0
  67. package/lib/module/components/brand/BaconLogo.js +25 -0
  68. package/lib/module/components/brand/BaconLogo.js.map +1 -0
  69. package/lib/module/components/brand/index.js +1 -0
  70. package/lib/module/components/brand/index.js.map +1 -1
  71. package/lib/module/components/index.js +3 -0
  72. package/lib/module/components/index.js.map +1 -1
  73. package/lib/typescript/commonjs/components/EmptyState/EmptyState.d.ts +30 -0
  74. package/lib/typescript/commonjs/components/EmptyState/EmptyState.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/components/EmptyState/index.d.ts +3 -0
  76. package/lib/typescript/commonjs/components/EmptyState/index.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/components/Sidebar/Sidebar.d.ts +57 -0
  78. package/lib/typescript/commonjs/components/Sidebar/Sidebar.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/components/Sidebar/index.d.ts +3 -0
  80. package/lib/typescript/commonjs/components/Sidebar/index.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/components/Table/DataTable.d.ts +53 -0
  82. package/lib/typescript/commonjs/components/Table/DataTable.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/components/Table/Table.d.ts +85 -0
  84. package/lib/typescript/commonjs/components/Table/Table.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/components/Table/index.d.ts +5 -0
  86. package/lib/typescript/commonjs/components/Table/index.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/components/brand/BaconLogo.d.ts +13 -0
  88. package/lib/typescript/commonjs/components/brand/BaconLogo.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/components/brand/index.d.ts +1 -0
  90. package/lib/typescript/commonjs/components/brand/index.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/components/index.d.ts +3 -0
  92. package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
  93. package/lib/typescript/module/components/EmptyState/EmptyState.d.ts +30 -0
  94. package/lib/typescript/module/components/EmptyState/EmptyState.d.ts.map +1 -0
  95. package/lib/typescript/module/components/EmptyState/index.d.ts +3 -0
  96. package/lib/typescript/module/components/EmptyState/index.d.ts.map +1 -0
  97. package/lib/typescript/module/components/Sidebar/Sidebar.d.ts +57 -0
  98. package/lib/typescript/module/components/Sidebar/Sidebar.d.ts.map +1 -0
  99. package/lib/typescript/module/components/Sidebar/index.d.ts +3 -0
  100. package/lib/typescript/module/components/Sidebar/index.d.ts.map +1 -0
  101. package/lib/typescript/module/components/Table/DataTable.d.ts +53 -0
  102. package/lib/typescript/module/components/Table/DataTable.d.ts.map +1 -0
  103. package/lib/typescript/module/components/Table/Table.d.ts +85 -0
  104. package/lib/typescript/module/components/Table/Table.d.ts.map +1 -0
  105. package/lib/typescript/module/components/Table/index.d.ts +5 -0
  106. package/lib/typescript/module/components/Table/index.d.ts.map +1 -0
  107. package/lib/typescript/module/components/brand/BaconLogo.d.ts +13 -0
  108. package/lib/typescript/module/components/brand/BaconLogo.d.ts.map +1 -0
  109. package/lib/typescript/module/components/brand/index.d.ts +1 -0
  110. package/lib/typescript/module/components/brand/index.d.ts.map +1 -1
  111. package/lib/typescript/module/components/index.d.ts +3 -0
  112. package/lib/typescript/module/components/index.d.ts.map +1 -1
  113. package/package.json +1 -1
  114. package/src/components/EmptyState/EmptyState.tsx +91 -0
  115. package/src/components/EmptyState/index.ts +2 -0
  116. package/src/components/Sidebar/Sidebar.tsx +320 -0
  117. package/src/components/Sidebar/index.ts +2 -0
  118. package/src/components/Table/DataTable.tsx +173 -0
  119. package/src/components/Table/Table.tsx +556 -0
  120. package/src/components/Table/index.ts +10 -0
  121. package/src/components/brand/BaconLogo.tsx +25 -0
  122. package/src/components/brand/index.ts +1 -0
  123. package/src/components/index.ts +3 -0
@@ -0,0 +1,556 @@
1
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ Platform,
6
+ Pressable,
7
+ useWindowDimensions,
8
+ View,
9
+ type StyleProp,
10
+ type TextStyle,
11
+ type ViewStyle,
12
+ } from 'react-native';
13
+ import { useTheme } from '../../theme';
14
+ import { Checkbox } from '../Checkbox/Checkbox';
15
+ import { Icon } from '../Icon/Icon';
16
+ import { Spinner } from '../Spinner/Spinner';
17
+ import { Text } from '../Text/Text';
18
+ import { ScrollArea } from '../_internal/ScrollArea';
19
+
20
+ export type ColumnAlign = 'left' | 'center' | 'right';
21
+ export type SortDirection = 'asc' | 'desc';
22
+ export interface TableSort {
23
+ key: string;
24
+ direction: SortDirection;
25
+ }
26
+
27
+ export interface TableColumn<T> {
28
+ /** Unique column id. */
29
+ key: string;
30
+ /** Header content (string or node). */
31
+ header: ReactNode;
32
+ /** Cell renderer. Defaults to `String(row[key])`. Return a string/number or any node (e.g. a `Badge`). */
33
+ render?: (row: T, index: number) => ReactNode;
34
+ /** Flex grow. Defaults to 1. Ignored when `width` is set. */
35
+ flex?: number;
36
+ /** Fixed column width in px. */
37
+ width?: number;
38
+ /** Text/content alignment. Defaults to `left`. */
39
+ align?: ColumnAlign;
40
+ /** Enable sorting on this column (needs `sortAccessor`). */
41
+ sortable?: boolean;
42
+ /** Value used to sort this column when sorting internally. */
43
+ sortAccessor?: (row: T) => string | number;
44
+ }
45
+
46
+ export interface TableProps<T> {
47
+ columns: TableColumn<T>[];
48
+ data: T[];
49
+ /** Stable key per row. */
50
+ keyExtractor: (row: T, index: number) => string;
51
+
52
+ /** Row press (non-expandable tables). Expandable rows toggle on press instead. */
53
+ onRowPress?: (row: T, index: number) => void;
54
+
55
+ /** Expandable (accordion) rows — mutually exclusive with `renderActions`. */
56
+ expandable?: boolean;
57
+ /** Content shown when a row is expanded. */
58
+ renderExpanded?: (row: T, index: number) => ReactNode;
59
+ /** One open row at a time (`single`) or many (`multiple`). Defaults to `single`. */
60
+ expansionMode?: 'single' | 'multiple';
61
+ /** Controlled expanded row keys. */
62
+ expandedKeys?: string[];
63
+ defaultExpandedKeys?: string[];
64
+ onExpandedChange?: (keys: string[]) => void;
65
+
66
+ /** Trailing per-row actions (e.g. edit/delete) — mutually exclusive with `expandable`. */
67
+ renderActions?: (row: T, index: number) => ReactNode;
68
+ /** Width reserved for the actions column. Defaults to 96. */
69
+ actionsWidth?: number;
70
+
71
+ /** Row selection via checkboxes. */
72
+ selectable?: boolean;
73
+ selectedKeys?: string[];
74
+ defaultSelectedKeys?: string[];
75
+ onSelectionChange?: (keys: string[]) => void;
76
+
77
+ /** Controlled sort. Omit for internal sorting via each column's `sortAccessor`. */
78
+ sort?: TableSort;
79
+ defaultSort?: TableSort;
80
+ onSortChange?: (sort: TableSort) => void;
81
+
82
+ /** Rounded, clipped container. Defaults to true. */
83
+ rounded?: boolean;
84
+ /** Cap the body height; rows scroll with the elegant indicator, header stays. */
85
+ maxHeight?: number;
86
+ scrollbarColor?: string;
87
+
88
+ /** Show a loading placeholder instead of rows. */
89
+ loading?: boolean;
90
+ /** Loading style: a centered `spinner` (default) or shimmering `skeleton` rows. */
91
+ loadingVariant?: 'spinner' | 'skeleton';
92
+ /** Text under the spinner. Defaults to "Loading data"; pass `null` to hide it. */
93
+ loadingText?: string | null;
94
+ /** Number of skeleton rows. Defaults to 5. */
95
+ skeletonRows?: number;
96
+ /** Shown when there is no data (and not loading). */
97
+ empty?: ReactNode;
98
+
99
+ /** Below this width the table renders each row as a card. Defaults to 640. */
100
+ mobileBreakpoint?: number;
101
+
102
+ style?: StyleProp<ViewStyle>;
103
+ }
104
+
105
+ const ROW_HEIGHT = 72;
106
+ const PADDING = 24;
107
+ const CELL_GAP = 8;
108
+ const SEL_WIDTH = 24;
109
+ const CHEVRON_WIDTH = 24;
110
+
111
+ function alignToJustify(a: ColumnAlign | undefined): ViewStyle['justifyContent'] {
112
+ return a === 'center' ? 'center' : a === 'right' ? 'flex-end' : 'flex-start';
113
+ }
114
+ function alignToText(a: ColumnAlign | undefined): TextStyle['textAlign'] {
115
+ return a === 'center' ? 'center' : a === 'right' ? 'right' : 'left';
116
+ }
117
+
118
+ /**
119
+ * Table — a flexible, token-driven data table (Figma "Table"). Columns define
120
+ * width/flex, alignment and cell renderers (text, `Badge`, anything). Supports
121
+ * sortable headers, checkbox selection, and accordion rows (expand on row press)
122
+ * OR a trailing actions column (the two are mutually exclusive). A sticky header
123
+ * with an elegant scrolling body (`maxHeight`), loading and empty states, and a
124
+ * card fallback below `mobileBreakpoint`.
125
+ */
126
+ export function Table<T>({
127
+ columns,
128
+ data,
129
+ keyExtractor,
130
+ onRowPress,
131
+ expandable = false,
132
+ renderExpanded,
133
+ expansionMode = 'single',
134
+ expandedKeys,
135
+ defaultExpandedKeys,
136
+ onExpandedChange,
137
+ renderActions,
138
+ actionsWidth = 96,
139
+ selectable = false,
140
+ selectedKeys,
141
+ defaultSelectedKeys,
142
+ onSelectionChange,
143
+ sort,
144
+ defaultSort,
145
+ onSortChange,
146
+ rounded = true,
147
+ maxHeight,
148
+ scrollbarColor,
149
+ loading = false,
150
+ loadingVariant = 'spinner',
151
+ loadingText = 'Loading data',
152
+ skeletonRows = 5,
153
+ empty,
154
+ mobileBreakpoint = 640,
155
+ style,
156
+ }: TableProps<T>) {
157
+ const theme = useTheme();
158
+ const { width: screenWidth } = useWindowDimensions();
159
+ const isMobile = screenWidth < mobileBreakpoint;
160
+
161
+ // Expandable and actions are mutually exclusive; expandable wins if both set.
162
+ const hasActions = !expandable && !!renderActions;
163
+ const trailingWidth = expandable ? CHEVRON_WIDTH : hasActions ? actionsWidth : 0;
164
+
165
+ // ---- sort ----
166
+ const sortControlled = sort !== undefined;
167
+ const [sortInternal, setSortInternal] = useState<TableSort | undefined>(defaultSort);
168
+ const activeSort = sortControlled ? sort : sortInternal;
169
+ const setSort = (next: TableSort) => {
170
+ if (!sortControlled) setSortInternal(next);
171
+ onSortChange?.(next);
172
+ };
173
+ const onHeaderPress = (col: TableColumn<T>) => {
174
+ if (!col.sortable) return;
175
+ const direction: SortDirection =
176
+ activeSort?.key === col.key && activeSort.direction === 'asc' ? 'desc' : 'asc';
177
+ setSort({ key: col.key, direction });
178
+ };
179
+ const sortedData = useMemo(() => {
180
+ if (sortControlled || !activeSort) return data; // controlled → consumer sorts
181
+ const col = columns.find((c) => c.key === activeSort.key);
182
+ if (!col?.sortAccessor) return data;
183
+ const accessor = col.sortAccessor;
184
+ const arr = [...data].sort((a, b) => {
185
+ const av = accessor(a);
186
+ const bv = accessor(b);
187
+ if (av < bv) return -1;
188
+ if (av > bv) return 1;
189
+ return 0;
190
+ });
191
+ return activeSort.direction === 'asc' ? arr : arr.reverse();
192
+ }, [data, activeSort, sortControlled, columns]);
193
+
194
+ // ---- selection ----
195
+ const selControlled = selectedKeys !== undefined;
196
+ const [selInternal, setSelInternal] = useState<string[]>(defaultSelectedKeys ?? []);
197
+ const selected = selControlled ? selectedKeys : selInternal;
198
+ const setSelected = (keys: string[]) => {
199
+ if (!selControlled) setSelInternal(keys);
200
+ onSelectionChange?.(keys);
201
+ };
202
+ const allKeys = sortedData.map((r, i) => keyExtractor(r, i));
203
+ const allSelected = allKeys.length > 0 && allKeys.every((k) => selected.includes(k));
204
+ const someSelected = !allSelected && allKeys.some((k) => selected.includes(k));
205
+ const toggleRowSel = (k: string) =>
206
+ setSelected(selected.includes(k) ? selected.filter((x) => x !== k) : [...selected, k]);
207
+ const toggleAllSel = () => setSelected(allSelected ? [] : allKeys);
208
+
209
+ // ---- expansion ----
210
+ const expControlled = expandedKeys !== undefined;
211
+ const [expInternal, setExpInternal] = useState<string[]>(defaultExpandedKeys ?? []);
212
+ const expanded = expControlled ? expandedKeys : expInternal;
213
+ const setExpanded = (keys: string[]) => {
214
+ if (!expControlled) setExpInternal(keys);
215
+ onExpandedChange?.(keys);
216
+ };
217
+ const toggleExpand = (k: string) => {
218
+ const isOpen = expanded.includes(k);
219
+ if (expansionMode === 'single') setExpanded(isOpen ? [] : [k]);
220
+ else setExpanded(isOpen ? expanded.filter((x) => x !== k) : [...expanded, k]);
221
+ };
222
+
223
+ const cellStyle = (col: TableColumn<T>): ViewStyle => ({
224
+ ...(col.width != null ? { width: col.width } : { flex: col.flex ?? 1 }),
225
+ minWidth: 0,
226
+ flexDirection: 'row',
227
+ alignItems: 'center',
228
+ justifyContent: alignToJustify(col.align),
229
+ });
230
+
231
+ const renderCellContent = (col: TableColumn<T>, row: T, index: number) => {
232
+ const content = col.render ? col.render(row, index) : (row as Record<string, unknown>)[col.key];
233
+ if (content == null || typeof content === 'string' || typeof content === 'number') {
234
+ return (
235
+ <Text
236
+ variant="bodyMedium"
237
+ color={theme.colors.action.secondaryForeground}
238
+ numberOfLines={1}
239
+ style={{ flexShrink: 1, textAlign: alignToText(col.align) }}
240
+ >
241
+ {content == null ? '' : String(content)}
242
+ </Text>
243
+ );
244
+ }
245
+ return content as ReactNode;
246
+ };
247
+
248
+ // ---------- Desktop ----------
249
+ const header = (
250
+ <View
251
+ style={{
252
+ flexDirection: 'row',
253
+ alignItems: 'center',
254
+ gap: CELL_GAP,
255
+ paddingHorizontal: PADDING,
256
+ paddingVertical: 16,
257
+ backgroundColor: theme.colors.background.muted,
258
+ ...(rounded ? { borderTopLeftRadius: theme.radii.lg, borderTopRightRadius: theme.radii.lg } : null),
259
+ }}
260
+ >
261
+ {selectable ? (
262
+ <View style={{ width: SEL_WIDTH, flexShrink: 0 }}>
263
+ <Checkbox
264
+ checked={allSelected}
265
+ indeterminate={someSelected}
266
+ onChange={toggleAllSel}
267
+ />
268
+ </View>
269
+ ) : null}
270
+ {columns.map((col) => {
271
+ const isSorted = activeSort?.key === col.key;
272
+ return (
273
+ <Pressable
274
+ key={col.key}
275
+ disabled={!col.sortable}
276
+ onPress={() => onHeaderPress(col)}
277
+ style={[cellStyle(col), { gap: 4 }]}
278
+ >
279
+ {typeof col.header === 'string' ? (
280
+ <Text
281
+ variant="bodyMedium"
282
+ weight="medium"
283
+ color="secondary"
284
+ numberOfLines={1}
285
+ style={{ flexShrink: 1, textAlign: alignToText(col.align) }}
286
+ >
287
+ {col.header}
288
+ </Text>
289
+ ) : (
290
+ col.header
291
+ )}
292
+ {col.sortable ? (
293
+ <View style={{ transform: [{ rotate: isSorted && activeSort?.direction === 'asc' ? '180deg' : '0deg' }] }}>
294
+ <Icon name="chevron-down" size={16} color={isSorted ? 'secondary' : 'tertiary'} />
295
+ </View>
296
+ ) : null}
297
+ </Pressable>
298
+ );
299
+ })}
300
+ {trailingWidth > 0 ? <View style={{ width: trailingWidth, flexShrink: 0 }} /> : null}
301
+ </View>
302
+ );
303
+
304
+ const renderDesktopRow = (row: T, index: number) => {
305
+ const k = keyExtractor(row, index);
306
+ const isOpen = expanded.includes(k);
307
+ const rowPressable = expandable || !!onRowPress;
308
+ return (
309
+ <View key={k}>
310
+ <Pressable
311
+ onPress={rowPressable ? () => (expandable ? toggleExpand(k) : onRowPress?.(row, index)) : undefined}
312
+ disabled={!rowPressable}
313
+ style={({ pressed }) => ({
314
+ flexDirection: 'row',
315
+ alignItems: 'center',
316
+ gap: CELL_GAP,
317
+ minHeight: ROW_HEIGHT,
318
+ paddingHorizontal: PADDING,
319
+ paddingVertical: 12,
320
+ backgroundColor:
321
+ pressed && rowPressable ? theme.colors.background.subtle : theme.colors.background.default,
322
+ borderBottomWidth: 1,
323
+ borderBottomColor: theme.colors.border.subtle,
324
+ })}
325
+ >
326
+ {selectable ? (
327
+ <View style={{ width: SEL_WIDTH, flexShrink: 0 }}>
328
+ <Checkbox
329
+ checked={selected.includes(k)}
330
+ onChange={() => toggleRowSel(k)}
331
+ />
332
+ </View>
333
+ ) : null}
334
+ {columns.map((col) => (
335
+ <View key={col.key} style={cellStyle(col)}>
336
+ {renderCellContent(col, row, index)}
337
+ </View>
338
+ ))}
339
+ {expandable ? (
340
+ <View
341
+ style={{
342
+ width: CHEVRON_WIDTH,
343
+ flexShrink: 0,
344
+ transform: [{ rotate: isOpen ? '180deg' : '0deg' }],
345
+ }}
346
+ >
347
+ <Icon name="chevron-down" size={24} color="secondary" />
348
+ </View>
349
+ ) : hasActions ? (
350
+ <View style={{ width: actionsWidth, flexShrink: 0, flexDirection: 'row', justifyContent: 'flex-end', gap: 4 }}>
351
+ {renderActions!(row, index)}
352
+ </View>
353
+ ) : null}
354
+ </Pressable>
355
+ {expandable && isOpen && renderExpanded ? (
356
+ <View
357
+ style={{
358
+ paddingHorizontal: PADDING,
359
+ paddingVertical: 16,
360
+ backgroundColor: theme.colors.background.subtle,
361
+ borderBottomWidth: 1,
362
+ borderBottomColor: theme.colors.border.subtle,
363
+ }}
364
+ >
365
+ {renderExpanded(row, index)}
366
+ </View>
367
+ ) : null}
368
+ </View>
369
+ );
370
+ };
371
+
372
+ // ---------- Mobile (cards) ----------
373
+ const renderMobileCard = (row: T, index: number) => {
374
+ const k = keyExtractor(row, index);
375
+ const isOpen = expanded.includes(k);
376
+ const rowPressable = expandable || !!onRowPress;
377
+ return (
378
+ <View
379
+ key={k}
380
+ style={{
381
+ borderWidth: 1,
382
+ borderColor: theme.colors.border.default,
383
+ borderRadius: theme.radii.lg,
384
+ backgroundColor: theme.colors.background.default,
385
+ overflow: 'hidden',
386
+ }}
387
+ >
388
+ <Pressable
389
+ onPress={rowPressable ? () => (expandable ? toggleExpand(k) : onRowPress?.(row, index)) : undefined}
390
+ disabled={!rowPressable}
391
+ style={{ padding: 16, gap: 10 }}
392
+ >
393
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
394
+ {selectable ? (
395
+ <Checkbox
396
+ checked={selected.includes(k)}
397
+ onChange={() => toggleRowSel(k)}
398
+ />
399
+ ) : null}
400
+ <View style={{ flex: 1, gap: 8 }}>
401
+ {columns.map((col) => (
402
+ <View key={col.key} style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
403
+ {typeof col.header === 'string' ? (
404
+ <Text variant="bodySmall" color="tertiary" numberOfLines={1}>
405
+ {col.header}
406
+ </Text>
407
+ ) : (
408
+ col.header
409
+ )}
410
+ <View style={{ flexShrink: 1, alignItems: 'flex-end' }}>{renderCellContent(col, row, index)}</View>
411
+ </View>
412
+ ))}
413
+ </View>
414
+ {expandable ? (
415
+ <View style={{ transform: [{ rotate: isOpen ? '180deg' : '0deg' }] }}>
416
+ <Icon name="chevron-down" size={24} color="secondary" />
417
+ </View>
418
+ ) : null}
419
+ </View>
420
+ {hasActions ? (
421
+ <View style={{ flexDirection: 'row', justifyContent: 'flex-end', gap: 4 }}>{renderActions!(row, index)}</View>
422
+ ) : null}
423
+ </Pressable>
424
+ {expandable && isOpen && renderExpanded ? (
425
+ <View style={{ padding: 16, paddingTop: 0 }}>{renderExpanded(row, index)}</View>
426
+ ) : null}
427
+ </View>
428
+ );
429
+ };
430
+
431
+ // ---------- Loading ----------
432
+ const pulse = useRef(new Animated.Value(0)).current;
433
+ useEffect(() => {
434
+ if (!loading || loadingVariant !== 'skeleton') return;
435
+ const loop = Animated.loop(
436
+ Animated.sequence([
437
+ Animated.timing(pulse, { toValue: 1, duration: 700, easing: Easing.inOut(Easing.ease), useNativeDriver: Platform.OS !== 'web' }),
438
+ Animated.timing(pulse, { toValue: 0, duration: 700, easing: Easing.inOut(Easing.ease), useNativeDriver: Platform.OS !== 'web' }),
439
+ ]),
440
+ );
441
+ loop.start();
442
+ return () => loop.stop();
443
+ }, [loading, loadingVariant, pulse]);
444
+ const barOpacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.45, 0.9] });
445
+ const bar = (style?: StyleProp<ViewStyle>) => (
446
+ <Animated.View
447
+ style={[{ height: 16, borderRadius: 8, backgroundColor: theme.colors.background.muted, opacity: barOpacity }, style]}
448
+ />
449
+ );
450
+
451
+ const renderSkeleton = () =>
452
+ isMobile ? (
453
+ <View style={{ gap: 12 }}>
454
+ {Array.from({ length: skeletonRows }).map((_, i) => (
455
+ <View
456
+ key={i}
457
+ style={{
458
+ borderWidth: 1,
459
+ borderColor: theme.colors.border.default,
460
+ borderRadius: theme.radii.lg,
461
+ padding: 16,
462
+ gap: 10,
463
+ }}
464
+ >
465
+ {columns.map((col) => (
466
+ <View key={col.key} style={{ flexDirection: 'row', justifyContent: 'space-between', gap: 12 }}>
467
+ {bar({ width: 72 })}
468
+ {bar({ width: 96 })}
469
+ </View>
470
+ ))}
471
+ </View>
472
+ ))}
473
+ </View>
474
+ ) : (
475
+ <View>
476
+ {Array.from({ length: skeletonRows }).map((_, i) => (
477
+ <View
478
+ key={i}
479
+ style={{
480
+ flexDirection: 'row',
481
+ alignItems: 'center',
482
+ gap: CELL_GAP,
483
+ minHeight: 64,
484
+ paddingHorizontal: PADDING,
485
+ borderBottomWidth: 1,
486
+ borderBottomColor: theme.colors.border.subtle,
487
+ }}
488
+ >
489
+ {selectable ? <View style={{ width: SEL_WIDTH, flexShrink: 0 }}>{bar({ width: 20, height: 20, borderRadius: 6 })}</View> : null}
490
+ {columns.map((col) => (
491
+ <View key={col.key} style={cellStyle(col)}>
492
+ {bar({ width: '70%', maxWidth: 180 })}
493
+ </View>
494
+ ))}
495
+ {trailingWidth > 0 ? <View style={{ width: trailingWidth, flexShrink: 0 }} /> : null}
496
+ </View>
497
+ ))}
498
+ </View>
499
+ );
500
+
501
+ const renderSpinner = () => (
502
+ <View style={{ paddingVertical: 48, alignItems: 'center', gap: 12 }}>
503
+ <Spinner />
504
+ {loadingText ? (
505
+ <Text variant="bodyMedium" color="secondary">
506
+ {loadingText}
507
+ </Text>
508
+ ) : null}
509
+ </View>
510
+ );
511
+
512
+ // ---------- Body ----------
513
+ const bodyContent = loading ? (
514
+ loadingVariant === 'skeleton' ? (
515
+ renderSkeleton()
516
+ ) : (
517
+ renderSpinner()
518
+ )
519
+ ) : sortedData.length === 0 ? (
520
+ <View>{empty ?? null}</View>
521
+ ) : isMobile ? (
522
+ <View style={{ gap: 12 }}>{sortedData.map(renderMobileCard)}</View>
523
+ ) : (
524
+ <View>{sortedData.map(renderDesktopRow)}</View>
525
+ );
526
+
527
+ const body =
528
+ maxHeight != null ? (
529
+ <ScrollArea style={{ maxHeight }} indicatorColor={scrollbarColor ?? theme.colors.border.strong}>
530
+ {bodyContent}
531
+ </ScrollArea>
532
+ ) : (
533
+ bodyContent
534
+ );
535
+
536
+ if (isMobile) {
537
+ // Cards don't use the table header; labels live inside each card.
538
+ return <View style={[{ gap: 12 }, style]}>{body}</View>;
539
+ }
540
+
541
+ return (
542
+ <View
543
+ style={[
544
+ {
545
+ borderRadius: rounded ? theme.radii.lg : 0,
546
+ overflow: 'hidden',
547
+ backgroundColor: theme.colors.background.default,
548
+ },
549
+ style,
550
+ ]}
551
+ >
552
+ {header}
553
+ {body}
554
+ </View>
555
+ );
556
+ }
@@ -0,0 +1,10 @@
1
+ export { Table } from './Table';
2
+ export type {
3
+ TableProps,
4
+ TableColumn,
5
+ TableSort,
6
+ ColumnAlign,
7
+ SortDirection,
8
+ } from './Table';
9
+ export { DataTable } from './DataTable';
10
+ export type { DataTableProps, DataTableTab, DataTablePagination } from './DataTable';
@@ -0,0 +1,25 @@
1
+ import { SvgXml } from 'react-native-svg';
2
+
3
+ const VIEWBOX_W = 90.029;
4
+ const VIEWBOX_H = 75.2663;
5
+ const ASPECT = VIEWBOX_W / VIEWBOX_H;
6
+
7
+ const PATH =
8
+ 'M79.6034 46.0332C84.1787 43.6264 87.0741 39.2894 87.0741 33.6655C87.0741 24.914 79.8477 19.0519 65.794 19.0519H51.2579H41.8987C40.594 19.0519 39.3728 18.7481 38.2826 18.2119C37.1626 17.6638 36.1975 16.8715 35.4409 15.9004C34.3923 14.5421 33.7609 12.8443 33.7609 11.0034V8.05447C33.7609 3.60426 30.1149 0 25.6289 0C21.143 0 17.497 3.60426 17.497 8.05447V11.0034C17.497 12.8443 16.8715 14.5421 15.817 15.9004C15.0604 16.8715 14.0953 17.6698 12.9753 18.2119C11.8851 18.7481 10.6638 19.0519 9.35915 19.0519H0V28.846H9.35915C13.857 28.846 17.4911 25.2357 17.4911 20.7915V16.4306C17.4911 11.9804 21.137 8.37617 25.623 8.37617C30.1089 8.37617 33.7549 11.9804 33.7549 16.4306V20.7915C33.7549 25.2357 37.3949 28.846 41.8868 28.846H64.1736C70.5183 28.846 73.9677 31.0145 73.9677 35.4289C73.9677 39.8434 70.5123 42.0953 64.1736 42.0953H51.9728H41.7617C40.5047 42.0775 39.3251 41.7796 38.2706 41.2553C37.1506 40.7132 36.1855 39.9149 35.4289 38.9438C34.3804 37.5855 33.7489 35.8877 33.7489 34.0468V31.0979C33.7489 26.6477 30.103 23.0494 25.6111 23.0494C21.1192 23.0494 17.4732 26.6536 17.4732 31.0979V34.0468C17.4732 35.8877 16.8477 37.5855 15.7932 38.9438C15.0366 39.9149 14.0715 40.7132 12.9515 41.2553C11.897 41.7796 10.7174 42.0775 9.46043 42.0953H0V51.5736H12.7251C12.7251 51.5736 12.737 51.5736 12.743 51.5736C15.3583 51.5736 17.4792 49.4528 17.4792 46.8375V44.24C17.4851 44.1089 17.4911 43.9779 17.4911 43.8468V39.486C17.4911 35.0357 21.137 31.4315 25.6289 31.4315C30.1209 31.4315 33.7668 35.0357 33.7668 39.486V43.8468C33.7668 43.9779 33.7728 44.1089 33.7787 44.24V46.8375C33.7787 49.4528 35.8996 51.5736 38.5149 51.5736C38.5149 51.5736 38.5268 51.5736 38.5328 51.5736H51.2579H66.4315C73.1753 51.5736 76.7915 53.8196 76.7915 58.5617C76.7915 63.3038 73.1753 65.4664 66.4315 65.4664H41.8928C40.5881 65.4664 39.3668 65.1626 38.2766 64.6264C37.1566 64.0783 36.1915 63.286 35.4349 62.3149C34.3864 60.9566 33.7549 59.2587 33.7549 57.4179V54.4689C33.7549 50.0187 30.1089 46.4145 25.623 46.4145C21.137 46.4145 17.4911 50.0187 17.4911 54.4689V57.4179C17.4911 59.2587 16.8655 60.9566 15.8111 62.3149C15.0545 63.286 14.0894 64.0843 12.9694 64.6264C11.8792 65.1626 10.6579 65.4664 9.35319 65.4664H0V75.2604H9.35915C13.857 75.2604 17.4911 71.6502 17.4911 67.206V62.8451C17.4911 58.3949 21.137 54.7906 25.623 54.7906C30.1089 54.7906 33.7549 58.3949 33.7549 62.8451V67.206C33.7549 71.6502 37.3949 75.2604 41.8868 75.2604H67.3906C82.1651 75.2604 89.8741 69.6366 89.8741 59.926C89.8741 52.8604 85.8587 48.0409 79.5975 46.0332H79.6034Z';
9
+
10
+ export interface BaconLogoProps {
11
+ /** Rendered height in px; width follows the mark's aspect ratio. Defaults to 40. */
12
+ height?: number;
13
+ /** Mark color. Defaults to the Bacon red. */
14
+ color?: string;
15
+ }
16
+
17
+ /**
18
+ * The Bacon POS brand mark (the red "B"). Rendered at its natural aspect ratio
19
+ * and not tinted by default — the brand color is preserved. Used as the default
20
+ * logo in `Sidebar`.
21
+ */
22
+ export function BaconLogo({ height = 40, color = '#FF003A' }: BaconLogoProps) {
23
+ const xml = `<svg viewBox="0 0 ${VIEWBOX_W} ${VIEWBOX_H}" xmlns="http://www.w3.org/2000/svg"><path d="${PATH}" fill="${color}"/></svg>`;
24
+ return <SvgXml xml={xml} width={height * ASPECT} height={height} />;
25
+ }
@@ -1,3 +1,4 @@
1
1
  export { Logo, type LogoProps, type LogoName } from './Logo';
2
+ export { BaconLogo, type BaconLogoProps } from './BaconLogo';
2
3
  export { Flag, type FlagProps, type FlagCountry } from './Flag';
3
4
  export { graphics, type GraphicName, type Graphic } from './registry';
@@ -9,6 +9,7 @@ export * from './CodeField';
9
9
  export * from './CodeInput';
10
10
  export * from './Dialog';
11
11
  export * from './Dropdown';
12
+ export * from './EmptyState';
12
13
  export * from './Icon';
13
14
  export * from './IconButton';
14
15
  export * from './Input';
@@ -20,9 +21,11 @@ export * from './ProgressBar';
20
21
  export * from './QuantityStepper';
21
22
  export * from './Radio';
22
23
  export * from './Select';
24
+ export * from './Sidebar';
23
25
  export * from './Spinner';
24
26
  export * from './Surface';
25
27
  export * from './Switch';
28
+ export * from './Table';
26
29
  export * from './Text';
27
30
  export * from './Toast';
28
31
  export * from './UserChip';