@multiplatform.one/table 6.4.1 → 6.4.2
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/package.json +12 -12
- package/src/components/DataTable/index.native.tsx +1 -1
- package/src/components/DataTableColumnPinning/index.tsx +5 -2
- package/src/components/DataTableFilterChips/index.tsx +1 -1
- package/src/components/DataTableGrouping/DataTableGrouping.stories.tsx +6 -2
- package/src/components/DataTableGrouping/DataTableGroupingConfig.stories.tsx +120 -0
- package/src/components/DataTableGrouping/index.tsx +26 -2
- package/src/components/SimpleTable/index.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/table",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.2",
|
|
4
4
|
"description": "Advanced table components with filtering, pagination, sorting, and URL state management",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cross-platform",
|
|
@@ -80,13 +80,13 @@
|
|
|
80
80
|
"@tanstack/react-table": "^8.21.3",
|
|
81
81
|
"@tanstack/table-core": "^8.21.3",
|
|
82
82
|
"tamagui": "2.0.0-rc.41",
|
|
83
|
-
"@multiplatform.one/components": "6.4.
|
|
84
|
-
"@multiplatform.one/forms": "6.4.
|
|
85
|
-
"@multiplatform.one/platform": "6.4.
|
|
86
|
-
"@multiplatform.one/
|
|
87
|
-
"@multiplatform.one/
|
|
88
|
-
"@multiplatform.one/
|
|
89
|
-
"@multiplatform.one/theme": "6.4.
|
|
83
|
+
"@multiplatform.one/components": "6.4.2",
|
|
84
|
+
"@multiplatform.one/forms": "6.4.2",
|
|
85
|
+
"@multiplatform.one/platform": "6.4.2",
|
|
86
|
+
"@multiplatform.one/rich-text": "6.4.2",
|
|
87
|
+
"@multiplatform.one/router": "6.4.2",
|
|
88
|
+
"@multiplatform.one/table-primitives": "6.4.2",
|
|
89
|
+
"@multiplatform.one/theme": "6.4.2"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
92
|
"@tamagui/build": "2.0.0-rc.41",
|
|
@@ -99,10 +99,10 @@
|
|
|
99
99
|
"react": "19.2.5",
|
|
100
100
|
"react-dom": "19.2.5",
|
|
101
101
|
"vitest": "^4.1.5",
|
|
102
|
-
"@multiplatform.one/
|
|
103
|
-
"@multiplatform.one/
|
|
104
|
-
"@multiplatform.one/
|
|
105
|
-
"@multiplatform.one/
|
|
102
|
+
"@multiplatform.one/config": "6.4.2",
|
|
103
|
+
"@multiplatform.one/router": "6.4.2",
|
|
104
|
+
"@multiplatform.one/storybook": "6.4.2",
|
|
105
|
+
"@multiplatform.one/test-utils": "6.4.2"
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
108
108
|
"i18next": "^25.0.0",
|
|
@@ -657,7 +657,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
|
|
|
657
657
|
aria-describedby={ariaDescribedBy}
|
|
658
658
|
aria-live="polite"
|
|
659
659
|
aria-atomic="false"
|
|
660
|
-
{...knobProps.
|
|
660
|
+
{...knobProps.containerRadius}
|
|
661
661
|
// UX-011 / Axiom 6 HONEST STATE: `disabled` makes the whole
|
|
662
662
|
// assembly inert (pointerEvents none blocks every touch) and
|
|
663
663
|
// says so (aria-disabled). LC-40 assembly fragment carries the
|
|
@@ -296,11 +296,14 @@ export function DataTableColumnPinning<TData>(props: DataTableColumnPinningProps
|
|
|
296
296
|
);
|
|
297
297
|
}, [table, pinnedColumns]);
|
|
298
298
|
|
|
299
|
+
// Pinned cells are R-NONE/SF-TRANSPARENT like every other cell — they carry
|
|
300
|
+
// only the sticky occlusion fill and their own seam border, so they take the
|
|
301
|
+
// radius-only fragment (D-03); a border-bearing one ringed each pinned cell.
|
|
299
302
|
const ThemedStickyColumn = useCallback(
|
|
300
303
|
(props: React.ComponentProps<typeof StickyColumn>) => (
|
|
301
|
-
<StickyColumn {...knobProps.
|
|
304
|
+
<StickyColumn {...knobProps.containerRadius} {...props} />
|
|
302
305
|
),
|
|
303
|
-
[knobProps.
|
|
306
|
+
[knobProps.containerRadius],
|
|
304
307
|
);
|
|
305
308
|
|
|
306
309
|
return {
|
|
@@ -96,7 +96,7 @@ export function DataTableFilterChips() {
|
|
|
96
96
|
tabIndex={0}
|
|
97
97
|
aria-label={t("Edit filter {{label}}", { label: chip.label })}
|
|
98
98
|
cursor="pointer"
|
|
99
|
-
{...knobProps.
|
|
99
|
+
{...knobProps.containerRadius}
|
|
100
100
|
focusVisibleStyle={{
|
|
101
101
|
outlineColor: "$outlineColor",
|
|
102
102
|
outlineWidth: 2,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { action } from "@multiplatform.one/storybook";
|
|
2
2
|
import React from "react";
|
|
3
|
+
import { formatCurrency } from "@multiplatform.one/theme";
|
|
3
4
|
import { Button } from "@multiplatform.one/forms";
|
|
4
5
|
import { Card, Text, XStack, YStack } from "tamagui";
|
|
5
6
|
import { type AggregationConfig, DataTableGrouping, type GroupingConfig } from "./index";
|
|
@@ -101,7 +102,7 @@ const columns = [
|
|
|
101
102
|
{
|
|
102
103
|
accessorKey: "totalRevenue",
|
|
103
104
|
header: "Revenue",
|
|
104
|
-
cell: (info: any) =>
|
|
105
|
+
cell: (info: any) => formatCurrency(info.getValue(), { symbol: "$", fractionDigits: "auto" }),
|
|
105
106
|
},
|
|
106
107
|
{
|
|
107
108
|
accessorKey: "date",
|
|
@@ -273,7 +274,10 @@ export const CustomGroupHeaders = () => {
|
|
|
273
274
|
</Text>
|
|
274
275
|
</YStack>
|
|
275
276
|
<Text fontSize="$3" fontWeight="500" color="$blue11">
|
|
276
|
-
|
|
277
|
+
{formatCurrency(group.aggregations?.totalRevenue?.sum ?? 0, {
|
|
278
|
+
symbol: "$",
|
|
279
|
+
fractionDigits: "auto",
|
|
280
|
+
})}
|
|
277
281
|
</Text>
|
|
278
282
|
</XStack>
|
|
279
283
|
</YStack>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTableGroupingConfig — coverage arm of G-15 story honesty (D-05 rider).
|
|
3
|
+
*
|
|
4
|
+
* `DataTableGroupingConfig` is a public export of @multiplatform.one/table
|
|
5
|
+
* that shipped with no story at all (audit-tc §1.2, audit-table-chrome-
|
|
6
|
+
* functional "coverage gap"), so its trigger badge and configuration popover
|
|
7
|
+
* could never be screenshot-verified from the index. These stories give the
|
|
8
|
+
* component a home next to its implementation.
|
|
9
|
+
*
|
|
10
|
+
* Stories compose catalog components only and are network-hermetic.
|
|
11
|
+
*/
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { Text, XStack, YStack } from "@multiplatform.one/components";
|
|
14
|
+
import { DataTableGroupingConfig, type GroupingConfig } from "./index";
|
|
15
|
+
|
|
16
|
+
const columns = [
|
|
17
|
+
{ accessorKey: "region", header: "Region", type: "string" as const },
|
|
18
|
+
{ accessorKey: "category", header: "Category", type: "string" as const },
|
|
19
|
+
{ accessorKey: "salesperson", header: "Salesperson", type: "string" as const },
|
|
20
|
+
{ accessorKey: "quantity", header: "Quantity", type: "number" as const },
|
|
21
|
+
{ accessorKey: "totalRevenue", header: "Revenue", type: "number" as const },
|
|
22
|
+
{ accessorKey: "date", header: "Date", type: "date" as const },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
title: "Table/DataTableGroupingConfig",
|
|
27
|
+
component: DataTableGroupingConfig,
|
|
28
|
+
parameters: {
|
|
29
|
+
status: { type: "beta" },
|
|
30
|
+
docs: {
|
|
31
|
+
description: {
|
|
32
|
+
component:
|
|
33
|
+
"Trigger + popover that edits a GroupingConfig (group-by columns and aggregations). The trigger carries a count badge once either list is non-empty. Standalone here; inside a DataTable it reads columns from DataTableStateContext.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Live config state so the popover edits (and the badge count) are real. */
|
|
40
|
+
function ConfigHarness({
|
|
41
|
+
initial,
|
|
42
|
+
disabled = false,
|
|
43
|
+
}: {
|
|
44
|
+
initial: GroupingConfig;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
const [config, setConfig] = useState<GroupingConfig>(initial);
|
|
48
|
+
return (
|
|
49
|
+
<YStack gap="$3" padding="$4" minHeight={420} alignItems="flex-start">
|
|
50
|
+
<DataTableGroupingConfig
|
|
51
|
+
columns={columns}
|
|
52
|
+
currentConfig={config}
|
|
53
|
+
onConfigChange={setConfig}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
/>
|
|
56
|
+
{/* Echo the committed config so a screenshot proves the edit landed. */}
|
|
57
|
+
<YStack gap="$1">
|
|
58
|
+
<Text fontSize="$2" color="$color11">
|
|
59
|
+
Group by: {config.columns.length > 0 ? config.columns.join(", ") : "(none)"}
|
|
60
|
+
</Text>
|
|
61
|
+
<Text fontSize="$2" color="$color11">
|
|
62
|
+
Aggregations:{" "}
|
|
63
|
+
{config.aggregations.length > 0
|
|
64
|
+
? config.aggregations.map((a) => `${a.function}(${a.column})`).join(", ")
|
|
65
|
+
: "(none)"}
|
|
66
|
+
</Text>
|
|
67
|
+
</YStack>
|
|
68
|
+
</YStack>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const emptyConfig: GroupingConfig = { columns: [], aggregations: [] };
|
|
73
|
+
|
|
74
|
+
/** Rest state: nothing grouped yet, so the trigger shows no count badge. */
|
|
75
|
+
export const Default = () => <ConfigHarness initial={emptyConfig} />;
|
|
76
|
+
Default.storyName = "Main";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One group-by column and one aggregation: the trigger badge reads 2. This is
|
|
80
|
+
* the badge that audit-table-chrome-functional fixed from a theme-blind
|
|
81
|
+
* `color="white"` to `$color12` and could not screenshot-verify without a story.
|
|
82
|
+
*/
|
|
83
|
+
export const WithBadgeCount = () => (
|
|
84
|
+
<ConfigHarness
|
|
85
|
+
initial={{
|
|
86
|
+
columns: ["region"],
|
|
87
|
+
aggregations: [{ column: "totalRevenue", function: "sum", label: "Revenue" }],
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/** Multi-level grouping plus several aggregations — the popover's scroll case. */
|
|
93
|
+
export const ManySelections = () => (
|
|
94
|
+
<ConfigHarness
|
|
95
|
+
initial={{
|
|
96
|
+
columns: ["region", "category", "salesperson"],
|
|
97
|
+
aggregations: [
|
|
98
|
+
{ column: "totalRevenue", function: "sum", label: "Revenue" },
|
|
99
|
+
{ column: "quantity", function: "avg", label: "Avg qty" },
|
|
100
|
+
{ column: "quantity", function: "max", label: "Max qty" },
|
|
101
|
+
{ column: "totalRevenue", function: "min", label: "Min revenue" },
|
|
102
|
+
],
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
/** Disabled trigger: the control stays legible rather than vanishing (LC-40). */
|
|
108
|
+
export const Disabled = () => (
|
|
109
|
+
<ConfigHarness initial={{ columns: ["region"], aggregations: [] }} disabled />
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
/** No columns to offer — the popover must say so instead of rendering empty chrome. */
|
|
113
|
+
export const NoColumns = () => {
|
|
114
|
+
const [config, setConfig] = useState<GroupingConfig>(emptyConfig);
|
|
115
|
+
return (
|
|
116
|
+
<XStack padding="$4" minHeight={420}>
|
|
117
|
+
<DataTableGroupingConfig columns={[]} currentConfig={config} onConfigChange={setConfig} />
|
|
118
|
+
</XStack>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
GearIcon,
|
|
12
12
|
} from "@phosphor-icons/react";
|
|
13
13
|
import { useDirection } from "@multiplatform.one/components";
|
|
14
|
-
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
14
|
+
import { formatNumber, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
15
15
|
import { useCallback, useMemo, useState } from "react";
|
|
16
16
|
import { Button, Select } from "@multiplatform.one/forms";
|
|
17
17
|
import { Popover, Separator, Text, View, XStack, YStack, isWeb } from "tamagui";
|
|
@@ -88,6 +88,30 @@ const groupMetaSizeMap: Record<string, string> = {
|
|
|
88
88
|
$5: "$4",
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Group aggregates are the same number class as the cells they summarise, so
|
|
93
|
+
* they render through the house number register (LC-12 render channel) —
|
|
94
|
+
* "15,400", never the raw `15400` beside a localized cell. Counts are whole
|
|
95
|
+
* by construction; a custom aggregation may return anything, and a
|
|
96
|
+
* non-numeric result passes through as its own text.
|
|
97
|
+
*/
|
|
98
|
+
export function formatAggregateValue(value: unknown, agg: AggregationConfig): string {
|
|
99
|
+
if (value == null) return formatNumber(0);
|
|
100
|
+
if (typeof value === "number") {
|
|
101
|
+
// The aggregation math is NaN-guarded upstream; keep the zero floor so a
|
|
102
|
+
// guard slipping through still reads as a number, not a hole.
|
|
103
|
+
if (!Number.isFinite(value)) return formatNumber(0);
|
|
104
|
+
return agg.function === "count"
|
|
105
|
+
? formatNumber(value, { maximumFractionDigits: 0 })
|
|
106
|
+
: formatNumber(value);
|
|
107
|
+
}
|
|
108
|
+
if (typeof value === "string") {
|
|
109
|
+
const asNumber = Number(value);
|
|
110
|
+
return value.trim() !== "" && Number.isFinite(asNumber) ? formatNumber(asNumber) : value;
|
|
111
|
+
}
|
|
112
|
+
return String(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
export function DataTableGrouping<T = any>(props: DataTableGroupingProps<T>) {
|
|
92
116
|
// Try to consume from DataTableStateContext (compound component pattern)
|
|
93
117
|
const ctx = useDataTableState();
|
|
@@ -345,7 +369,7 @@ function GroupHeader<T = any>({
|
|
|
345
369
|
{agg.label || `${agg.function}(${agg.column})`}:
|
|
346
370
|
</Text>
|
|
347
371
|
<Text fontSize={groupMetaSizeMap[knobProps.sizeToken] ?? "$3"} fontWeight="500">
|
|
348
|
-
{group.aggregations[agg.column]?.[agg.function]
|
|
372
|
+
{formatAggregateValue(group.aggregations[agg.column]?.[agg.function], agg)}
|
|
349
373
|
</Text>
|
|
350
374
|
</XStack>
|
|
351
375
|
))}
|
|
@@ -237,7 +237,7 @@ export function SimpleTable({ data, columns, layout = "auto", ...props }: Simple
|
|
|
237
237
|
// Card layout below `$sm` (layout="auto") or when pinned via layout="cards"
|
|
238
238
|
if (layout === "cards" || (layout === "auto" && sm === false)) {
|
|
239
239
|
return (
|
|
240
|
-
<View {...knobProps.
|
|
240
|
+
<View {...knobProps.containerRadius} {...props} testID="simple-table-cards">
|
|
241
241
|
<SimpleTableMobileCards data={data} columns={columns} />
|
|
242
242
|
</View>
|
|
243
243
|
);
|