@multiplatform.one/table-primitives 6.1.0 → 6.3.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.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @multiplatform.one/table-primitives
2
+
3
+ Semantic, knob-aware table primitives (`<Table>`, rows, cells) plus the table
4
+ cell context that lets form fields render chromeless inside cells. Layer 1
5
+ only: depends on theme + Tamagui — no forms, no data-table logic.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @multiplatform.one/table-primitives
11
+ ```
12
+
13
+ ## What it owns
14
+
15
+ - **`Table`** compound component — `Table.Head`, `Table.Body`, `Table.Row`,
16
+ `Table.Cell`, `Table.HeaderCell`, `Table.Foot`, `Table.Caption`
17
+ - **`TableCellContext`** / `useIsInTableCell()` / `useTableCellContext()` —
18
+ the signal `@multiplatform.one/forms` fields use to auto-detect they are
19
+ inside a cell and drop their chrome
20
+
21
+ ## What it must not do
22
+
23
+ - No data-table features (filtering, sorting, pagination, virtualization) —
24
+ that is `@multiplatform.one/table`
25
+ - No form fields, no Frappe/API assumptions
26
+
27
+ ## Usage
28
+
29
+ ```tsx
30
+ import { Table } from "@multiplatform.one/table-primitives";
31
+
32
+ export function PeopleTable() {
33
+ return (
34
+ <Table>
35
+ <Table.Head>
36
+ <Table.Row>
37
+ <Table.HeaderCell>Name</Table.HeaderCell>
38
+ <Table.HeaderCell>Role</Table.HeaderCell>
39
+ </Table.Row>
40
+ </Table.Head>
41
+ <Table.Body>
42
+ <Table.Row>
43
+ <Table.Cell>Ada Lovelace</Table.Cell>
44
+ <Table.Cell>Admin</Table.Cell>
45
+ </Table.Row>
46
+ </Table.Body>
47
+ </Table>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## Styling
53
+
54
+ Table chrome (borders, padding, radius) resolves through the knobs system in
55
+ `@multiplatform.one/theme` — flip knobs or wrap in a `<Preset>` instead of
56
+ overriding style props. See `agent-os/standards/frontend/knobs-system.md` in the
57
+ [multiplatform.one repo](https://gitlab.com/bitspur/frappe/multiplatform.one).
58
+
59
+ ## License
60
+
61
+ Apache-2.0
@@ -24,12 +24,14 @@ var index_exports = {};
24
24
  __export(index_exports, {
25
25
  Table: () => Table,
26
26
  TableCellContext: () => import_tableCellContext2.TableCellContext,
27
+ getTableCellPadding: () => getTableCellPadding,
27
28
  useIsInTableCell: () => import_tableCellContext2.useIsInTableCell,
28
29
  useTableCellContext: () => import_tableCellContext2.useTableCellContext
29
30
  });
30
31
  module.exports = __toCommonJS(index_exports);
31
32
  var import_theme = require("@multiplatform.one/theme");
32
33
  var import_tableCellContext = require("./tableCellContext.cjs");
34
+ var import_react = require("react");
33
35
  var import_tamagui = require("tamagui");
34
36
  var import_tableCellContext2 = require("./tableCellContext.cjs");
35
37
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -45,12 +47,15 @@ const TableContext = (0, import_tamagui.createStyledContext)({
45
47
  y: "center"
46
48
  },
47
49
  borderColor: "$color6",
48
- displayMode: "table"
50
+ // CSS table display values ("table-row", "table-cell", …) are no-ops in
51
+ // React Native — cells stack vertically and bare Tables are unreadable.
52
+ // Native defaults to the flex composition; web keeps semantic table layout.
53
+ displayMode: import_tamagui.isWeb ? "table" : "flex"
49
54
  });
50
55
  const Row = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
51
56
  name: "TableRow",
52
57
  ...{
53
- tag: "tr"
58
+ render: "tr"
54
59
  },
55
60
  role: "row",
56
61
  context: TableContext,
@@ -77,8 +82,9 @@ const Row = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
77
82
  const Cell = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
78
83
  name: "TableCell",
79
84
  ...{
80
- tag: "td"
85
+ render: "td"
81
86
  },
87
+ role: "cell",
82
88
  context: TableContext,
83
89
  display: "table-cell",
84
90
  // verticalAlign goes via inline style; Tamagui forwards it to the DOM as a JSX
@@ -86,10 +92,16 @@ const Cell = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
86
92
  style: {
87
93
  verticalAlign: "middle"
88
94
  },
95
+ // LC-44 ROW-LINE: cells are ROW-flex so each axis does what it says —
96
+ // justifyContent is horizontal (start by default; align-by-type overrides
97
+ // land here) and alignItems is vertical (center). The previous column
98
+ // direction put alignItems:center on the HORIZONTAL axis, centering every
99
+ // cell's text and making variable-width columns ragged (A16-02).
100
+ flexDirection: "row",
89
101
  alignItems: "center",
90
102
  justifyContent: "flex-start",
91
103
  paddingHorizontal: "$2",
92
- paddingVertical: "$1",
104
+ paddingVertical: "$3",
93
105
  overflow: "hidden",
94
106
  variants: {
95
107
  displayMode: {
@@ -129,6 +141,8 @@ const Cell = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
129
141
  };
130
142
  }
131
143
  },
144
+ // Row-flex axis map (LC-44): y → alignItems (vertical), x →
145
+ // justifyContent (horizontal).
132
146
  alignCells: val => {
133
147
  return {
134
148
  alignItems: val.y === "center" ? "center" : `flex-${val.y}`,
@@ -145,26 +159,37 @@ const Cell = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
145
159
  const HeaderCell = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
146
160
  name: "TableHeaderCell",
147
161
  ...{
148
- tag: "th",
162
+ render: "th",
149
163
  scope: "col"
150
164
  },
165
+ role: "columnheader",
151
166
  context: TableContext,
152
167
  display: "table-cell",
153
168
  // verticalAlign goes via inline style; see TableCell above for context.
169
+ // fontWeight/textAlign neutralize the UA styles a real <th> brings
170
+ // (bold + centered) so header content keeps its pre-migration look;
171
+ // logical `start` stays RTL-correct.
154
172
  style: {
155
- verticalAlign: "middle"
173
+ verticalAlign: "middle",
174
+ fontWeight: "normal",
175
+ textAlign: "start"
156
176
  },
177
+ // LC-44 ROW-LINE: row-flex like TableCell — header labels start-align on
178
+ // the same column line as body cell text.
179
+ flexDirection: "row",
157
180
  alignItems: "center",
158
181
  justifyContent: "flex-start",
159
182
  paddingHorizontal: "$2",
160
- paddingVertical: "$1",
161
- backgroundColor: "$color3",
183
+ paddingVertical: "$3",
184
+ backgroundColor: "$color2",
162
185
  variants: {
163
186
  displayMode: {
164
187
  table: {
165
188
  display: "table-cell",
166
189
  style: {
167
- verticalAlign: "middle"
190
+ verticalAlign: "middle",
191
+ fontWeight: "normal",
192
+ textAlign: "start"
168
193
  }
169
194
  },
170
195
  flex: {
@@ -187,6 +212,8 @@ const HeaderCell = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
187
212
  minWidth: tokens.size[name]
188
213
  })
189
214
  },
215
+ // Row-flex axis map (LC-44): y → alignItems (vertical), x →
216
+ // justifyContent (horizontal).
190
217
  alignHeaderCells: val => {
191
218
  return {
192
219
  alignItems: val.y === "center" ? "center" : `flex-${val.y}`,
@@ -210,8 +237,9 @@ const HeaderCell = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
210
237
  const TableBody = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
211
238
  name: "TableBody",
212
239
  ...{
213
- tag: "tbody"
240
+ render: "tbody"
214
241
  },
242
+ role: "rowgroup",
215
243
  context: TableContext,
216
244
  display: "table-row-group",
217
245
  variants: {
@@ -224,7 +252,13 @@ const TableBody = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
224
252
  flexDirection: "column",
225
253
  width: "100%",
226
254
  overflowY: "auto",
227
- flex: 1,
255
+ // basis auto (not flex:1 basis-0): in auto-height parents a basis-0
256
+ // chain resolves to 0 and the body collapses to a scrollbar sliver;
257
+ // basis auto sizes to rows there while still filling sized parents
258
+ // (grow) and scrolling (shrink + minHeight 0).
259
+ flexGrow: 1,
260
+ flexShrink: 1,
261
+ flexBasis: "auto",
228
262
  minHeight: 0
229
263
  }
230
264
  }
@@ -233,8 +267,9 @@ const TableBody = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
233
267
  const TableHead = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
234
268
  name: "TableHead",
235
269
  ...{
236
- tag: "thead"
270
+ render: "thead"
237
271
  },
272
+ role: "rowgroup",
238
273
  context: TableContext,
239
274
  display: "table-header-group",
240
275
  borderBottomWidth: 1,
@@ -256,8 +291,9 @@ const TableHead = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
256
291
  const TableFoot = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
257
292
  name: "TableFoot",
258
293
  ...{
259
- tag: "tfoot"
294
+ render: "tfoot"
260
295
  },
296
+ role: "rowgroup",
261
297
  context: TableContext,
262
298
  display: "table-footer-group",
263
299
  variants: {
@@ -277,18 +313,24 @@ const TableFoot = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
277
313
  const TableCaption = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
278
314
  name: "TableCaption",
279
315
  ...{
280
- tag: "caption"
316
+ render: "caption"
281
317
  },
282
318
  context: TableContext,
283
319
  display: "table-caption",
320
+ // Real <caption> centers its text by UA default — keep the primitive's
321
+ // pre-migration start alignment (logical, RTL-correct).
322
+ style: {
323
+ textAlign: "start"
324
+ },
284
325
  paddingVertical: "$2",
285
326
  paddingHorizontal: "$2"
286
327
  });
287
328
  const TableComp = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
288
329
  name: "Table",
289
330
  ...{
290
- tag: "table"
331
+ render: "table"
291
332
  },
333
+ role: "table",
292
334
  context: TableContext,
293
335
  display: "table",
294
336
  borderWidth: 1,
@@ -304,7 +346,10 @@ const TableComp = (0, import_tamagui.styled)(import_tamagui.ThemeableStack, {
304
346
  flex: {
305
347
  display: "flex",
306
348
  flexDirection: "column",
307
- flex: 1,
349
+ // See Body: basis auto so auto-height parents get content height.
350
+ flexGrow: 1,
351
+ flexShrink: 1,
352
+ flexBasis: "auto",
308
353
  minHeight: 0
309
354
  }
310
355
  },
@@ -330,24 +375,191 @@ function resolveIntent(props) {
330
375
  if (props.success) return "success";
331
376
  return void 0;
332
377
  }
333
- function ThemedTableComp(props) {
378
+ function getTableCellPadding(space) {
379
+ const map = {
380
+ small: {
381
+ paddingHorizontal: "$1.5",
382
+ paddingVertical: "$2"
383
+ },
384
+ medium: {
385
+ paddingHorizontal: "$2",
386
+ paddingVertical: "$3"
387
+ },
388
+ large: {
389
+ paddingHorizontal: "$3",
390
+ paddingVertical: "$4"
391
+ }
392
+ };
393
+ return map[space] ?? map.medium;
394
+ }
395
+ function flattenChildren(children) {
396
+ return import_react.Children.toArray(children).flatMap(child => {
397
+ if ((0, import_react.isValidElement)(child) && child.type === import_react.Fragment) {
398
+ return flattenChildren(child.props.children);
399
+ }
400
+ return [child];
401
+ });
402
+ }
403
+ function extractCardModel(children) {
404
+ const headerLabels = [];
405
+ const rows = [];
406
+ let caption = null;
407
+ for (const section of flattenChildren(children)) {
408
+ if (!(0, import_react.isValidElement)(section)) continue;
409
+ const sectionProps = section.props;
410
+ if (section.type === ThemedTableHead) {
411
+ for (const rowEl of flattenChildren(sectionProps.children)) {
412
+ if (!(0, import_react.isValidElement)(rowEl) || rowEl.type !== ThemedRow) continue;
413
+ flattenChildren(rowEl.props.children).forEach((cellEl, index) => {
414
+ if (!(0, import_react.isValidElement)(cellEl) || cellEl.type !== ThemedHeaderCell) return;
415
+ if (headerLabels[index] === void 0) {
416
+ headerLabels[index] = cellEl.props.children;
417
+ }
418
+ });
419
+ }
420
+ } else if (section.type === TableBody) {
421
+ for (const rowEl of flattenChildren(sectionProps.children)) {
422
+ if (!(0, import_react.isValidElement)(rowEl) || rowEl.type !== ThemedRow) continue;
423
+ const rowProps = rowEl.props;
424
+ const cells = flattenChildren(rowProps.children).filter(cellEl => (0, import_react.isValidElement)(cellEl) && cellEl.type === ThemedCell).map((cellEl, index) => ({
425
+ key: cellEl.key ?? String(index),
426
+ content: cellEl.props.children
427
+ }));
428
+ rows.push({
429
+ key: rowEl.key ?? String(rows.length),
430
+ intent: resolveIntent(rowProps),
431
+ cells
432
+ });
433
+ }
434
+ } else if (section.type === TableCaption) {
435
+ caption = sectionProps.children;
436
+ }
437
+ }
438
+ return {
439
+ headerLabels,
440
+ rows,
441
+ caption
442
+ };
443
+ }
444
+ function TableCardsList({
445
+ children,
446
+ ...props
447
+ }) {
448
+ const {
449
+ knobProps
450
+ } = (0, import_theme.useResolvedKnobs)();
451
+ const {
452
+ headerLabels,
453
+ rows,
454
+ caption
455
+ } = extractCardModel(children);
456
+ const {
457
+ cellWidth: _cellWidth,
458
+ cellHeight: _cellHeight,
459
+ alignCells: _alignCells,
460
+ alignHeaderCells: _alignHeaderCells,
461
+ displayMode: _displayMode,
462
+ style: _style,
463
+ ...viewProps
464
+ } = props;
465
+ return /* @__PURE__ */(0, import_jsx_runtime.jsxs)(import_tamagui.ThemeableStack, {
466
+ flexDirection: "column",
467
+ ...knobProps.gap,
468
+ testID: "table-cards",
469
+ ...viewProps,
470
+ children: [caption != null && (typeof caption === "string" || typeof caption === "number" ? /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.SizableText, {
471
+ fontSize: knobProps.sizeToken,
472
+ color: "$color10",
473
+ children: caption
474
+ }) : caption), rows.map(row => {
475
+ const card = /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.ThemeableStack, {
476
+ testID: "table-card",
477
+ ...knobProps.surface,
478
+ ...knobProps.cardSurface,
479
+ elevation: knobProps.elevation,
480
+ borderColor: "$color6",
481
+ ...(row.intent ? {
482
+ backgroundColor: "$color3"
483
+ } : {}),
484
+ children: row.cells.map((cell, index) => {
485
+ const label = headerLabels[index];
486
+ const value = typeof cell.content === "string" || typeof cell.content === "number" ? /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.SizableText, {
487
+ fontSize: knobProps.sizeToken,
488
+ children: cell.content
489
+ }) : cell.content;
490
+ return /* @__PURE__ */(0, import_jsx_runtime.jsxs)(import_tamagui.ThemeableStack, {
491
+ flexDirection: "row",
492
+ alignItems: "center",
493
+ gap: "$2",
494
+ paddingVertical: "$1.5",
495
+ children: [label != null && /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.SizableText, {
496
+ fontSize: knobProps.sizeToken,
497
+ fontWeight: "500",
498
+ color: "$color10",
499
+ width: "40%",
500
+ flexShrink: 0,
501
+ children: label
502
+ }), /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.ThemeableStack, {
503
+ flex: 1,
504
+ minWidth: 0,
505
+ children: /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tableCellContext.TableCellContext.Provider, {
506
+ value: {
507
+ inTableCell: true,
508
+ isHeader: false
509
+ },
510
+ children: value
511
+ })
512
+ })]
513
+ }, cell.key);
514
+ })
515
+ }, row.key);
516
+ return row.intent ? /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.Theme, {
517
+ name: row.intent,
518
+ children: card
519
+ }, row.key) : card;
520
+ })]
521
+ });
522
+ }
523
+ function ThemedTableComp({
524
+ layout = "table",
525
+ ...props
526
+ }) {
334
527
  const {
335
528
  knobProps
336
529
  } = (0, import_theme.useResolvedKnobs)();
337
- const baseBorderWidth = knobProps.borderRadius.borderWidth;
530
+ const {
531
+ sm
532
+ } = (0, import_tamagui.useMedia)();
533
+ const baseBorderWidth = knobProps.surface.borderWidth;
338
534
  const resolvedBorderWidth = knobProps.outlined ? Math.max(baseBorderWidth, 1) : baseBorderWidth;
535
+ const showCards = layout === "cards" || layout === "auto" && sm === false;
536
+ if (showCards) {
537
+ return /* @__PURE__ */(0, import_jsx_runtime.jsx)(TableCardsList, {
538
+ ...props
539
+ });
540
+ }
339
541
  return /* @__PURE__ */(0, import_jsx_runtime.jsx)(TableComp, {
340
- ...knobProps.borderRadius,
542
+ ...knobProps.surface,
543
+ ...knobProps.borderRadiusOuter,
544
+ ...(knobProps.outlined ? {
545
+ backgroundColor: "transparent"
546
+ } : null),
341
547
  borderWidth: resolvedBorderWidth,
342
- borderColor: "$color6",
343
- backgroundColor: "$background",
344
548
  elevation: knobProps.elevation,
549
+ cellHeight: knobProps.sizeToken,
345
550
  ...props,
346
551
  style: [{
347
- tableLayout: "fixed"
552
+ tableLayout: "fixed",
553
+ borderSpacing: 0
348
554
  }, props.style]
349
555
  });
350
556
  }
