@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/table",
3
- "version": "6.4.1",
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.1",
84
- "@multiplatform.one/forms": "6.4.1",
85
- "@multiplatform.one/platform": "6.4.1",
86
- "@multiplatform.one/table-primitives": "6.4.1",
87
- "@multiplatform.one/rich-text": "6.4.1",
88
- "@multiplatform.one/router": "6.4.1",
89
- "@multiplatform.one/theme": "6.4.1"
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/router": "6.4.1",
103
- "@multiplatform.one/storybook": "6.4.1",
104
- "@multiplatform.one/test-utils": "6.4.1",
105
- "@multiplatform.one/config": "6.4.1"
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.borderRadius}
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.borderRadius} {...props} />
304
+ <StickyColumn {...knobProps.containerRadius} {...props} />
302
305
  ),
303
- [knobProps.borderRadius],
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.borderRadius}
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) => `$${info.getValue().toLocaleString()}`,
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
- ${group.aggregations?.totalRevenue?.sum?.toLocaleString() || 0}
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] || 0}
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.borderRadius} {...props} testID="simple-table-cards">
240
+ <View {...knobProps.containerRadius} {...props} testID="simple-table-cards">
241
241
  <SimpleTableMobileCards data={data} columns={columns} />
242
242
  </View>
243
243
  );