@rovula/ui 0.1.28 → 0.1.30

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 (65) hide show
  1. package/dist/cjs/bundle.css +522 -67
  2. package/dist/cjs/bundle.js +589 -589
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/DataTable/DataTable.d.ts +195 -4
  5. package/dist/cjs/types/components/DataTable/DataTable.editing.d.ts +20 -0
  6. package/dist/cjs/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
  7. package/dist/cjs/types/components/DataTable/DataTable.stories.d.ts +294 -6
  8. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +22 -0
  9. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  10. package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +3 -3
  11. package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
  12. package/dist/cjs/types/components/Table/Table.d.ts +33 -3
  13. package/dist/cjs/types/components/Table/Table.stories.d.ts +86 -4
  14. package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +8 -0
  15. package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +1 -0
  16. package/dist/components/DataTable/DataTable.editing.js +385 -0
  17. package/dist/components/DataTable/DataTable.editing.types.js +1 -0
  18. package/dist/components/DataTable/DataTable.js +993 -50
  19. package/dist/components/DataTable/DataTable.stories.js +1137 -25
  20. package/dist/components/Dropdown/Dropdown.js +8 -6
  21. package/dist/components/ScrollArea/ScrollArea.js +2 -2
  22. package/dist/components/ScrollArea/ScrollArea.stories.js +68 -2
  23. package/dist/components/Table/Table.js +103 -13
  24. package/dist/components/Table/Table.stories.js +226 -9
  25. package/dist/components/TextInput/TextInput.js +6 -4
  26. package/dist/components/TextInput/TextInput.stories.js +8 -0
  27. package/dist/components/TextInput/TextInput.styles.js +7 -1
  28. package/dist/esm/bundle.css +522 -67
  29. package/dist/esm/bundle.js +1545 -1545
  30. package/dist/esm/bundle.js.map +1 -1
  31. package/dist/esm/types/components/DataTable/DataTable.d.ts +195 -4
  32. package/dist/esm/types/components/DataTable/DataTable.editing.d.ts +20 -0
  33. package/dist/esm/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
  34. package/dist/esm/types/components/DataTable/DataTable.stories.d.ts +294 -6
  35. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +22 -0
  36. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  37. package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +3 -3
  38. package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
  39. package/dist/esm/types/components/Table/Table.d.ts +33 -3
  40. package/dist/esm/types/components/Table/Table.stories.d.ts +86 -4
  41. package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +8 -0
  42. package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +1 -0
  43. package/dist/index.d.ts +493 -122
  44. package/dist/src/theme/global.css +775 -96
  45. package/package.json +14 -2
  46. package/src/components/DataTable/DataTable.editing.tsx +861 -0
  47. package/src/components/DataTable/DataTable.editing.types.ts +192 -0
  48. package/src/components/DataTable/DataTable.stories.tsx +2310 -31
  49. package/src/components/DataTable/DataTable.test.tsx +696 -0
  50. package/src/components/DataTable/DataTable.tsx +2275 -94
  51. package/src/components/Dropdown/Dropdown.tsx +22 -6
  52. package/src/components/ScrollArea/ScrollArea.stories.tsx +146 -3
  53. package/src/components/ScrollArea/ScrollArea.tsx +6 -6
  54. package/src/components/Table/Table.stories.tsx +789 -44
  55. package/src/components/Table/Table.tsx +306 -28
  56. package/src/components/TextInput/TextInput.stories.tsx +80 -0
  57. package/src/components/TextInput/TextInput.styles.ts +7 -1
  58. package/src/components/TextInput/TextInput.tsx +21 -14
  59. package/src/test/setup.ts +50 -0
  60. package/src/theme/global.css +81 -42
  61. package/src/theme/presets/colors.js +12 -0
  62. package/src/theme/themes/variable.css +27 -28
  63. package/src/theme/tokens/baseline.css +2 -1
  64. package/src/theme/tokens/components/scrollbar.css +9 -4
  65. package/src/theme/tokens/components/table.css +63 -0
@@ -3,10 +3,11 @@ import type { Meta, StoryObj } from "@storybook/react";
3
3
  import {
4
4
  Table,
5
5
  TableBody,
6
- TableCaption,
7
6
  TableCell,
7
+ TableFooter,
8
8
  TableHead,
9
9
  TableHeader,
10
+ TablePagination,
10
11
  TableRow,
11
12
  } from "./Table";
12
13
 