557
+ function ThemedTableHead(props) {
558
+ return /* @__PURE__ */(0, import_jsx_runtime.jsx)(TableHead, {
559
+ ...import_theme.hairline.bottom,
560
+ ...props
561
+ });
562
+ }
351
563
  function ThemedRow({
352
564
  accent,
353
565
  error,
@@ -363,13 +575,20 @@ function ThemedRow({
363
575
  });
364
576
  const row = /* @__PURE__ */(0, import_jsx_runtime.jsx)(Row, {
365
577
  ...(intent ? {
366
- backgroundColor: "$color3"
578
+ backgroundColor: "$color3",
579
+ // `color: $color` mirrors the inline color the Theme span used
580
+ // to set, so unstyled text in cells inherits the intent color.
581
+ color: "$color",
582
+ ...(import_tamagui.isWeb ? {
583
+ className: `t_${intent}`
584
+ } : {})
367
585
  } : {}),
368
586
  ...props
369
587
  });
370
588
  if (intent) {
371
589
  return /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.Theme, {
372
590
  name: intent,
591
+ forceClassName: false,
373
592
  children: row
374
593
  });
375
594
  }
@@ -377,36 +596,63 @@ function ThemedRow({
377
596
  }
378
597
  function ThemedCell({
379
598
  children,
599
+ style,
380
600
  ...props
381
601
  }) {
602
+ const {
603
+ knobProps
604
+ } = (0, import_theme.useResolvedKnobs)();
605
+ const content = typeof children === "string" || typeof children === "number" ? /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.SizableText, {
606
+ fontSize: knobProps.sizeToken,
607
+ children
608
+ }) : children;
382
609
  return /* @__PURE__ */(0, import_jsx_runtime.jsx)(Cell, {
610
+ ...getTableCellPadding(knobProps.space),
383
611
  ...props,
612
+ style: [{
613
+ verticalAlign: "middle"
614
+ }, style],
384
615
  children: /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tableCellContext.TableCellContext.Provider, {
385
616
  value: {
386
617
  inTableCell: true,
387
618
  isHeader: false
388
619
  },
389
- children
620
+ children: content
390
621
  })
391
622
  });
392
623
  }
