@multiplatform.one/table-primitives 6.0.4 → 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 +61 -0
- package/dist/cjs/index.cjs +272 -26
- package/dist/cjs/index.native.js +349 -26
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/index.mjs +275 -30
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +352 -30
- package/dist/esm/index.native.js.map +1 -1
- package/dist/jsx/index.js +275 -30
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +275 -30
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +349 -26
- package/dist/jsx/index.native.js.map +1 -1
- package/package.json +13 -10
- package/src/index.tsx +374 -35
- package/src/table.spec.tsx +57 -28
- package/types/index.d.ts +193 -0
- package/types/index.d.ts.map +1 -0
- package/types/tableCellContext.d.ts +18 -0
- package/types/tableCellContext.d.ts.map +1 -0
- package/dist/cjs/table.spec.cjs +0 -183
- package/dist/cjs/table.spec.native.js +0 -186
- package/dist/cjs/table.spec.native.js.map +0 -1
- package/dist/esm/table.spec.mjs +0 -184
- package/dist/esm/table.spec.mjs.map +0 -1
- package/dist/esm/table.spec.native.js +0 -184
- package/dist/esm/table.spec.native.js.map +0 -1
- package/dist/jsx/table.spec.mjs +0 -184
- package/dist/jsx/table.spec.mjs.map +0 -1
- package/dist/jsx/table.spec.native.js +0 -186
- package/dist/jsx/table.spec.native.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/table-primitives",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "Semantic Table primitives and cell context (no forms or data-table)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -12,34 +12,37 @@
|
|
|
12
12
|
"author": "BitSpur <support@risserlabs.com> (https://risserlabs.com)",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://gitlab.com/bitspur/
|
|
15
|
+
"url": "git+https://gitlab.com/bitspur/frappe/multiplatform.one.git",
|
|
16
|
+
"directory": "public/table-primitives"
|
|
16
17
|
},
|
|
17
18
|
"source": "src/index.tsx",
|
|
18
19
|
"files": [
|
|
19
20
|
"dist",
|
|
20
21
|
"src",
|
|
21
|
-
"types"
|
|
22
|
+
"types",
|
|
23
|
+
"!types/.tsbuildinfo"
|
|
22
24
|
],
|
|
23
25
|
"sideEffects": false,
|
|
24
26
|
"main": "src/index.tsx",
|
|
25
27
|
"module": "src/index.tsx",
|
|
26
|
-
"types": "
|
|
28
|
+
"types": "types/index.d.ts",
|
|
27
29
|
"exports": {
|
|
28
30
|
"./package.json": "./package.json",
|
|
29
31
|
".": {
|
|
30
32
|
"source": "./src/index.tsx",
|
|
31
33
|
"react-native": {
|
|
34
|
+
"types": "./types/index.d.ts",
|
|
32
35
|
"import": "./dist/esm/index.native.js",
|
|
33
36
|
"require": "./dist/cjs/index.native.js"
|
|
34
37
|
},
|
|
35
|
-
"types": "./
|
|
38
|
+
"types": "./types/index.d.ts",
|
|
36
39
|
"import": "./dist/esm/index.mjs",
|
|
37
40
|
"require": "./dist/cjs/index.cjs",
|
|
38
41
|
"default": "./dist/cjs/index.cjs"
|
|
39
42
|
},
|
|
40
43
|
"./tableCellContext": {
|
|
41
44
|
"source": "./src/tableCellContext.ts",
|
|
42
|
-
"types": "./
|
|
45
|
+
"types": "./types/tableCellContext.d.ts",
|
|
43
46
|
"import": "./src/tableCellContext.ts",
|
|
44
47
|
"require": "./src/tableCellContext.ts",
|
|
45
48
|
"default": "./src/tableCellContext.ts"
|
|
@@ -50,7 +53,7 @@
|
|
|
50
53
|
},
|
|
51
54
|
"dependencies": {
|
|
52
55
|
"tamagui": "2.0.0-rc.41",
|
|
53
|
-
"@multiplatform.one/theme": "6.0
|
|
56
|
+
"@multiplatform.one/theme": "6.3.0"
|
|
54
57
|
},
|
|
55
58
|
"devDependencies": {
|
|
56
59
|
"@tamagui/build": "2.0.0-rc.41",
|
|
@@ -62,8 +65,8 @@
|
|
|
62
65
|
"react": "19.2.5",
|
|
63
66
|
"react-dom": "19.2.5",
|
|
64
67
|
"vitest": "^4.1.5",
|
|
65
|
-
"@multiplatform.one/
|
|
66
|
-
"@multiplatform.one/
|
|
68
|
+
"@multiplatform.one/test-utils": "6.3.0",
|
|
69
|
+
"@multiplatform.one/config": "6.3.0"
|
|
67
70
|
},
|
|
68
71
|
"peerDependencies": {
|
|
69
72
|
"react": "^19.1.0",
|
|
@@ -71,7 +74,7 @@
|
|
|
71
74
|
},
|
|
72
75
|
"module:jsx": "dist/jsx",
|
|
73
76
|
"scripts": {
|
|
74
|
-
"build": "rm -rf dist types 2>/dev/null && tamagui-build --ts-project ./tsconfig.build.json",
|
|
77
|
+
"build": "rm -rf dist types 2>/dev/null && tamagui-build --ts-project ./tsconfig.build.json --exclude '\\.spec\\.|\\.test\\.|\\.stories\\.'",
|
|
75
78
|
"test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=lcov --coverage.reporter=html",
|
|
76
79
|
"typecheck": "tsc --noEmit"
|
|
77
80
|
}
|
package/src/index.tsx
CHANGED
|
@@ -5,10 +5,20 @@
|
|
|
5
5
|
* Consumed by @multiplatform.one/table, @multiplatform.one/forms, etc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
8
|
+
import { hairline, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
9
9
|
import { TableCellContext } from "./tableCellContext";
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
10
|
+
import { Children, Fragment, isValidElement } from "react";
|
|
11
|
+
import type { FontSizeTokens, SizeTokens } from "tamagui";
|
|
12
|
+
import {
|
|
13
|
+
SizableText,
|
|
14
|
+
Theme,
|
|
15
|
+
ThemeableStack,
|
|
16
|
+
createStyledContext,
|
|
17
|
+
isWeb,
|
|
18
|
+
styled,
|
|
19
|
+
useMedia,
|
|
20
|
+
withStaticProperties,
|
|
21
|
+
} from "tamagui";
|
|
12
22
|
|
|
13
23
|
export { TableCellContext, useIsInTableCell, useTableCellContext } from "./tableCellContext";
|
|
14
24
|
export type { TableCellContextValue } from "./tableCellContext";
|
|
@@ -39,13 +49,28 @@ const TableContext = createStyledContext<{
|
|
|
39
49
|
alignHeaderCells: { x: "start", y: "center" },
|
|
40
50
|
alignCells: { x: "start", y: "center" },
|
|
41
51
|
borderColor: "$color6",
|
|
42
|
-
|
|
52
|
+
// CSS table display values ("table-row", "table-cell", …) are no-ops in
|
|
53
|
+
// React Native — cells stack vertically and bare Tables are unreadable.
|
|
54
|
+
// Native defaults to the flex composition; web keeps semantic table layout.
|
|
55
|
+
displayMode: isWeb ? "table" : "flex",
|
|
43
56
|
});
|
|
44
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Element rendering: tamagui 2.0.0-rc derives the DOM element from the
|
|
60
|
+
* `render` prop (`tag:` passes through as a plain attribute), so every
|
|
61
|
+
* primitive sets `render:` to emit real `<table>/<thead>/<tbody>/<tfoot>/
|
|
62
|
+
* <tr>/<th>/<td>/<caption>` elements on web. On native `render` is inert
|
|
63
|
+
* (skipProps) and the components stay RN Views in the flex displayMode.
|
|
64
|
+
*
|
|
65
|
+
* Explicit ARIA roles stay on every element: consumers restyle these
|
|
66
|
+
* elements (`displayMode="flex"`, sticky cells), and browsers drop the
|
|
67
|
+
* implicit table roles when a table element's display is overridden.
|
|
68
|
+
*/
|
|
69
|
+
|
|
45
70
|
/** Table Components */
|
|
46
71
|
const Row = styled(ThemeableStack, {
|
|
47
72
|
name: "TableRow",
|
|
48
|
-
...({
|
|
73
|
+
...({ render: "tr" } as { render?: string }),
|
|
49
74
|
role: "row",
|
|
50
75
|
context: TableContext,
|
|
51
76
|
display: "table-row" as any,
|
|
@@ -69,16 +94,23 @@ const Row = styled(ThemeableStack, {
|
|
|
69
94
|
|
|
70
95
|
const Cell = styled(ThemeableStack, {
|
|
71
96
|
name: "TableCell",
|
|
72
|
-
...({
|
|
97
|
+
...({ render: "td" } as { render?: string }),
|
|
98
|
+
role: "cell",
|
|
73
99
|
context: TableContext,
|
|
74
100
|
display: "table-cell" as any,
|
|
75
101
|
// verticalAlign goes via inline style; Tamagui forwards it to the DOM as a JSX
|
|
76
102
|
// prop otherwise, which React rejects ("React does not recognize verticalAlign").
|
|
77
103
|
style: { verticalAlign: "middle" },
|
|
104
|
+
// LC-44 ROW-LINE: cells are ROW-flex so each axis does what it says —
|
|
105
|
+
// justifyContent is horizontal (start by default; align-by-type overrides
|
|
106
|
+
// land here) and alignItems is vertical (center). The previous column
|
|
107
|
+
// direction put alignItems:center on the HORIZONTAL axis, centering every
|
|
108
|
+
// cell's text and making variable-width columns ragged (A16-02).
|
|
109
|
+
flexDirection: "row",
|
|
78
110
|
alignItems: "center",
|
|
79
111
|
justifyContent: "flex-start",
|
|
80
112
|
paddingHorizontal: "$2",
|
|
81
|
-
paddingVertical: "$
|
|
113
|
+
paddingVertical: "$3",
|
|
82
114
|
overflow: "hidden",
|
|
83
115
|
variants: {
|
|
84
116
|
displayMode: {
|
|
@@ -106,6 +138,8 @@ const Cell = styled(ThemeableStack, {
|
|
|
106
138
|
};
|
|
107
139
|
},
|
|
108
140
|
},
|
|
141
|
+
// Row-flex axis map (LC-44): y → alignItems (vertical), x →
|
|
142
|
+
// justifyContent (horizontal).
|
|
109
143
|
alignCells: (val: AlignCells) => {
|
|
110
144
|
return {
|
|
111
145
|
alignItems: val.y === "center" ? "center" : `flex-${val.y}`,
|
|
@@ -122,20 +156,30 @@ const Cell = styled(ThemeableStack, {
|
|
|
122
156
|
|
|
123
157
|
const HeaderCell = styled(ThemeableStack, {
|
|
124
158
|
name: "TableHeaderCell",
|
|
125
|
-
...({
|
|
159
|
+
...({ render: "th", scope: "col" } as { render?: string; scope: string }),
|
|
160
|
+
role: "columnheader",
|
|
126
161
|
context: TableContext,
|
|
127
162
|
display: "table-cell" as any,
|
|
128
163
|
// verticalAlign goes via inline style; see TableCell above for context.
|
|
129
|
-
|
|
164
|
+
// fontWeight/textAlign neutralize the UA styles a real <th> brings
|
|
165
|
+
// (bold + centered) so header content keeps its pre-migration look;
|
|
166
|
+
// logical `start` stays RTL-correct.
|
|
167
|
+
style: { verticalAlign: "middle", fontWeight: "normal", textAlign: "start" },
|
|
168
|
+
// LC-44 ROW-LINE: row-flex like TableCell — header labels start-align on
|
|
169
|
+
// the same column line as body cell text.
|
|
170
|
+
flexDirection: "row",
|
|
130
171
|
alignItems: "center",
|
|
131
172
|
justifyContent: "flex-start",
|
|
132
173
|
paddingHorizontal: "$2",
|
|
133
|
-
paddingVertical: "$
|
|
134
|
-
backgroundColor: "$
|
|
174
|
+
paddingVertical: "$3",
|
|
175
|
+
backgroundColor: "$color2",
|
|
135
176
|
|
|
136
177
|
variants: {
|
|
137
178
|
displayMode: {
|
|
138
|
-
table: {
|
|
179
|
+
table: {
|
|
180
|
+
display: "table-cell" as any,
|
|
181
|
+
style: { verticalAlign: "middle", fontWeight: "normal", textAlign: "start" },
|
|
182
|
+
},
|
|
139
183
|
flex: { display: "flex" as any },
|
|
140
184
|
},
|
|
141
185
|
cellWidth: {
|
|
@@ -152,6 +196,8 @@ const HeaderCell = styled(ThemeableStack, {
|
|
|
152
196
|
minWidth: tokens.size[name],
|
|
153
197
|
}),
|
|
154
198
|
},
|
|
199
|
+
// Row-flex axis map (LC-44): y → alignItems (vertical), x →
|
|
200
|
+
// justifyContent (horizontal).
|
|
155
201
|
alignHeaderCells: (val: AlignHeaderCells) => {
|
|
156
202
|
return {
|
|
157
203
|
alignItems: val.y === "center" ? "center" : `flex-${val.y}`,
|
|
@@ -173,7 +219,8 @@ const HeaderCell = styled(ThemeableStack, {
|
|
|
173
219
|
|
|
174
220
|
const TableBody = styled(ThemeableStack, {
|
|
175
221
|
name: "TableBody",
|
|
176
|
-
...({
|
|
222
|
+
...({ render: "tbody" } as { render?: string }),
|
|
223
|
+
role: "rowgroup",
|
|
177
224
|
context: TableContext,
|
|
178
225
|
display: "table-row-group" as any,
|
|
179
226
|
variants: {
|
|
@@ -184,7 +231,13 @@ const TableBody = styled(ThemeableStack, {
|
|
|
184
231
|
flexDirection: "column" as any,
|
|
185
232
|
width: "100%",
|
|
186
233
|
overflowY: "auto" as any,
|
|
187
|
-
flex:
|
|
234
|
+
// basis auto (not flex:1 basis-0): in auto-height parents a basis-0
|
|
235
|
+
// chain resolves to 0 and the body collapses to a scrollbar sliver;
|
|
236
|
+
// basis auto sizes to rows there while still filling sized parents
|
|
237
|
+
// (grow) and scrolling (shrink + minHeight 0).
|
|
238
|
+
flexGrow: 1,
|
|
239
|
+
flexShrink: 1,
|
|
240
|
+
flexBasis: "auto" as any,
|
|
188
241
|
minHeight: 0,
|
|
189
242
|
},
|
|
190
243
|
},
|
|
@@ -193,7 +246,8 @@ const TableBody = styled(ThemeableStack, {
|
|
|
193
246
|
|
|
194
247
|
const TableHead = styled(ThemeableStack, {
|
|
195
248
|
name: "TableHead",
|
|
196
|
-
...({
|
|
249
|
+
...({ render: "thead" } as { render?: string }),
|
|
250
|
+
role: "rowgroup",
|
|
197
251
|
context: TableContext,
|
|
198
252
|
display: "table-header-group" as any,
|
|
199
253
|
borderBottomWidth: 1,
|
|
@@ -213,7 +267,8 @@ const TableHead = styled(ThemeableStack, {
|
|
|
213
267
|
|
|
214
268
|
const TableFoot = styled(ThemeableStack, {
|
|
215
269
|
name: "TableFoot",
|
|
216
|
-
...({
|
|
270
|
+
...({ render: "tfoot" } as { render?: string }),
|
|
271
|
+
role: "rowgroup",
|
|
217
272
|
context: TableContext,
|
|
218
273
|
display: "table-footer-group" as any,
|
|
219
274
|
variants: {
|
|
@@ -231,16 +286,20 @@ const TableFoot = styled(ThemeableStack, {
|
|
|
231
286
|
|
|
232
287
|
const TableCaption = styled(ThemeableStack, {
|
|
233
288
|
name: "TableCaption",
|
|
234
|
-
...({
|
|
289
|
+
...({ render: "caption" } as { render?: string }),
|
|
235
290
|
context: TableContext,
|
|
236
291
|
display: "table-caption" as any,
|
|
292
|
+
// Real <caption> centers its text by UA default — keep the primitive's
|
|
293
|
+
// pre-migration start alignment (logical, RTL-correct).
|
|
294
|
+
style: { textAlign: "start" },
|
|
237
295
|
paddingVertical: "$2",
|
|
238
296
|
paddingHorizontal: "$2",
|
|
239
297
|
});
|
|
240
298
|
|
|
241
299
|
const TableComp = styled(ThemeableStack, {
|
|
242
300
|
name: "Table",
|
|
243
|
-
...({
|
|
301
|
+
...({ render: "table" } as { render?: string }),
|
|
302
|
+
role: "table",
|
|
244
303
|
context: TableContext,
|
|
245
304
|
display: "table" as any,
|
|
246
305
|
borderWidth: 1,
|
|
@@ -254,7 +313,10 @@ const TableComp = styled(ThemeableStack, {
|
|
|
254
313
|
flex: {
|
|
255
314
|
display: "flex" as any,
|
|
256
315
|
flexDirection: "column" as any,
|
|
257
|
-
|
|
316
|
+
// See Body: basis auto so auto-height parents get content height.
|
|
317
|
+
flexGrow: 1,
|
|
318
|
+
flexShrink: 1,
|
|
319
|
+
flexBasis: "auto" as any,
|
|
258
320
|
minHeight: 0,
|
|
259
321
|
},
|
|
260
322
|
},
|
|
@@ -292,59 +354,336 @@ function resolveIntent(props: ThemedRowProps): IntentName | undefined {
|
|
|
292
354
|
return undefined;
|
|
293
355
|
}
|
|
294
356
|
|
|
357
|
+
/**
|
|
358
|
+
* SP-EDGE row density: the `space` knob scales cell padding (row density)
|
|
359
|
+
* without adding an outer row inset (LC-18 / theme-propagation-spec SP-EDGE).
|
|
360
|
+
* `medium` matches the pre-knob defaults so default rendering is unchanged.
|
|
361
|
+
*/
|
|
362
|
+
export function getTableCellPadding(space: string): {
|
|
363
|
+
paddingHorizontal: string;
|
|
364
|
+
paddingVertical: string;
|
|
365
|
+
} {
|
|
366
|
+
const map: Record<string, { paddingHorizontal: string; paddingVertical: string }> = {
|
|
367
|
+
small: { paddingHorizontal: "$1.5", paddingVertical: "$2" },
|
|
368
|
+
medium: { paddingHorizontal: "$2", paddingVertical: "$3" },
|
|
369
|
+
large: { paddingHorizontal: "$3", paddingVertical: "$4" },
|
|
370
|
+
};
|
|
371
|
+
return map[space] ?? map.medium;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Responsive layout mode for the Table primitive.
|
|
376
|
+
* - `table`: always render the semantic table (default — Layer-1 consumers
|
|
377
|
+
* like markdown tables and DataTable's grid own their responsive handling)
|
|
378
|
+
* - `cards`: render each body row as a stacked card of label/value pairs
|
|
379
|
+
* - `auto`: table on `$sm` and up, cards below (W11 compact class)
|
|
380
|
+
*/
|
|
381
|
+
export type TableLayout = "table" | "cards" | "auto";
|
|
382
|
+
|
|
383
|
+
interface CardRowModel {
|
|
384
|
+
key: string;
|
|
385
|
+
intent?: IntentName;
|
|
386
|
+
cells: Array<{ key: string; content: React.ReactNode }>;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Flatten fragments/arrays into a single element list. */
|
|
390
|
+
function flattenChildren(children: React.ReactNode): React.ReactNode[] {
|
|
391
|
+
return Children.toArray(children).flatMap((child) => {
|
|
392
|
+
if (isValidElement(child) && child.type === Fragment) {
|
|
393
|
+
return flattenChildren((child.props as { children?: React.ReactNode }).children);
|
|
394
|
+
}
|
|
395
|
+
return [child];
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Statically extract header labels, body rows, and the caption from the
|
|
401
|
+
* canonical `Table > Head/Body/Caption > Row > Cell` composition. Rows or
|
|
402
|
+
* cells wrapped in custom components are not discovered — those cards render
|
|
403
|
+
* value-only rows (documented limitation of the card layout).
|
|
404
|
+
*/
|
|
405
|
+
function extractCardModel(children: React.ReactNode): {
|
|
406
|
+
headerLabels: React.ReactNode[];
|
|
407
|
+
rows: CardRowModel[];
|
|
408
|
+
caption: React.ReactNode;
|
|
409
|
+
} {
|
|
410
|
+
const headerLabels: React.ReactNode[] = [];
|
|
411
|
+
const rows: CardRowModel[] = [];
|
|
412
|
+
let caption: React.ReactNode = null;
|
|
413
|
+
|
|
414
|
+
for (const section of flattenChildren(children)) {
|
|
415
|
+
if (!isValidElement(section)) continue;
|
|
416
|
+
const sectionProps = section.props as { children?: React.ReactNode };
|
|
417
|
+
if (section.type === ThemedTableHead) {
|
|
418
|
+
for (const rowEl of flattenChildren(sectionProps.children)) {
|
|
419
|
+
if (!isValidElement(rowEl) || rowEl.type !== ThemedRow) continue;
|
|
420
|
+
flattenChildren((rowEl.props as { children?: React.ReactNode }).children).forEach(
|
|
421
|
+
(cellEl, index) => {
|
|
422
|
+
if (!isValidElement(cellEl) || cellEl.type !== ThemedHeaderCell) return;
|
|
423
|
+
if (headerLabels[index] === undefined) {
|
|
424
|
+
headerLabels[index] = (cellEl.props as { children?: React.ReactNode }).children;
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
} else if (section.type === TableBody) {
|
|
430
|
+
for (const rowEl of flattenChildren(sectionProps.children)) {
|
|
431
|
+
if (!isValidElement(rowEl) || rowEl.type !== ThemedRow) continue;
|
|
432
|
+
const rowProps = rowEl.props as ThemedRowProps;
|
|
433
|
+
const cells = flattenChildren(rowProps.children)
|
|
434
|
+
.filter(
|
|
435
|
+
(cellEl): cellEl is React.ReactElement =>
|
|
436
|
+
isValidElement(cellEl) && cellEl.type === ThemedCell,
|
|
437
|
+
)
|
|
438
|
+
.map((cellEl, index) => ({
|
|
439
|
+
key: cellEl.key ?? String(index),
|
|
440
|
+
content: (cellEl.props as { children?: React.ReactNode }).children,
|
|
441
|
+
}));
|
|
442
|
+
rows.push({
|
|
443
|
+
key: rowEl.key ?? String(rows.length),
|
|
444
|
+
intent: resolveIntent(rowProps),
|
|
445
|
+
cells,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
} else if (section.type === TableCaption) {
|
|
449
|
+
caption = sectionProps.children;
|
|
450
|
+
}
|
|
451
|
+
// Foot content has no card slot — it is omitted in the card layout.
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return { headerLabels, rows, caption };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Card list rendering for the Table primitive (layout="cards" / narrow auto). */
|
|
458
|
+
function TableCardsList({
|
|
459
|
+
children,
|
|
460
|
+
...props
|
|
461
|
+
}: {
|
|
462
|
+
children?: React.ReactNode;
|
|
463
|
+
[key: string]: any;
|
|
464
|
+
}) {
|
|
465
|
+
const { knobProps } = useResolvedKnobs();
|
|
466
|
+
const { headerLabels, rows, caption } = extractCardModel(children);
|
|
467
|
+
// Table-context props have no meaning on the card list — drop them.
|
|
468
|
+
const {
|
|
469
|
+
cellWidth: _cellWidth,
|
|
470
|
+
cellHeight: _cellHeight,
|
|
471
|
+
alignCells: _alignCells,
|
|
472
|
+
alignHeaderCells: _alignHeaderCells,
|
|
473
|
+
displayMode: _displayMode,
|
|
474
|
+
style: _style,
|
|
475
|
+
...viewProps
|
|
476
|
+
} = props;
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<ThemeableStack flexDirection="column" {...knobProps.gap} testID="table-cards" {...viewProps}>
|
|
480
|
+
{caption != null &&
|
|
481
|
+
(typeof caption === "string" || typeof caption === "number" ? (
|
|
482
|
+
<SizableText fontSize={knobProps.sizeToken as FontSizeTokens} color="$color10">
|
|
483
|
+
{caption}
|
|
484
|
+
</SizableText>
|
|
485
|
+
) : (
|
|
486
|
+
caption
|
|
487
|
+
))}
|
|
488
|
+
{rows.map((row) => {
|
|
489
|
+
const card = (
|
|
490
|
+
<ThemeableStack
|
|
491
|
+
key={row.key}
|
|
492
|
+
testID="table-card"
|
|
493
|
+
{...knobProps.surface}
|
|
494
|
+
{...knobProps.cardSurface}
|
|
495
|
+
elevation={knobProps.elevation}
|
|
496
|
+
borderColor="$color6"
|
|
497
|
+
{...(row.intent ? { backgroundColor: "$color3" as const } : {})}
|
|
498
|
+
>
|
|
499
|
+
{row.cells.map((cell, index) => {
|
|
500
|
+
const label = headerLabels[index];
|
|
501
|
+
const value =
|
|
502
|
+
typeof cell.content === "string" || typeof cell.content === "number" ? (
|
|
503
|
+
<SizableText fontSize={knobProps.sizeToken as FontSizeTokens}>
|
|
504
|
+
{cell.content}
|
|
505
|
+
</SizableText>
|
|
506
|
+
) : (
|
|
507
|
+
cell.content
|
|
508
|
+
);
|
|
509
|
+
return (
|
|
510
|
+
<ThemeableStack
|
|
511
|
+
key={cell.key}
|
|
512
|
+
flexDirection="row"
|
|
513
|
+
alignItems="center"
|
|
514
|
+
gap="$2"
|
|
515
|
+
paddingVertical="$1.5"
|
|
516
|
+
>
|
|
517
|
+
{label != null && (
|
|
518
|
+
<SizableText
|
|
519
|
+
fontSize={knobProps.sizeToken as FontSizeTokens}
|
|
520
|
+
fontWeight="500"
|
|
521
|
+
color="$color10"
|
|
522
|
+
width="40%"
|
|
523
|
+
flexShrink={0}
|
|
524
|
+
>
|
|
525
|
+
{label}
|
|
526
|
+
</SizableText>
|
|
527
|
+
)}
|
|
528
|
+
<ThemeableStack flex={1} minWidth={0}>
|
|
529
|
+
<TableCellContext.Provider value={{ inTableCell: true, isHeader: false }}>
|
|
530
|
+
{value}
|
|
531
|
+
</TableCellContext.Provider>
|
|
532
|
+
</ThemeableStack>
|
|
533
|
+
</ThemeableStack>
|
|
534
|
+
);
|
|
535
|
+
})}
|
|
536
|
+
</ThemeableStack>
|
|
537
|
+
);
|
|
538
|
+
return row.intent ? (
|
|
539
|
+
<Theme key={row.key} name={row.intent}>
|
|
540
|
+
{card}
|
|
541
|
+
</Theme>
|
|
542
|
+
) : (
|
|
543
|
+
card
|
|
544
|
+
);
|
|
545
|
+
})}
|
|
546
|
+
</ThemeableStack>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export interface ThemedTableProps extends React.ComponentProps<typeof TableComp> {
|
|
551
|
+
/** Responsive layout — see {@link TableLayout}. Default `table`. */
|
|
552
|
+
layout?: TableLayout;
|
|
553
|
+
}
|
|
554
|
+
|
|
295
555
|
// Themed wrappers that inject resolved knob recipes
|
|
296
|
-
function ThemedTableComp(props:
|
|
556
|
+
function ThemedTableComp({ layout = "table", ...props }: ThemedTableProps) {
|
|
297
557
|
const { knobProps } = useResolvedKnobs();
|
|
298
|
-
const
|
|
558
|
+
const { sm } = useMedia();
|
|
559
|
+
const baseBorderWidth = knobProps.surface.borderWidth;
|
|
299
560
|
// When outlined, ensure at least 1px border. When filled, use base or 0.
|
|
300
561
|
const resolvedBorderWidth = knobProps.outlined ? Math.max(baseBorderWidth, 1) : baseBorderWidth;
|
|
301
562
|
|
|
563
|
+
const showCards = layout === "cards" || (layout === "auto" && sm === false);
|
|
564
|
+
if (showCards) {
|
|
565
|
+
return <TableCardsList {...props} />;
|
|
566
|
+
}
|
|
567
|
+
|
|
302
568
|
return (
|
|
303
569
|
<TableComp
|
|
304
|
-
|
|
570
|
+
// Table frames are R-OUTER + SF-CARD-tier surface (theme-propagation-spec
|
|
571
|
+
// `@multiplatform.one/table`): outer corners follow the capped outer
|
|
572
|
+
// radius map; fill flips the frame between the surface bg and transparent.
|
|
573
|
+
{...knobProps.surface}
|
|
574
|
+
{...knobProps.borderRadiusOuter}
|
|
575
|
+
{...(knobProps.outlined ? { backgroundColor: "transparent" as const } : null)}
|
|
305
576
|
borderWidth={resolvedBorderWidth}
|
|
306
|
-
borderColor="$color6"
|
|
307
|
-
backgroundColor="$background"
|
|
308
577
|
elevation={knobProps.elevation}
|
|
578
|
+
// Size knob sets default row height through the table context; consumer
|
|
579
|
+
// cellHeight (table- or cell-level) still overrides via {...props}.
|
|
580
|
+
cellHeight={knobProps.sizeToken as SizeTokens}
|
|
309
581
|
{...props}
|
|
310
|
-
|
|
582
|
+
// borderSpacing 0 neutralizes the real <table> UA default (2px gaps
|
|
583
|
+
// between cells) so the migrated element keeps the div-table look.
|
|
584
|
+
style={[{ tableLayout: "fixed" as const, borderSpacing: 0 }, props.style]}
|
|
311
585
|
/>
|
|
312
586
|
);
|
|
313
587
|
}
|
|
314
588
|
|
|
589
|
+
function ThemedTableHead(props: React.ComponentProps<typeof TableHead>) {
|
|
590
|
+
// Head/body rule is a divider, not a control border — hairline on
|
|
591
|
+
// high-DPI (Axiom 15). Explicit consumer props still override.
|
|
592
|
+
return <TableHead {...hairline.bottom} {...props} />;
|
|
593
|
+
}
|
|
594
|
+
|
|
315
595
|
function ThemedRow({ accent, error, warning, success, ...props }: ThemedRowProps) {
|
|
316
596
|
const intent = resolveIntent({ accent, error, warning, success } as ThemedRowProps);
|
|
317
597
|
|
|
318
|
-
|
|
598
|
+
// Intent rows must not use the plain <Theme> wrapper: on web it renders a
|
|
599
|
+
// display:contents <span> between <tbody> and <tr>, which is invalid HTML
|
|
600
|
+
// once the primitives emit real table elements (SSR parsers foster-parent
|
|
601
|
+
// the span out of the table and hydration breaks). Instead the theme's
|
|
602
|
+
// CSS class goes on the <tr> itself (scoping the theme CSS variables for
|
|
603
|
+
// the row and its cells) while <Theme forceClassName={false}> still
|
|
604
|
+
// provides the theme context for JS-resolved values and native styling
|
|
605
|
+
// without emitting any wrapper element.
|
|
606
|
+
const row = (
|
|
607
|
+
<Row
|
|
608
|
+
{...(intent
|
|
609
|
+
? {
|
|
610
|
+
backgroundColor: "$color3" as const,
|
|
611
|
+
// `color: $color` mirrors the inline color the Theme span used
|
|
612
|
+
// to set, so unstyled text in cells inherits the intent color.
|
|
613
|
+
color: "$color" as const,
|
|
614
|
+
...(isWeb ? { className: `t_${intent}` } : {}),
|
|
615
|
+
}
|
|
616
|
+
: {})}
|
|
617
|
+
{...props}
|
|
618
|
+
/>
|
|
619
|
+
);
|
|
319
620
|
|
|
320
621
|
if (intent) {
|
|
321
|
-
return
|
|
622
|
+
return (
|
|
623
|
+
<Theme name={intent} forceClassName={false}>
|
|
624
|
+
{row}
|
|
625
|
+
</Theme>
|
|
626
|
+
);
|
|
322
627
|
}
|
|
323
628
|
return row;
|
|
324
629
|
}
|
|
325
630
|
|
|
326
|
-
function ThemedCell({ children, ...props }: React.ComponentProps<typeof Cell>) {
|
|
631
|
+
function ThemedCell({ children, style, ...props }: React.ComponentProps<typeof Cell>) {
|
|
632
|
+
const { knobProps } = useResolvedKnobs();
|
|
633
|
+
// Bare string/number children would render unstyled (and crash on native,
|
|
634
|
+
// where text must live inside a Text component) — wrap them so the size
|
|
635
|
+
// knob reaches cell text (T-VALUE); element children own their text styling.
|
|
636
|
+
const content =
|
|
637
|
+
typeof children === "string" || typeof children === "number" ? (
|
|
638
|
+
<SizableText fontSize={knobProps.sizeToken as FontSizeTokens}>{children}</SizableText>
|
|
639
|
+
) : (
|
|
640
|
+
children
|
|
641
|
+
);
|
|
327
642
|
return (
|
|
328
|
-
<Cell
|
|
643
|
+
<Cell
|
|
644
|
+
{...getTableCellPadding(knobProps.space)}
|
|
645
|
+
{...props}
|
|
646
|
+
// Compose (not replace) the inline style so a consumer style (e.g.
|
|
647
|
+
// sticky pinned cells) keeps the verticalAlign baseline.
|
|
648
|
+
style={[{ verticalAlign: "middle" }, style as any]}
|
|
649
|
+
>
|
|
329
650
|
<TableCellContext.Provider value={{ inTableCell: true, isHeader: false }}>
|
|
330
|
-
{
|
|
651
|
+
{content}
|
|
331
652
|
</TableCellContext.Provider>
|
|
332
653
|
</Cell>
|
|
333
654
|
);
|
|
334
655
|
}
|
|
335
656
|
|
|
336
|
-
function ThemedHeaderCell({ children, ...props }: React.ComponentProps<typeof HeaderCell>) {
|
|
657
|
+
function ThemedHeaderCell({ children, style, ...props }: React.ComponentProps<typeof HeaderCell>) {
|
|
658
|
+
const { knobProps } = useResolvedKnobs();
|
|
659
|
+
// Bare string/number children would render unstyled (and crash on native,
|
|
660
|
+
// where text must live inside a Text component) — wrap them; element
|
|
661
|
+
// children own their text styling.
|
|
662
|
+
const content =
|
|
663
|
+
typeof children === "string" || typeof children === "number" ? (
|
|
664
|
+
<SizableText fontSize={knobProps.sizeToken as FontSizeTokens} fontWeight="600">
|
|
665
|
+
{children}
|
|
666
|
+
</SizableText>
|
|
667
|
+
) : (
|
|
668
|
+
children
|
|
669
|
+
);
|
|
337
670
|
return (
|
|
338
|
-
<HeaderCell
|
|
671
|
+
<HeaderCell
|
|
672
|
+
{...getTableCellPadding(knobProps.space)}
|
|
673
|
+
{...props}
|
|
674
|
+
// Compose (not replace) the inline style so consumer styles (sticky
|
|
675
|
+
// pinned headers) keep the real-<th> UA neutralizers (bold/centered).
|
|
676
|
+
style={[{ verticalAlign: "middle", fontWeight: "normal", textAlign: "start" }, style as any]}
|
|
677
|
+
>
|
|
339
678
|
<TableCellContext.Provider value={{ inTableCell: true, isHeader: true }}>
|
|
340
|
-
{
|
|
679
|
+
{content}
|
|
341
680
|
</TableCellContext.Provider>
|
|
342
681
|
</HeaderCell>
|
|
343
682
|
);
|
|
344
683
|
}
|
|
345
684
|
|
|
346
685
|
export const Table = withStaticProperties(ThemedTableComp, {
|
|
347
|
-
Head:
|
|
686
|
+
Head: ThemedTableHead,
|
|
348
687
|
Body: TableBody,
|
|
349
688
|
Row: ThemedRow,
|
|
350
689
|
Cell: ThemedCell,
|
|
@@ -354,7 +693,7 @@ export const Table = withStaticProperties(ThemedTableComp, {
|
|
|
354
693
|
});
|
|
355
694
|
|
|
356
695
|
// Export prop types for all styled components
|
|
357
|
-
export type TableProps =
|
|
696
|
+
export type TableProps = ThemedTableProps;
|
|
358
697
|
export type TableRowProps = ThemedRowProps;
|
|
359
698
|
export type TableCellProps = React.ComponentProps<typeof Cell>;
|
|
360
699
|
export type TableHeaderCellProps = React.ComponentProps<typeof HeaderCell>;
|