@@ -14,12 +15,17 @@ const meta = {
14
15
  title: "Components/Table",
15
16
  component: Table,
16
17
  tags: ["autodocs"],
17
- parameters: {
18
- layout: "fullscreen",
18
+ parameters: { layout: "fullscreen" },
19
+ argTypes: {
20
+ bordered: {
21
+ control: "boolean",
22
+ description:
23
+ "Outer `rounded-md` + `border-table-c-border`, inner scroll (`Table.tsx`).",
24
+ },
19
25
  },
20
26
  decorators: [
21
27
  (Story) => (
22
- <div className="p-5 flex w-full">
28
+ <div className="p-8 flex flex-col gap-10 bg-page-bg-main min-h-screen w-full">
23
29
  <Story />
24
30
  </div>
25
31
  ),
@@ -28,52 +34,791 @@ const meta = {
28
34
 
29
35
  export default meta;
30
36
 
31
- export const Default = {
32
- args: {},
33
- render: (args) => {
34
- const props: typeof args = {
35
- ...args,
36
- };
37
+ const rows = [
38
+ { id: "INV001", status: "Paid", method: "Credit Card", amount: "$250.00" },
39
+ { id: "INV002", status: "Pending", method: "PayPal", amount: "$150.00" },
40
+ { id: "INV003", status: "Paid", method: "Bank Transfer", amount: "$350.00" },
41
+ {
42
+ id: "INV004",
43
+ status: "Cancelled",
44
+ method: "Credit Card",
45
+ amount: "$450.00",
46
+ },
47
+ { id: "INV005", status: "Pending", method: "PayPal", amount: "$90.00" },
48
+ ];
49
+
50
+ // ── Shared helpers ────────────────────────────────────────────────────────────
51
+
52
+ const Headers = ({ colDivided = false }: { colDivided?: boolean }) => (
53
+ <TableHeader>
54
+ {/* divided={false}: TableHeader already applies border-b via [&_tr>th]:border-b */}
55
+ <TableRow divided={false} colDivided={colDivided}>
56
+ <TableHead className="w-[120px]">Invoice</TableHead>
57
+ <TableHead>Status</TableHead>
58
+ <TableHead>Method</TableHead>
59
+ <TableHead className="text-right">Amount</TableHead>
60
+ </TableRow>
61
+ </TableHeader>
62
+ );
63
+
64
+ const Rows = ({
65
+ divided = true,
66
+ colDivided = false,
67
+ selectedIndex,
68
+ }: {
69
+ divided?: boolean;
70
+ colDivided?: boolean;
71
+ selectedIndex?: number;
72
+ }) => (
73
+ <>
74
+ {rows.map((r, i) => (
75
+ <TableRow
76
+ key={r.id}
77
+ divided={divided}
78
+ colDivided={colDivided}
79
+ data-state={i === selectedIndex ? "selected" : undefined}
80
+ >
81
+ <TableCell className="font-medium">{r.id}</TableCell>
82
+ <TableCell>{r.status}</TableCell>
83
+ <TableCell>{r.method}</TableCell>
84
+ <TableCell className="text-right">{r.amount}</TableCell>
85
+ </TableRow>
86
+ ))}
87
+ </>
88
+ );
89
+
90
+ // ── Stories ───────────────────────────────────────────────────────────────────
91
+
92
+ /** NON-STRIPED — horizontal row strokes, no column dividers */
93
+ export const Default: StoryObj = {
94
+ render: () => (
95
+ <Table>
96
+ <Headers />
97
+ <TableBody>
98
+ <Rows />
99
+ </TableBody>
100
+ </Table>
101
+ ),
102
+ };
103
+
104
+ /** NON-STRIPED + COL DIVIDED — horizontal row strokes + vertical column dividers */
105
+ export const NonStripedDivided: StoryObj = {
106
+ render: () => (
107
+ <Table bordered>
108
+ <Headers colDivided />
109
+ <TableBody>
110
+ <Rows colDivided />
111
+ </TableBody>
112
+ </Table>
113
+ ),
114
+ };
115
+
116
+ /** STRIPED — alternating bg-a / bg-b, NO row strokes, NO column dividers */
117
+ export const Striped: StoryObj = {
118
+ render: () => (
119
+ <Table bordered>
120
+ <Headers />
121
+ <TableBody striped>
122
+ <Rows divided={false} />
123
+ </TableBody>
124
+ </Table>
125
+ ),
126
+ };
127
+
128
+ /**
129
+ * STRIPED + COL DIVIDED — alternating bg, NO row strokes, WITH column dividers.
130
+ * Primary Xspector table style.
131
+ */
132
+ export const StripedAndDivided: StoryObj = {
133
+ render: () => (
134
+ <Table bordered>
135
+ <Headers colDivided />
136
+ <TableBody striped>
137
+ <Rows divided={false} colDivided />
138
+ </TableBody>
139
+ </Table>
140
+ ),
141
+ };
142
+
143
+ /** SELECTED ROW — data-[state=selected] applies transparent-primary-12. Row 1 pre-selected. */
144
+ export const WithSelectedRow: StoryObj = {
145
+ render: () => (
146
+ <Table bordered>
147
+ <Headers colDivided />
148
+ <TableBody striped>
149
+ <Rows divided={false} colDivided selectedIndex={1} />
150
+ </TableBody>
151
+ </Table>
152
+ ),
153
+ };
154
+
155
+ /**
156
+ * WITH FOOTER — tfoot summary row with bg-table-bg-main + top border.
157
+ * TableFooter is inside Table so bordered works naturally.
158
+ */
159
+ export const WithFooter: StoryObj = {
160
+ render: () => (
161
+ <Table bordered>
162
+ <Headers colDivided />
163
+ <TableBody>
164
+ <Rows colDivided />
165
+ </TableBody>
166
+ <TableFooter>
167
+ <TableRow divided={false} colDivided>
168
+ <TableCell className="font-medium">5 invoices</TableCell>
169
+ <TableCell />
170
+ <TableCell />
171
+ <TableCell className="text-right font-medium">$1,290.00</TableCell>
172
+ </TableRow>
173
+ </TableFooter>
174
+ </Table>
175
+ ),
176
+ };
177
+
178
+ /**
179
+ * WITH PAGINATION — TablePagination is a sibling of Table so needs an explicit
180
+ * outer wrapper (not covered by Table's bordered prop).
181
+ */
182
+ export const WithPagination: StoryObj = {
183
+ render: () => {
184
+ const [pageIndex, setPageIndex] = React.useState(0);
185
+ const [pageSize, setPageSize] = React.useState(2);
186
+ const paged = rows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
37
187
  return (
38
- <div className="flex flex-row gap-4 w-full">
188
+ // Explicit outer wrapper — required because TablePagination is a sibling of Table
189
+ <div className="rounded-md overflow-hidden border border-table-c-border w-full">
39
190
  <Table>
40
- <TableCaption>A list of your recent invoices.</TableCaption>
41
- <TableHeader>
42
- <TableRow>
43
- <TableHead className="w-[100px]">Invoice</TableHead>
44
- <TableHead>Status</TableHead>
45
- <TableHead>Method</TableHead>
46
- <TableHead className="text-right">Amount</TableHead>
47
- </TableRow>
48
- </TableHeader>
191
+ <Headers colDivided />
49
192
  <TableBody>
50
- <TableRow>
51
- <TableCell className="font-medium">INV001</TableCell>
52
- <TableCell>Paid</TableCell>
53
- <TableCell>Credit Card</TableCell>
54
- <TableCell className="text-right">$250.00</TableCell>
55
- </TableRow>
56
- <TableRow>
57
- <TableCell className="font-medium">INV001</TableCell>
58
- <TableCell>Paid</TableCell>
59
- <TableCell>Credit Card</TableCell>
60
- <TableCell className="text-right">$250.00</TableCell>
61
- </TableRow>
62
- <TableRow>
63
- <TableCell className="font-medium">INV001</TableCell>
64
- <TableCell>Paid</TableCell>
65
- <TableCell>Credit Card</TableCell>
66
- <TableCell className="text-right">$250.00</TableCell>
67
- </TableRow>
68
- <TableRow>
69
- <TableCell className="font-medium">INV001</TableCell>
70
- <TableCell>Paid</TableCell>
71
- <TableCell>Credit Card</TableCell>
72
- <TableCell className="text-right">$250.00</TableCell>
193
+ {paged.map((r) => (
194
+ <TableRow key={r.id} divided={true} colDivided>
195
+ <TableCell className="font-medium">{r.id}</TableCell>
196
+ <TableCell>{r.status}</TableCell>
197
+ <TableCell>{r.method}</TableCell>
198
+ <TableCell className="text-right">{r.amount}</TableCell>
199
+ </TableRow>
200
+ ))}
201
+ </TableBody>
202
+ </Table>
203
+ <TablePagination
204
+ pageIndex={pageIndex}
205
+ pageSize={pageSize}
206
+ totalCount={rows.length}
207
+ pageSizeOptions={[2, 3, 5]}
208
+ onPageChange={setPageIndex}
209
+ onPageSizeChange={(s) => {
210
+ setPageSize(s);
211
+ setPageIndex(0);
212
+ }}
213
+ />
214
+ </div>
215
+ );
216
+ },
217
+ };
218
+
219
+ /** EMPTY STATE — single full-width row with centered message */
220
+ export const Empty: StoryObj = {
221
+ render: () => (
222
+ <Table bordered>
223
+ <Headers colDivided />
224
+ <TableBody>
225
+ <TableRow divided={false} className="hover:bg-transparent">
226
+ <TableCell
227
+ colSpan={4}
228
+ className="h-32 text-center text-text-g-contrast-medium"
229
+ >
230
+ No data found.
231
+ </TableCell>
232
+ </TableRow>
233
+ </TableBody>
234
+ </Table>
235
+ ),
236
+ };
237
+
238
+ /** ALL STATES SIDE-BY-SIDE — reference sheet for design review */
239
+ export const AllStates: StoryObj = {
240
+ render: () => (
241
+ <div className="flex flex-col gap-6 w-full">
242
+ <p className="typography-subtitle4 text-text-g-contrast-high">
243
+ Non-striped — row stroke only
244
+ </p>
245
+ <Table bordered>
246
+ <Headers />
247
+ <TableBody>
248
+ <Rows />
249
+ </TableBody>
250
+ </Table>
251
+
252
+ <p className="typography-subtitle4 text-text-g-contrast-high">
253
+ Non-striped + col divided — row stroke + column dividers
254
+ </p>
255
+ <Table bordered>
256
+ <Headers colDivided />
257
+ <TableBody>
258
+ <Rows colDivided />
259
+ </TableBody>
260
+ </Table>
261
+
262
+ <p className="typography-subtitle4 text-text-g-contrast-high">
263
+ Striped — alternating bg, no row stroke, no column dividers
264
+ </p>
265
+ <Table bordered>
266
+ <Headers />
267
+ <TableBody striped>
268
+ <Rows divided={false} />
269
+ </TableBody>
270
+ </Table>
271
+
272
+ <p className="typography-subtitle4 text-text-g-contrast-high">
273
+ Striped + col divided — alternating bg, no row stroke, column dividers
274
+ (primary style)
275
+ </p>
276
+ <Table bordered>
277
+ <Headers colDivided />
278
+ <TableBody striped>
279
+ <Rows divided={false} colDivided />
280
+ </TableBody>
281
+ </Table>
282
+
283
+ <p className="typography-subtitle4 text-text-g-contrast-high">
284
+ Selected row (index 2) — transparent-primary-12 overlay
285
+ </p>
286
+ <Table bordered>
287
+ <Headers colDivided />
288
+ <TableBody striped>
289
+ <Rows divided={false} colDivided selectedIndex={2} />
290
+ </TableBody>
291
+ </Table>
292
+
293
+ <p className="typography-subtitle4 text-text-g-contrast-high">
294
+ With footer — summary row in tfoot
295
+ </p>
296
+ <Table bordered>
297
+ <Headers colDivided />
298
+ <TableBody>
299
+ <Rows colDivided />
300
+ </TableBody>
301
+ <TableFooter>
302
+ <TableRow divided={false} colDivided>
303
+ <TableCell className="font-medium">5 invoices</TableCell>
304
+ <TableCell />
305
+ <TableCell />
306
+ <TableCell className="text-right font-medium">$1,290.00</TableCell>
307
+ </TableRow>
308
+ </TableFooter>
309
+ </Table>
310
+ </div>
311
+ ),
312
+ };
313
+
314
+ // ── Scroll ────────────────────────────────────────────────────────────────────
315
+
316
+ const scrollCols = [
317
+ "ID",
318
+ "Column name",
319
+ "Category",
320
+ "Data type",
321
+ "Format",
322
+ "Source",
323
+ "Default value",
324
+ "Required",
325
+ "Nullable",
326
+ "Min length",
327
+ "Max length",
328
+ "Pattern",
329
+ "Unit",
330
+ "Precision",
331
+ "Encoding",
332
+ "Description",
333
+ ];
334
+
335
+ const scrollRows = Array.from({ length: 40 }, (_, i) => [
336
+ `COL-${String(i + 1).padStart(3, "0")}`,
337
+ `column_field_${i + 1}`,
338
+ ["Identifier", "Asset", "Timestamp", "Status", "User", "Metric", "Tag"][
339
+ i % 7
340
+ ],
341
+ ["String", "Number", "Date", "Boolean", "Enum", "Array", "Object"][i % 7],
342
+ ["UUID", "Free text", "ISO 8601", "true/false", "1–5", "JSON", "Base64"][
343
+ i % 7
344
+ ],
345
+ ["System", "Manual", "Device", "Auth", "Sensor", "Import", "Computed"][i % 7],
346
+ ["-", "auto", "now()", "true", "0", "[]", "{}"][i % 7],
347
+ i % 3 === 0 ? "Yes" : "No",
348
+ i % 2 === 0 ? "Yes" : "No",
349
+ ["-", "1", "3", "8", "16", "36", "64"][i % 7],
350
+ ["255", "128", "1024", "36", "64", "512", "-"][i % 7],
351
+ ["-", "^[A-Z]+$", "^\\d+$", ".*", "^[a-z_]+$", "-", "-"][i % 7],
352
+ ["m", "kg", "°C", "-", "%", "px", "ms"][i % 7],
353
+ ["-", "2", "4", "6", "0", "3", "8"][i % 7],
354
+ ["UTF-8", "ASCII", "Base64", "UTF-16", "-", "UTF-8", "UTF-8"][i % 7],
355
+ `Description text for column ${i + 1}`,
356
+ ]);
357
+
358
+ /**
359
+ * WITH SCROLL (Figma 9637-10817)
360
+ *
361
+ * Demonstrates both axes of overflow:
362
+ * - Vertical: fixed-height container → body rows overflow and scroll vertically
363
+ * - Horizontal: 10 columns wider than the viewport → native h-scroll bar appears
364
+ *
365
+ * Implementation notes:
366
+ * - Outer div sets the fixed height and owns the rounded border
367
+ * (TablePagination must sit outside the scrollable Table)
368
+ * - Table gets rootClassName="flex-1 min-h-0" so it grows to fill flex space
369
+ * and allows overflow-auto to kick in
370
+ * - TableHeader gets className="sticky top-0 z-10" for a frozen header row
371
+ */
372
+ export const WithScroll: StoryObj = {
373
+ render: () => {
374
+ const [pageIndex, setPageIndex] = React.useState(0);
375
+ const [pageSize, setPageSize] = React.useState(5);
376
+ const totalCount = scrollRows.length;
377
+ const paged = scrollRows.slice(
378
+ pageIndex * pageSize,
379
+ (pageIndex + 1) * pageSize,
380
+ );
381
+
382
+ return (
383
+ // Outer wrapper: fixed height + border owns the rounded corners.
384
+ // TablePagination lives here as a sibling (outside the scroll area).
385
+ <div className="flex flex-col h-[320px] w-full">
386
+ {/* Table fills remaining height; overflow-auto on rootDiv handles both scroll axes */}
387
+ <Table rootClassName="flex-1 min-h-0">
388
+ {/* Sticky header — stays visible during vertical scroll */}
389
+ <TableHeader className="sticky top-0 z-10">
390
+ <TableRow divided={false} colDivided>
391
+ {scrollCols.map((col) => (
392
+ <TableHead key={col} className="whitespace-nowrap">
393
+ {col}
394
+ </TableHead>
395
+ ))}
73
396
  </TableRow>
397
+ </TableHeader>
398
+
399
+ <TableBody striped>
400
+ {paged.map((cells, i) => (
401
+ <TableRow key={i} divided={false} colDivided>
402
+ {cells.map((cell, j) => (
403
+ <TableCell key={j} className="whitespace-nowrap">
404
+ {cell}
405
+ </TableCell>
406
+ ))}
407
+ </TableRow>
408
+ ))}
74
409
  </TableBody>
75
410
  </Table>
411
+
412
+ {/* Pagination sits outside the scroll container — always visible */}
413
+ <TablePagination
414
+ pageIndex={pageIndex}
415
+ pageSize={pageSize}
416
+ totalCount={totalCount}
417
+ pageSizeOptions={[5, 10, 20]}
418
+ onPageChange={setPageIndex}
419
+ onPageSizeChange={(s) => {
420
+ setPageSize(s);
421
+ setPageIndex(0);
422
+ }}
423
+ />
76
424
  </div>
77
425
  );
78
426
  },
79
- } satisfies StoryObj;
427
+ };
428
+
429
+ /**
430
+ * SCROLLBAR SIZES — per-axis demo
431
+ *
432
+ * Combine ui-scrollbar-x-{size} + ui-scrollbar-y-{size} independently.
433
+ * CSS: height → horizontal bar thickness | width → vertical bar thickness
434
+ */
435
+ export const ScrollbarSizes: StoryObj = {
436
+ render: () => {
437
+ const makeTable = (label: string, xClass: string, yClass: string) => (
438
+ <div key={label} className="flex flex-col gap-2">
439
+ <p className="typography-subtitle4 text-text-g-contrast-high">
440
+ {label}
441
+ </p>
442
+ <div className="rounded-xl overflow-hidden border border-table-bg-line h-[180px] flex flex-col">
443
+ <Table
444
+ rootClassName={`flex-1 min-h-0 ui-scrollbar ${xClass} ${yClass}`}
445
+ >
446
+ <TableHeader className="sticky top-0 z-10">
447
+ <TableRow divided={false} colDivided>
448
+ {scrollCols.map((c) => (
449
+ <TableHead key={c} className="whitespace-nowrap">
450
+ {c}
451
+ </TableHead>
452
+ ))}
453
+ </TableRow>
454
+ </TableHeader>
455
+ <TableBody striped>
456
+ {scrollRows.slice(0, 12).map((cells, i) => (
457
+ <TableRow key={i} divided={false} colDivided>
458
+ {cells.map((cell, j) => (
459
+ <TableCell key={j} className="whitespace-nowrap">
460
+ {cell}
461
+ </TableCell>
462
+ ))}
463
+ </TableRow>
464
+ ))}
465
+ </TableBody>
466
+ </Table>
467
+ </div>
468
+ </div>
469
+ );
470
+
471
+ return (
472
+ <div className="flex flex-col gap-8 w-full">
473
+ {makeTable(
474
+ "X = M (12px) + Y = S (6px) ← Table default",
475
+ "ui-scrollbar-x-m",
476
+ "ui-scrollbar-y-s",
477
+ )}
478
+ {makeTable(
479
+ "X = S (6px) + Y = M (12px)",
480
+ "ui-scrollbar-x-s",
481
+ "ui-scrollbar-y-m",
482
+ )}
483
+ {makeTable(
484
+ "X = M (12px) + Y = M (12px)",
485
+ "ui-scrollbar-x-m",
486
+ "ui-scrollbar-y-m",
487
+ )}
488
+ {makeTable(
489
+ "X = S (6px) + Y = S (6px)",
490
+ "ui-scrollbar-x-s",
491
+ "ui-scrollbar-y-s",
492
+ )}
493
+ {makeTable(
494
+ "X = XS (2px) + Y = XS (2px)",
495
+ "ui-scrollbar-x-xs",
496
+ "ui-scrollbar-y-xs",
497
+ )}
498
+ {makeTable(
499
+ "X = M (12px) + Y = XS (2px)",
500
+ "ui-scrollbar-x-m",
501
+ "ui-scrollbar-y-xs",
502
+ )}
503
+ </div>
504
+ );
505
+ },
506
+ };
507
+
508
+ // ── Panel (on Modal / Drawer / Panel surface) ─────────────────────────────────
509
+ //
510
+ // The Panel wrapper carries data-surface="panel" which overrides --table-c-*
511
+ // tokens via CSS cascade — no variant prop on Table or TablePagination needed.
512
+ // Every story below mirrors its non-panel counterpart exactly.
513
+
514
+ /** Simulates a Modal / Drawer / Panel surface wrapper.
515
+ * data-surface="panel" triggers the CSS token overrides in table.css. */
516
+ const PanelDecorator = ({ children }: { children: React.ReactNode }) => (
517
+ <div
518
+ data-surface="panel"
519
+ className=" bg-modal-surface p-6 w-full max-w-3xl mx-auto"
520
+ >
521
+ <p className="typography-subtitle4 text-text-g-contrast-medium mb-4">
522
+ Modal / Panel surface
523
+ </p>
524
+ {children}
525
+ </div>
526
+ );
527
+
528
+ /** PANEL DEFAULT — transparent rows + panel-sub-line row dividers */
529
+ export const PanelDefault: StoryObj = {
530
+ render: () => (
531
+ <PanelDecorator>
532
+ <Table>
533
+ <Headers />
534
+ <TableBody>
535
+ <Rows />
536
+ </TableBody>
537
+ </Table>
538
+ </PanelDecorator>
539
+ ),
540
+ };
541
+
542
+ /** PANEL NON-STRIPED DIVIDED — row dividers + vertical column dividers */
543
+ export const PanelNonStripedDivided: StoryObj = {
544
+ render: () => (
545
+ <PanelDecorator>
546
+ <Table>
547
+ <Headers colDivided />
548
+ <TableBody>
549
+ <Rows colDivided />
550
+ </TableBody>
551
+ </Table>
552
+ </PanelDecorator>
553
+ ),
554
+ };
555
+
556
+ /** PANEL STRIPED — alternating rows (both transparent in panel mode), no row strokes */
557
+ export const PanelStriped: StoryObj = {
558
+ render: () => (
559
+ <PanelDecorator>
560
+ <Table bordered>
561
+ <Headers />
562
+ <TableBody striped>
563
+ <Rows divided={false} />
564
+ </TableBody>
565
+ </Table>
566
+ </PanelDecorator>
567
+ ),
568
+ };
569
+
570
+ /** PANEL STRIPED + COL DIVIDED */
571
+ export const PanelStripedAndDivided: StoryObj = {
572
+ render: () => (
573
+ <PanelDecorator>
574
+ <Table bordered>
575
+ <Headers colDivided />
576
+ <TableBody striped>
577
+ <Rows divided={false} colDivided />
578
+ </TableBody>
579
+ </Table>
580
+ </PanelDecorator>
581
+ ),
582
+ };
583
+
584
+ /** PANEL SELECTED ROW — table-panel-selected (yellow/olive) */
585
+ export const PanelWithSelectedRow: StoryObj = {
586
+ render: () => (
587
+ <PanelDecorator>
588
+ <Table bordered>
589
+ <Headers colDivided />
590
+ <TableBody striped>
591
+ <Rows divided={false} colDivided selectedIndex={1} />
592
+ </TableBody>
593
+ </Table>
594
+ </PanelDecorator>
595
+ ),
596
+ };
597
+
598
+ /** PANEL WITH FOOTER */
599
+ export const PanelWithFooter: StoryObj = {
600
+ render: () => (
601
+ <PanelDecorator>
602
+ <Table bordered>
603
+ <Headers colDivided />
604
+ <TableBody>
605
+ <Rows colDivided />
606
+ </TableBody>
607
+ <TableFooter>
608
+ <TableRow divided={false} colDivided>
609
+ <TableCell className="font-medium">5 invoices</TableCell>
610
+ <TableCell />
611
+ <TableCell />
612
+ <TableCell className="text-right font-medium">$1,290.00</TableCell>
613
+ </TableRow>
614
+ </TableFooter>
615
+ </Table>
616
+ </PanelDecorator>
617
+ ),
618
+ };
619
+
620
+ /**
621
+ * PANEL WITH PAGINATION
622
+ * TablePagination sits inside the same data-surface="panel" scope so it
623
+ * automatically picks up the panel tokens — no variant prop needed.
624
+ */
625
+ export const PanelWithPagination: StoryObj = {
626
+ render: () => {
627
+ const [pageIndex, setPageIndex] = React.useState(0);
628
+ const [pageSize, setPageSize] = React.useState(3);
629
+ const paged = rows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
630
+ return (
631
+ <PanelDecorator>
632
+ <div className="rounded-lg overflow-hidden border border-table-c-border w-full">
633
+ <Table>
634
+ <Headers colDivided />
635
+ <TableBody>
636
+ {paged.map((r) => (
637
+ <TableRow key={r.id} divided colDivided>
638
+ <TableCell className="font-medium">{r.id}</TableCell>
639
+ <TableCell>{r.status}</TableCell>
640
+ <TableCell>{r.method}</TableCell>
641
+ <TableCell className="text-right">{r.amount}</TableCell>
642
+ </TableRow>
643
+ ))}
644
+ </TableBody>
645
+ </Table>
646
+ <TablePagination
647
+ pageIndex={pageIndex}
648
+ pageSize={pageSize}
649
+ totalCount={rows.length}
650
+ pageSizeOptions={[2, 3, 5]}
651
+ onPageChange={setPageIndex}
652
+ onPageSizeChange={(s) => {
653
+ setPageSize(s);
654
+ setPageIndex(0);
655
+ }}
656
+ />
657
+ </div>
658
+ </PanelDecorator>
659
+ );
660
+ },
661
+ };
662
+
663
+ /** PANEL EMPTY STATE */
664
+ export const PanelEmpty: StoryObj = {
665
+ render: () => (
666
+ <PanelDecorator>
667
+ <Table bordered>
668
+ <Headers colDivided />
669
+ <TableBody>
670
+ <TableRow divided={false} className="hover:bg-transparent">
671
+ <TableCell
672
+ colSpan={4}
673
+ className="h-32 text-center text-text-g-contrast-medium"
674
+ >
675
+ No data found.
676
+ </TableCell>
677
+ </TableRow>
678
+ </TableBody>
679
+ </Table>
680
+ </PanelDecorator>
681
+ ),
682
+ };
683
+
684
+ /**
685
+ * PANEL WITH SCROLL — sticky header + both scroll axes inside a fixed-height panel.
686
+ */
687
+ export const PanelWithScroll: StoryObj = {
688
+ render: () => {
689
+ const [pageIndex, setPageIndex] = React.useState(0);
690
+ const [pageSize, setPageSize] = React.useState(5);
691
+ const paged = scrollRows.slice(
692
+ pageIndex * pageSize,
693
+ (pageIndex + 1) * pageSize,
694
+ );
695
+ return (
696
+ <PanelDecorator>
697
+ <div className="flex flex-col h-[280px] w-full rounded-lg overflow-hidden border border-table-c-border">
698
+ <Table rootClassName="flex-1 min-h-0">
699
+ <TableHeader className="sticky top-0 z-10">
700
+ <TableRow divided={false} colDivided>
701
+ {scrollCols.map((col) => (
702
+ <TableHead key={col} className="whitespace-nowrap">
703
+ {col}
704
+ </TableHead>
705
+ ))}
706
+ </TableRow>
707
+ </TableHeader>
708
+ <TableBody>
709
+ {paged.map((cells, i) => (
710
+ <TableRow key={i} divided colDivided>
711
+ {cells.map((cell, j) => (
712
+ <TableCell key={j} className="whitespace-nowrap">
713
+ {cell}
714
+ </TableCell>
715
+ ))}
716
+ </TableRow>
717
+ ))}
718
+ </TableBody>
719
+ </Table>
720
+ <TablePagination
721
+ pageIndex={pageIndex}
722
+ pageSize={pageSize}
723
+ totalCount={scrollRows.length}
724
+ pageSizeOptions={[5, 10, 20]}
725
+ onPageChange={setPageIndex}
726
+ onPageSizeChange={(s) => {
727
+ setPageSize(s);
728
+ setPageIndex(0);
729
+ }}
730
+ />
731
+ </div>
732
+ </PanelDecorator>
733
+ );
734
+ },
735
+ };
736
+
737
+ /** PANEL ALL STATES — all variants side-by-side for design review */
738
+ export const PanelAllStates: StoryObj = {
739
+ render: () => (
740
+ <div className="flex flex-col gap-6 w-full">
741
+ <p className="typography-subtitle4 text-text-g-contrast-high">
742
+ Non-striped — row stroke only
743
+ </p>
744
+ <PanelDecorator>
745
+ <Table bordered>
746
+ <Headers />
747
+ <TableBody>
748
+ <Rows />
749
+ </TableBody>
750
+ </Table>
751
+ </PanelDecorator>
752
+
753
+ <p className="typography-subtitle4 text-text-g-contrast-high">
754
+ Non-striped + col divided
755
+ </p>
756
+ <PanelDecorator>
757
+ <Table bordered>
758
+ <Headers colDivided />
759
+ <TableBody>
760
+ <Rows colDivided />
761
+ </TableBody>
762
+ </Table>
763
+ </PanelDecorator>
764
+
765
+ <p className="typography-subtitle4 text-text-g-contrast-high">
766
+ Striped — alternating bg, no row stroke
767
+ </p>
768
+ <PanelDecorator>
769
+ <Table bordered>
770
+ <Headers />
771
+ <TableBody striped>
772
+ <Rows divided={false} />
773
+ </TableBody>
774
+ </Table>
775
+ </PanelDecorator>
776
+
777
+ <p className="typography-subtitle4 text-text-g-contrast-high">
778
+ Striped + col divided (primary style)
779
+ </p>
780
+ <PanelDecorator>
781
+ <Table bordered>
782
+ <Headers colDivided />
783
+ <TableBody striped>
784
+ <Rows divided={false} colDivided />
785
+ </TableBody>
786
+ </Table>
787
+ </PanelDecorator>
788
+
789
+ <p className="typography-subtitle4 text-text-g-contrast-high">
790
+ Selected row (index 2) — table-panel-selected overlay
791
+ </p>
792
+ <PanelDecorator>
793
+ <Table bordered>
794
+ <Headers colDivided />
795
+ <TableBody striped>
796
+ <Rows divided={false} colDivided selectedIndex={2} />
797
+ </TableBody>
798
+ </Table>
799
+ </PanelDecorator>
800
+
801
+ <p className="typography-subtitle4 text-text-g-contrast-high">
802
+ With footer
803
+ </p>
804
+ <PanelDecorator>
805
+ <Table bordered>
806
+ <Headers colDivided />
807
+ <TableBody>
808
+ <Rows colDivided />
809
+ </TableBody>
810
+ <TableFooter>
811
+ <TableRow divided={false} colDivided>
812
+ <TableCell className="font-medium">5 invoices</TableCell>
813
+ <TableCell />
814
+ <TableCell />
815
+ <TableCell className="text-right font-medium">
816
+ $1,290.00
817
+ </TableCell>
818
+ </TableRow>
819
+ </TableFooter>
820
+ </Table>
821
+ </PanelDecorator>
822
+ </div>
823
+ ),
824
+ };