393
624
  function ThemedHeaderCell({
394
625
  children,
626
+ style,
395
627
  ...props
396
628
  }) {
629
+ const {
630
+ knobProps
631
+ } = (0, import_theme.useResolvedKnobs)();
632
+ const content = typeof children === "string" || typeof children === "number" ? /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tamagui.SizableText, {
633
+ fontSize: knobProps.sizeToken,
634
+ fontWeight: "600",
635
+ children
636
+ }) : children;
397
637
  return /* @__PURE__ */(0, import_jsx_runtime.jsx)(HeaderCell, {
638
+ ...getTableCellPadding(knobProps.space),
398
639
  ...props,
640
+ style: [{
641
+ verticalAlign: "middle",
642
+ fontWeight: "normal",
643
+ textAlign: "start"
644
+ }, style],
399
645
  children: /* @__PURE__ */(0, import_jsx_runtime.jsx)(import_tableCellContext.TableCellContext.Provider, {
400
646
  value: {
401
647
  inTableCell: true,
402
648
  isHeader: true
403
649
  },
404
- children
650
+ children: content
405
651
  })
406
652
  });
407
653
  }
408
654
  const Table = (0, import_tamagui.withStaticProperties)(ThemedTableComp, {
409
- Head: TableHead,
655
+ Head: ThemedTableHead,
410
656
  Body: TableBody,
411
657
  Row: ThemedRow,
412
658
  Cell: ThemedCell,