@multiplatform.one/table 7.28.1 → 7.29.1

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 (178) hide show
  1. package/dist/cjs/components/DataTable/CellContent.cjs +77 -0
  2. package/dist/cjs/components/DataTable/CellContent.native.js +88 -0
  3. package/dist/cjs/components/DataTable/CellContent.native.js.map +1 -0
  4. package/dist/cjs/components/DataTable/DataTableInfiniteTail.cjs +90 -0
  5. package/dist/cjs/components/DataTable/DataTableInfiniteTail.native.js +99 -0
  6. package/dist/cjs/components/DataTable/DataTableInfiniteTail.native.js.map +1 -0
  7. package/dist/cjs/components/DataTable/DataTableRoot.cjs +17 -47
  8. package/dist/cjs/components/DataTable/DataTableRoot.native.js +17 -56
  9. package/dist/cjs/components/DataTable/DataTableRoot.native.js.map +1 -1
  10. package/dist/cjs/components/DataTable/NativeInfiniteSentinel.cjs +62 -0
  11. package/dist/cjs/components/DataTable/NativeInfiniteSentinel.native.js +67 -0
  12. package/dist/cjs/components/DataTable/NativeInfiniteSentinel.native.js.map +1 -0
  13. package/dist/cjs/components/DataTable/index.native.js +75 -15
  14. package/dist/cjs/components/DataTable/index.native.js.map +1 -1
  15. package/dist/cjs/components/DataTableBulkActions/index.cjs +70 -51
  16. package/dist/cjs/components/DataTableBulkActions/index.native.js +84 -63
  17. package/dist/cjs/components/DataTableBulkActions/index.native.js.map +1 -1
  18. package/dist/cjs/components/DataTableCell/index.cjs +3 -1
  19. package/dist/cjs/components/DataTableCell/index.native.js +3 -1
  20. package/dist/cjs/components/DataTableCell/index.native.js.map +1 -1
  21. package/dist/cjs/components/DataTableDateFilter/index.cjs +2 -1
  22. package/dist/cjs/components/DataTableDateFilter/index.native.js +2 -1
  23. package/dist/cjs/components/DataTableDateFilter/index.native.js.map +1 -1
  24. package/dist/cjs/components/DataTableExport/index.cjs +2 -1
  25. package/dist/cjs/components/DataTableExport/index.native.js +2 -1
  26. package/dist/cjs/components/DataTableExport/index.native.js.map +1 -1
  27. package/dist/cjs/components/DataTableGrouping/index.cjs +2 -2
  28. package/dist/cjs/components/DataTableGrouping/index.native.js +2 -2
  29. package/dist/cjs/components/DataTableViewOptions/index.cjs +1 -1
  30. package/dist/cjs/components/DataTableViewOptions/index.native.js +1 -1
  31. package/dist/cjs/components/DataTableViewOptions/index.native.js.map +1 -1
  32. package/dist/cjs/components/KnobDialog.cjs +2 -1
  33. package/dist/cjs/components/KnobDialog.native.js +4 -2
  34. package/dist/cjs/components/KnobDialog.native.js.map +1 -1
  35. package/dist/cjs/components/TableInput/RowDetailEditor.cjs +79 -81
  36. package/dist/cjs/components/TableInput/index.cjs +20 -19
  37. package/dist/cjs/components/TableInput/index.native.js +1 -0
  38. package/dist/cjs/components/TableInput/index.native.js.map +1 -1
  39. package/dist/esm/components/DataTable/CellContent.mjs +52 -0
  40. package/dist/esm/components/DataTable/CellContent.mjs.map +1 -0
  41. package/dist/esm/components/DataTable/CellContent.native.js +60 -0
  42. package/dist/esm/components/DataTable/CellContent.native.js.map +1 -0
  43. package/dist/esm/components/DataTable/DataTableInfiniteTail.mjs +65 -0
  44. package/dist/esm/components/DataTable/DataTableInfiniteTail.mjs.map +1 -0
  45. package/dist/esm/components/DataTable/DataTableInfiniteTail.native.js +71 -0
  46. package/dist/esm/components/DataTable/DataTableInfiniteTail.native.js.map +1 -0
  47. package/dist/esm/components/DataTable/DataTableRoot.mjs +18 -48
  48. package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
  49. package/dist/esm/components/DataTable/DataTableRoot.native.js +18 -57
  50. package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
  51. package/dist/esm/components/DataTable/NativeInfiniteSentinel.mjs +36 -0
  52. package/dist/esm/components/DataTable/NativeInfiniteSentinel.mjs.map +1 -0
  53. package/dist/esm/components/DataTable/NativeInfiniteSentinel.native.js +38 -0
  54. package/dist/esm/components/DataTable/NativeInfiniteSentinel.native.js.map +1 -0
  55. package/dist/esm/components/DataTable/index.native.js +77 -17
  56. package/dist/esm/components/DataTable/index.native.js.map +1 -1
  57. package/dist/esm/components/DataTableBulkActions/index.mjs +71 -52
  58. package/dist/esm/components/DataTableBulkActions/index.mjs.map +1 -1
  59. package/dist/esm/components/DataTableBulkActions/index.native.js +85 -64
  60. package/dist/esm/components/DataTableBulkActions/index.native.js.map +1 -1
  61. package/dist/esm/components/DataTableCell/index.mjs +3 -1
  62. package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
  63. package/dist/esm/components/DataTableCell/index.native.js +3 -1
  64. package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
  65. package/dist/esm/components/DataTableDateFilter/index.mjs +2 -1
  66. package/dist/esm/components/DataTableDateFilter/index.mjs.map +1 -1
  67. package/dist/esm/components/DataTableDateFilter/index.native.js +2 -1
  68. package/dist/esm/components/DataTableDateFilter/index.native.js.map +1 -1
  69. package/dist/esm/components/DataTableExport/index.mjs +2 -1
  70. package/dist/esm/components/DataTableExport/index.mjs.map +1 -1
  71. package/dist/esm/components/DataTableExport/index.native.js +2 -1
  72. package/dist/esm/components/DataTableExport/index.native.js.map +1 -1
  73. package/dist/esm/components/DataTableGrouping/index.mjs +2 -2
  74. package/dist/esm/components/DataTableGrouping/index.native.js +2 -2
  75. package/dist/esm/components/DataTableViewOptions/index.mjs +2 -2
  76. package/dist/esm/components/DataTableViewOptions/index.mjs.map +1 -1
  77. package/dist/esm/components/DataTableViewOptions/index.native.js +2 -2
  78. package/dist/esm/components/DataTableViewOptions/index.native.js.map +1 -1
  79. package/dist/esm/components/KnobDialog.mjs +2 -1
  80. package/dist/esm/components/KnobDialog.mjs.map +1 -1
  81. package/dist/esm/components/KnobDialog.native.js +4 -2
  82. package/dist/esm/components/KnobDialog.native.js.map +1 -1
  83. package/dist/esm/components/TableInput/RowDetailEditor.mjs +81 -83
  84. package/dist/esm/components/TableInput/RowDetailEditor.mjs.map +1 -1
  85. package/dist/esm/components/TableInput/index.mjs +20 -19
  86. package/dist/esm/components/TableInput/index.mjs.map +1 -1
  87. package/dist/esm/components/TableInput/index.native.js +1 -0
  88. package/dist/esm/components/TableInput/index.native.js.map +1 -1
  89. package/dist/jsx/components/DataTable/CellContent.mjs +52 -0
  90. package/dist/jsx/components/DataTable/CellContent.mjs.map +1 -0
  91. package/dist/jsx/components/DataTable/CellContent.native.js +88 -0
  92. package/dist/jsx/components/DataTable/CellContent.native.js.map +1 -0
  93. package/dist/jsx/components/DataTable/DataTableInfiniteTail.mjs +65 -0
  94. package/dist/jsx/components/DataTable/DataTableInfiniteTail.mjs.map +1 -0
  95. package/dist/jsx/components/DataTable/DataTableInfiniteTail.native.js +99 -0
  96. package/dist/jsx/components/DataTable/DataTableInfiniteTail.native.js.map +1 -0
  97. package/dist/jsx/components/DataTable/DataTableRoot.mjs +18 -48
  98. package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
  99. package/dist/jsx/components/DataTable/DataTableRoot.native.js +17 -56
  100. package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
  101. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.mjs +36 -0
  102. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.mjs.map +1 -0
  103. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.native.js +67 -0
  104. package/dist/jsx/components/DataTable/NativeInfiniteSentinel.native.js.map +1 -0
  105. package/dist/jsx/components/DataTable/index.native.js +75 -15
  106. package/dist/jsx/components/DataTable/index.native.js.map +1 -1
  107. package/dist/jsx/components/DataTableBulkActions/index.mjs +71 -52
  108. package/dist/jsx/components/DataTableBulkActions/index.mjs.map +1 -1
  109. package/dist/jsx/components/DataTableBulkActions/index.native.js +84 -63
  110. package/dist/jsx/components/DataTableBulkActions/index.native.js.map +1 -1
  111. package/dist/jsx/components/DataTableCell/index.mjs +3 -1
  112. package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
  113. package/dist/jsx/components/DataTableCell/index.native.js +3 -1
  114. package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
  115. package/dist/jsx/components/DataTableDateFilter/index.mjs +2 -1
  116. package/dist/jsx/components/DataTableDateFilter/index.mjs.map +1 -1
  117. package/dist/jsx/components/DataTableDateFilter/index.native.js +2 -1
  118. package/dist/jsx/components/DataTableDateFilter/index.native.js.map +1 -1
  119. package/dist/jsx/components/DataTableExport/index.mjs +2 -1
  120. package/dist/jsx/components/DataTableExport/index.mjs.map +1 -1
  121. package/dist/jsx/components/DataTableExport/index.native.js +2 -1
  122. package/dist/jsx/components/DataTableExport/index.native.js.map +1 -1
  123. package/dist/jsx/components/DataTableGrouping/index.mjs +2 -2
  124. package/dist/jsx/components/DataTableGrouping/index.native.js +2 -2
  125. package/dist/jsx/components/DataTableViewOptions/index.mjs +2 -2
  126. package/dist/jsx/components/DataTableViewOptions/index.mjs.map +1 -1
  127. package/dist/jsx/components/DataTableViewOptions/index.native.js +1 -1
  128. package/dist/jsx/components/DataTableViewOptions/index.native.js.map +1 -1
  129. package/dist/jsx/components/KnobDialog.mjs +2 -1
  130. package/dist/jsx/components/KnobDialog.mjs.map +1 -1
  131. package/dist/jsx/components/KnobDialog.native.js +4 -2
  132. package/dist/jsx/components/KnobDialog.native.js.map +1 -1
  133. package/dist/jsx/components/TableInput/RowDetailEditor.mjs +81 -83
  134. package/dist/jsx/components/TableInput/RowDetailEditor.mjs.map +1 -1
  135. package/dist/jsx/components/TableInput/index.mjs +20 -19
  136. package/dist/jsx/components/TableInput/index.mjs.map +1 -1
  137. package/dist/jsx/components/TableInput/index.native.js +1 -0
  138. package/dist/jsx/components/TableInput/index.native.js.map +1 -1
  139. package/package.json +12 -12
  140. package/src/components/ChildTable/childTable.rowDetail.spec.tsx +69 -0
  141. package/src/components/DataTable/CellContent.tsx +73 -0
  142. package/src/components/DataTable/DataTableInfiniteTail.tsx +94 -0
  143. package/src/components/DataTable/DataTableRoot.tsx +37 -81
  144. package/src/components/DataTable/NativeInfiniteSentinel.tsx +45 -0
  145. package/src/components/DataTable/cellWeight.native.spec.tsx +82 -0
  146. package/src/components/DataTable/cellWeight.spec.tsx +147 -0
  147. package/src/components/DataTable/index.native.tsx +108 -19
  148. package/src/components/DataTable/nativeRoot.spec.tsx +180 -0
  149. package/src/components/DataTableBulkActions/index.tsx +100 -71
  150. package/src/components/DataTableCell/index.tsx +3 -2
  151. package/src/components/DataTableDateFilter/index.tsx +2 -10
  152. package/src/components/DataTableExport/index.tsx +2 -1
  153. package/src/components/DataTableFilters/DataTableFilters.stories.tsx +7 -19
  154. package/src/components/DataTableGrouping/index.tsx +2 -2
  155. package/src/components/DataTablePagination/DataTablePagination.stories.tsx +12 -46
  156. package/src/components/DataTableViewOptions/index.tsx +2 -2
  157. package/src/components/KnobDialog.tsx +2 -0
  158. package/src/components/TableInput/RowDetailEditor.tsx +73 -72
  159. package/src/components/TableInput/index.native.tsx +1 -0
  160. package/src/components/TableInput/index.tsx +30 -21
  161. package/types/components/DataTable/CellContent.d.ts +19 -0
  162. package/types/components/DataTable/CellContent.d.ts.map +1 -0
  163. package/types/components/DataTable/DataTableInfiniteTail.d.ts +22 -0
  164. package/types/components/DataTable/DataTableInfiniteTail.d.ts.map +1 -0
  165. package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
  166. package/types/components/DataTable/NativeInfiniteSentinel.d.ts +17 -0
  167. package/types/components/DataTable/NativeInfiniteSentinel.d.ts.map +1 -0
  168. package/types/components/DataTable/index.native.d.ts +1 -1
  169. package/types/components/DataTable/index.native.d.ts.map +1 -1
  170. package/types/components/DataTableBulkActions/index.d.ts.map +1 -1
  171. package/types/components/DataTableCell/index.d.ts.map +1 -1
  172. package/types/components/DataTableDateFilter/index.d.ts.map +1 -1
  173. package/types/components/DataTableExport/index.d.ts.map +1 -1
  174. package/types/components/KnobDialog.d.ts.map +1 -1
  175. package/types/components/TableInput/RowDetailEditor.d.ts +4 -0
  176. package/types/components/TableInput/RowDetailEditor.d.ts.map +1 -1
  177. package/types/components/TableInput/index.d.ts.map +1 -1
  178. package/types/components/TableInput/index.native.d.ts.map +1 -1
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The native root honours the props the web root does (MPO-454).
3
+ *
4
+ * index.native.tsx handed useDataTable a subset of the web root's options
5
+ * and never drew a bulk bar, so on iOS the backoffice list (infinite server
6
+ * mode, rows only through `loadPage`) showed "No data available", and
7
+ * checked rows offered no Export CSV. These specs render the native root
8
+ * itself: the suite runs on web, so `react-native` resolves to
9
+ * react-native-web and FlashList, which the list layout never mounts, is
10
+ * stubbed.
11
+ */
12
+
13
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
14
+ import { fireEvent, screen, waitFor } from "@testing-library/react";
15
+ import { readFileSync } from "node:fs";
16
+ import { resolve } from "node:path";
17
+ import { afterEach, describe, expect, it, vi } from "vitest";
18
+ import type { BulkAction, DataTableColumn, PageLoadQuery, PageLoadResult } from "../../types";
19
+ import { DataTable } from "./index.native";
20
+
21
+ vi.mock("@shopify/flash-list", () => ({ FlashList: () => null }));
22
+
23
+ type Row = { id: number; name: string };
24
+
25
+ const rows: Row[] = Array.from({ length: 50 }, (_, i) => ({ id: i + 1, name: `Row ${i + 1}` }));
26
+
27
+ const columns: DataTableColumn<Row>[] = [
28
+ { accessorKey: "name", header: "Name", meta: { role: "title" } },
29
+ { accessorKey: "id", header: "ID" },
30
+ ];
31
+
32
+ function pageLoader() {
33
+ return vi.fn(async (q: PageLoadQuery): Promise<PageLoadResult<Row>> => {
34
+ const start = q.pageIndex * q.pageSize;
35
+ return { rows: rows.slice(start, start + q.pageSize), totalCount: rows.length };
36
+ });
37
+ }
38
+
39
+ function renderInfinite(loadPage: ReturnType<typeof pageLoader>) {
40
+ return renderWithProviders(
41
+ <DataTable
42
+ data={[]}
43
+ columns={columns}
44
+ layout="list"
45
+ readOnly
46
+ paginationMode="infinite"
47
+ loadPage={loadPage}
48
+ initialState={{ pagination: { pageIndex: 0, pageSize: 10 } }}
49
+ />,
50
+ );
51
+ }
52
+
53
+ /** Where the tail sentinel reports itself on screen, via measureInWindow. */
54
+ function placeSentinel(top: number) {
55
+ const real = HTMLElement.prototype.getBoundingClientRect;
56
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
57
+ function (this: HTMLElement) {
58
+ if (this.getAttribute("data-testid") !== "infinite-sentinel") return real.call(this);
59
+ return {
60
+ x: 0,
61
+ y: top,
62
+ top,
63
+ left: 0,
64
+ right: 320,
65
+ bottom: top + 1,
66
+ width: 320,
67
+ height: 1,
68
+ } as DOMRect;
69
+ },
70
+ );
71
+ }
72
+
73
+ afterEach(() => {
74
+ vi.restoreAllMocks();
75
+ });
76
+
77
+ describe("native DataTable infinite mode", () => {
78
+ it("renders page 0 from loadPage instead of the empty state", async () => {
79
+ const loadPage = pageLoader();
80
+ renderInfinite(loadPage);
81
+ expect(await screen.findByText("Row 1")).toBeDefined();
82
+ expect(screen.getByText("Row 10")).toBeDefined();
83
+ expect(screen.queryByText("Row 11")).toBeNull();
84
+ expect(screen.queryByText("No data available")).toBeNull();
85
+ expect(loadPage.mock.calls[0]![0].pageIndex).toBe(0);
86
+ expect(screen.getByTestId("infinite-status").textContent).toContain("50");
87
+ expect(screen.queryByLabelText("Go to next page")).toBeNull();
88
+ });
89
+
90
+ it("appends the next page once the tail sentinel comes on screen", async () => {
91
+ placeSentinel(200);
92
+ const loadPage = pageLoader();
93
+ renderInfinite(loadPage);
94
+ expect(await screen.findByText("Row 11", undefined, { timeout: 3000 })).toBeDefined();
95
+ expect(screen.getByText("Row 20")).toBeDefined();
96
+ });
97
+
98
+ it("holds the next page while the tail sentinel is below the fold", async () => {
99
+ placeSentinel(100_000);
100
+ const loadPage = pageLoader();
101
+ renderInfinite(loadPage);
102
+ expect(await screen.findByText("Row 10")).toBeDefined();
103
+ await waitFor(() => expect(loadPage.mock.calls.map(([q]) => q.pageIndex)).toEqual([0, 1]));
104
+ await new Promise((settle) => setTimeout(settle, 800));
105
+ expect(screen.queryByText("Row 11")).toBeNull();
106
+ });
107
+ });
108
+
109
+ describe("native DataTable bulk actions", () => {
110
+ it("checking rows shows the bulk bar, and an action receives the checked rows", async () => {
111
+ const exportCsv = vi.fn();
112
+ const bulkActions: BulkAction<Row>[] = [
113
+ { id: "assign", label: "Assign To", onExecute: vi.fn() },
114
+ { id: "export", label: "Export CSV", onExecute: exportCsv },
115
+ ];
116
+ renderWithProviders(
117
+ <DataTable
118
+ data={rows.slice(0, 3)}
119
+ columns={columns}
120
+ layout="list"
121
+ readOnly
122
+ enablePagination={false}
123
+ enableRowSelection
124
+ bulkActions={bulkActions}
125
+ />,
126
+ );
127
+ expect(screen.queryByText("Export CSV")).toBeNull();
128
+
129
+ fireEvent.click(screen.getByRole("checkbox", { name: "Select row Row 1" }));
130
+ fireEvent.click(screen.getByRole("checkbox", { name: "Select row Row 3" }));
131
+
132
+ expect(await screen.findByText("2 selected")).toBeDefined();
133
+ expect(screen.getByRole("button", { name: "Assign To" })).toBeDefined();
134
+ fireEvent.click(screen.getByRole("button", { name: "Export CSV" }));
135
+ await waitFor(() => expect(exportCsv).toHaveBeenCalledTimes(1));
136
+ expect(exportCsv.mock.calls[0]![0].map((row: Row) => row.name)).toEqual(["Row 1", "Row 3"]);
137
+ });
138
+ });
139
+
140
+ describe("native DataTable root parity with web", () => {
141
+ const read = (file: string) => readFileSync(resolve(__dirname, file), "utf-8");
142
+
143
+ /** Option names a root hands useDataTable. */
144
+ function useDataTableOptions(source: string): Set<string> {
145
+ const start = source.indexOf("useDataTable({");
146
+ const body = source.slice(start, source.indexOf("});", start));
147
+ return new Set([...body.matchAll(/^\s*([A-Za-z]+)\s*[,:]/gm)].map((match) => match[1]!));
148
+ }
149
+
150
+ /** Prop names every `<Component ... />` call site in a root passes. */
151
+ function propNames(source: string, component: string): Set<string> {
152
+ const names = new Set<string>();
153
+ for (const site of source.split(`<${component}`).slice(1)) {
154
+ // Stop at the element's end or at a nested element, whichever is first.
155
+ const end = site.search(/\/>|</);
156
+ const element = end === -1 ? site : site.slice(0, end);
157
+ for (const match of element.matchAll(/(?:^|\s)([A-Za-z][A-Za-z0-9]*)=[{"]/g)) {
158
+ names.add(match[1]!);
159
+ }
160
+ }
161
+ return names;
162
+ }
163
+
164
+ it("hands useDataTable every option the web root does", () => {
165
+ const web = useDataTableOptions(read("DataTableRoot.tsx"));
166
+ const native = useDataTableOptions(read("index.native.tsx"));
167
+ expect(web.size).toBeGreaterThan(0);
168
+ expect([...web].filter((option) => !native.has(option)).sort()).toEqual([]);
169
+ });
170
+
171
+ it.each(["DataTableBulkActions", "DataTableInfiniteTail"])(
172
+ "renders %s with every prop web passes",
173
+ (component) => {
174
+ const web = propNames(read("DataTableRoot.tsx"), component);
175
+ const native = propNames(read("index.native.tsx"), component);
176
+ expect(web.size).toBeGreaterThan(0);
177
+ expect([...web].filter((prop) => !native.has(prop)).sort()).toEqual([]);
178
+ },
179
+ );
180
+ });
@@ -4,7 +4,7 @@ import { useState } from "react";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import { Button, zIndex } from "@multiplatform.one/forms";
6
6
  import { ConfirmDialog, DropdownMenu } from "@multiplatform.one/components";
7
- import { Text, View, XStack } from "tamagui";
7
+ import { Text, View, XStack, isWeb, useMedia } from "tamagui";
8
8
  import type { BulkAction } from "../../types";
9
9
  import { iconSizeForToken } from "../../utils/chromeIcons";
10
10
  import { withInterp } from "../../utils/withInterp";
@@ -53,6 +53,10 @@ export function DataTableBulkActions<T = any>(props: DataTableBulkActionsProps<T
53
53
  // on-fill foreground) measured 1.88:1 as dark-scheme page text.
54
54
  const countColor = useAccentOnSurface();
55
55
  const placement = placementProp ?? (knobProps.bulkBarPlacement === "bottom" ? "bottom" : "top");
56
+ // Below $sm the actions do not fit one row beside the count and Clear:
57
+ // the count and Clear share the first line and the actions wrap beneath.
58
+ const { sm } = useMedia();
59
+ const stacked = !sm;
56
60
 
57
61
  // DG-TBL-03 scope switch: with page scope, once the visible page is fully
58
62
  // selected offer expanding the selection to every matching (filtered) row.
@@ -121,88 +125,113 @@ export function DataTableBulkActions<T = any>(props: DataTableBulkActionsProps<T
121
125
  return null;
122
126
  }
123
127
 
128
+ const summary = (
129
+ <>
130
+ {showSelectionCount && (
131
+ <Text {...knobProps.label} fontWeight="400" color={countColor}>
132
+ {/* D-15: one whole-sentence key per variant (never concatenated
133
+ fragments — word order is locale-owned); withInterp fills the
134
+ params when no i18n instance is mounted (house table pattern). */}
135
+ {filteredOutCount > 0
136
+ ? withInterp(
137
+ t("{{n}} selected (+{{hidden}} filtered out)", {
138
+ n: selectedItems.length,
139
+ hidden: filteredOutCount,
140
+ }),
141
+ { n: selectedItems.length, hidden: filteredOutCount },
142
+ )
143
+ : withInterp(t("{{n}} selected", { n: selectedItems.length }), {
144
+ n: selectedItems.length,
145
+ })}
146
+ </Text>
147
+ )}
148
+
149
+ {showSelectAllMatching && (
150
+ <Button
151
+ size={knobProps.sizeToken}
152
+ onPress={() => ctx?.table.toggleAllRowsSelected(true)}
153
+ aria-label={`Select all ${matchingCount} matching rows`}
154
+ {...knobProps.borderRadius}
155
+ >
156
+ {`Select all ${matchingCount}`}
157
+ </Button>
158
+ )}
159
+ </>
160
+ );
161
+
162
+ const clearButton = (
163
+ <Button
164
+ size={knobProps.sizeToken}
165
+ onPress={onClearSelection}
166
+ aria-label="Clear selection"
167
+ {...knobProps.borderRadius}
168
+ >
169
+ Clear
170
+ </Button>
171
+ );
172
+
173
+ const actionGroup = (
174
+ <XStack {...knobProps.gap} flexWrap="wrap" {...(stacked ? {} : { flex: 1 })}>
175
+ {visibleActions.map((action) => (
176
+ <Button
177
+ key={action.id}
178
+ size={knobProps.sizeToken}
179
+ error={action.variant === "destructive"}
180
+ disabled={action.disabled}
181
+ // DG-ST-03: reason renders via the Button explain affordance;
182
+ // executing is derived busy state (spinner), not a bare disable.
183
+ disabledReason={action.disabledReason}
184
+ loading={isExecuting === action.id}
185
+ onPress={() => handleExecuteAction(action)}
186
+ {...knobProps.borderRadius}
187
+ >
188
+ {action.label}
189
+ </Button>
190
+ ))}
191
+
192
+ {overflowActions.length > 0 && (
193
+ <BulkActionsOverflowMenu
194
+ actions={overflowActions}
195
+ selectedItems={selectedItems}
196
+ onExecuteAction={handleExecuteAction}
197
+ isExecuting={isExecuting}
198
+ />
199
+ )}
200
+ </XStack>
201
+ );
202
+
124
203
  return (
125
204
  <>
126
205
  <View
127
- flexDirection="row"
128
- alignItems="center"
206
+ flexDirection={stacked ? "column" : "row"}
207
+ alignItems={stacked ? "stretch" : "center"}
129
208
  {...knobProps.borderRadius}
130
209
  {...knobProps.panelPadding}
131
210
  gap="$3"
132
211
  backgroundColor="$color2"
133
212
  borderColor="$color6"
134
- position="sticky"
135
- top={placement === "top" ? 0 : undefined}
136
- bottom={placement === "bottom" ? 0 : undefined}
213
+ // Tamagui turns "sticky" into "absolute" on native, which floats the
214
+ // bar over the first rows; native keeps it in flow instead.
215
+ {...(isWeb
216
+ ? {
217
+ position: "sticky" as const,
218
+ top: placement === "top" ? 0 : undefined,
219
+ bottom: placement === "bottom" ? 0 : undefined,
220
+ }
221
+ : {})}
137
222
  zIndex={zIndex.stickyHeader}
138
223
  data-bulk-bar-placement={placement}
139
224
  >
140
- {showSelectionCount && (
141
- <Text {...knobProps.label} fontWeight="400" color={countColor}>
142
- {/* D-15: one whole-sentence key per variant (never concatenated
143
- fragments — word order is locale-owned); withInterp fills the
144
- params when no i18n instance is mounted (house table pattern). */}
145
- {filteredOutCount > 0
146
- ? withInterp(
147
- t("{{n}} selected (+{{hidden}} filtered out)", {
148
- n: selectedItems.length,
149
- hidden: filteredOutCount,
150
- }),
151
- { n: selectedItems.length, hidden: filteredOutCount },
152
- )
153
- : withInterp(t("{{n}} selected", { n: selectedItems.length }), {
154
- n: selectedItems.length,
155
- })}
156
- </Text>
157
- )}
158
-
159
- {showSelectAllMatching && (
160
- <Button
161
- size={knobProps.sizeToken}
162
- onPress={() => ctx?.table.toggleAllRowsSelected(true)}
163
- aria-label={`Select all ${matchingCount} matching rows`}
164
- {...knobProps.borderRadius}
165
- >
166
- {`Select all ${matchingCount}`}
167
- </Button>
225
+ {stacked ? (
226
+ <XStack alignItems="center" justifyContent="space-between" gap="$3">
227
+ {summary}
228
+ {clearButton}
229
+ </XStack>
230
+ ) : (
231
+ summary
168
232
  )}
169
-
170
- <XStack {...knobProps.gap} flex={1}>
171
- {visibleActions.map((action) => (
172
- <Button
173
- key={action.id}
174
- size={knobProps.sizeToken}
175
- error={action.variant === "destructive"}
176
- disabled={action.disabled}
177
- // DG-ST-03: reason renders via the Button explain affordance;
178
- // executing is derived busy state (spinner), not a bare disable.
179
- disabledReason={action.disabledReason}
180
- loading={isExecuting === action.id}
181
- onPress={() => handleExecuteAction(action)}
182
- {...knobProps.borderRadius}
183
- >
184
- {action.label}
185
- </Button>
186
- ))}
187
-
188
- {overflowActions.length > 0 && (
189
- <BulkActionsOverflowMenu
190
- actions={overflowActions}
191
- selectedItems={selectedItems}
192
- onExecuteAction={handleExecuteAction}
193
- isExecuting={isExecuting}
194
- />
195
- )}
196
- </XStack>
197
-
198
- <Button
199
- size={knobProps.sizeToken}
200
- onPress={onClearSelection}
201
- aria-label="Clear selection"
202
- {...knobProps.borderRadius}
203
- >
204
- Clear
205
- </Button>
233
+ {actionGroup}
234
+ {stacked ? null : clearButton}
206
235
  </View>
207
236
 
208
237
  {/* LC-16: confirm routes through the canonical DestructiveAction→ConfirmDialog
@@ -52,9 +52,10 @@ import { CellModalEditor } from "./CellModalEditor";
52
52
  import { CellOptionsEditor } from "./CellOptionsEditor";
53
53
  import { Star } from "@phosphor-icons/react";
54
54
  import { RichTextEditor } from "@multiplatform.one/rich-text";
55
+ import { Text } from "@multiplatform.one/components";
55
56
  import { useResolvedKnobs, ensureCompositeFocusRing } from "@multiplatform.one/theme";
56
57
  import React, { useCallback, useEffect, useRef, useState } from "react";
57
- import { Text, View, isWeb } from "tamagui";
58
+ import { View, isWeb } from "tamagui";
58
59
  import type { DataTableFieldProps, DataTableField } from "../../types";
59
60
  import { alignToFlex, getDefaultColumnAlign } from "../../utils/columnAlign";
60
61
  import { formatMoney } from "../../utils/money";
@@ -191,7 +192,7 @@ export function DataTableCell({
191
192
  typeof rowCurrency === "string" && rowCurrency ? rowCurrency : currency;
192
193
  return (
193
194
  <CellWrapper justify={cellJustify}>
194
- <Text numberOfLines={1} aria-label={cellName}>
195
+ <Text numberOfLines={1} aria-label={cellName} fontSize={knobProps.label.fontSize}>
195
196
  {formatMoney(value, resolvedCurrency, locale)}
196
197
  </Text>
197
198
  </CellWrapper>
@@ -9,16 +9,8 @@ import {
9
9
  import { formatAbsoluteDate, parseCalendarDate, useResolvedKnobs } from "@multiplatform.one/theme";
10
10
  import React, { useState, useMemo } from "react";
11
11
  import { Button, DatePicker, FloatingPanel } from "@multiplatform.one/forms";
12
- import {
13
- ScrollView,
14
- Separator,
15
- Text,
16
- View,
17
- XStack,
18
- YStack,
19
- getTokens,
20
- getVariableValue,
21
- } from "tamagui";
12
+ import { Separator, Text, View, XStack, YStack, getTokens, getVariableValue } from "tamagui";
13
+ import { ScrollView } from "@multiplatform.one/components";
22
14
  import { t } from "../../shared/t";
23
15
  import { iconSizeForToken } from "../../utils/chromeIcons";
24
16
 
@@ -9,7 +9,8 @@ import { downloadBlob } from "@multiplatform.one/platform";
9
9
  import { formatCalendarDate, useResolvedKnobs } from "@multiplatform.one/theme";
10
10
  import { useState } from "react";
11
11
  import { Button, Checkbox, FloatingPanel, Input } from "@multiplatform.one/forms";
12
- import { ScrollView, Separator, Text, XStack, YStack, isWeb } from "tamagui";
12
+ import { Separator, Text, XStack, YStack, isWeb } from "tamagui";
13
+ import { ScrollView } from "@multiplatform.one/components";
13
14
  import type { DataTableField } from "../../types";
14
15
  import { iconSizeForToken } from "../../utils/chromeIcons";
15
16
  import { getViewLeafRows } from "../../utils/viewData";
@@ -1,3 +1,4 @@
1
+ import { Card } from "@multiplatform.one/components";
1
2
  import { action } from "@multiplatform.one/storybook";
2
3
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
4
  import { Text, View, YStack } from "tamagui";
@@ -331,18 +332,12 @@ export const InteractiveDemo: Story = {
331
332
  onClearFilters={handleClearFilters}
332
333
  />
333
334
 
334
- <View
335
- style={{
336
- padding: "16px",
337
- background: "#f5f5f5",
338
- borderRadius: "4px",
339
- }}
340
- >
335
+ <Card>
341
336
  <Text fontSize="$4" fontWeight="bold">
342
337
  Current Filters:
343
338
  </Text>
344
339
  <pre style={{ fontSize: "12px", margin: 0 }}>{JSON.stringify(filters, null, 2)}</pre>
345
- </View>
340
+ </Card>
346
341
  </YStack>
347
342
  );
348
343
  };
@@ -417,16 +412,9 @@ export const Accessibility: Story = {
417
412
  filters: sampleFilters,
418
413
  },
419
414
  render: (args) => (
420
- <View>
415
+ <YStack gap="$4">
421
416
  <DataTableFilters {...args} />
422
- <View
423
- style={{
424
- marginTop: "20px",
425
- padding: "16px",
426
- background: "#f8f9fa",
427
- borderRadius: "4px",
428
- }}
429
- >
417
+ <Card>
430
418
  <Text fontSize="$4" fontWeight="bold">
431
419
  Accessibility Features:
432
420
  </Text>
@@ -437,8 +425,8 @@ export const Accessibility: Story = {
437
425
  <li>Focus management between filter controls</li>
438
426
  <li>Proper semantic markup for filter badges</li>
439
427
  </ul>
440
- </View>
441
- </View>
428
+ </Card>
429
+ </YStack>
442
430
  ),
443
431
  };
444
432
 
@@ -434,7 +434,7 @@ function GroupHeader<T = any>({
434
434
  <Text {...knobProps.label} fontWeight="400" numberOfLines={1}>
435
435
  {label}
436
436
  </Text>
437
- <Text fontSize={metaSize} color="$color10">
437
+ <Text fontSize={metaSize} color="$color11">
438
438
  ({countLabel})
439
439
  </Text>
440
440
  </XStack>
@@ -443,7 +443,7 @@ function GroupHeader<T = any>({
443
443
  <XStack gap="$3" flexWrap="wrap" justifyContent="flex-end">
444
444
  {aggregations.map((agg, index) => (
445
445
  <XStack key={index} alignItems="baseline" gap="$1">
446
- <Text fontSize={metaSize} color="$color10">
446
+ <Text fontSize={metaSize} color="$color11">
447
447
  {`${agg.label || `${agg.function}(${agg.column})`}:`}
448
448
  </Text>
449
449
  <Text {...knobProps.label} fontWeight="400">
@@ -1,3 +1,4 @@
1
+ import { Card } from "@multiplatform.one/components";
1
2
  import { action } from "@multiplatform.one/storybook";
2
3
  import type { GuardrailSpecimenParameter } from "@multiplatform.one/theme";
3
4
  import { Preset } from "@multiplatform.one/theme";
@@ -283,14 +284,7 @@ export const NavigationDemo: Story = {
283
284
  onPaginationChange={handlePaginationChange}
284
285
  />
285
286
 
286
- <View
287
- gap="$2"
288
- style={{
289
- padding: "16px",
290
- background: "#e8f4fd",
291
- borderRadius: "4px",
292
- }}
293
- >
287
+ <Card>
294
288
  <Text fontSize="$5" fontWeight="600">
295
289
  Navigation Controls:
296
290
  </Text>
@@ -344,7 +338,7 @@ export const NavigationDemo: Story = {
344
338
  Last
345
339
  </Button>
346
340
  </XStack>
347
- </View>
341
+ </Card>
348
342
  </YStack>
349
343
  );
350
344
  };
@@ -377,14 +371,7 @@ export const PageSizeDemo: Story = {
377
371
  onPaginationChange={handlePaginationChange}
378
372
  />
379
373
 
380
- <View
381
- gap="$2"
382
- style={{
383
- padding: "16px",
384
- background: "#f0f9ff",
385
- borderRadius: "4px",
386
- }}
387
- >
374
+ <Card>
388
375
  <Text fontSize="$5" fontWeight="600">
389
376
  Page Size Options:
390
377
  </Text>
@@ -394,20 +381,13 @@ export const PageSizeDemo: Story = {
394
381
  key={size}
395
382
  onPress={() => handlePaginationChange({ pageIndex: 0, pageSize: size })}
396
383
  size="$2"
397
- {...({
398
- padding: "$1",
399
- backgroundColor: pagination.pageSize === size ? "$blue10" : "$gray2",
400
- color: pagination.pageSize === size ? "white" : "$gray12",
401
- borderWidth: 1,
402
- borderColor: "$gray6",
403
- borderRadius: "$2",
404
- } as any)}
384
+ accent={pagination.pageSize === size}
405
385
  >
406
386
  {size}
407
387
  </Button>
408
388
  ))}
409
389
  </XStack>
410
- </View>
390
+ </Card>
411
391
  </YStack>
412
392
  );
413
393
  };
@@ -454,14 +434,7 @@ export const PageJumpDemo: Story = {
454
434
  onPaginationChange={handlePaginationChange}
455
435
  />
456
436
 
457
- <View
458
- gap="$2"
459
- style={{
460
- padding: "16px",
461
- background: "#fff3cd",
462
- borderRadius: "4px",
463
- }}
464
- >
437
+ <Card>
465
438
  <Text fontSize="$5" fontWeight="600">
466
439
  Page Jump:
467
440
  </Text>
@@ -483,7 +456,7 @@ export const PageJumpDemo: Story = {
483
456
  Go
484
457
  </Button>
485
458
  </XStack>
486
- </View>
459
+ </Card>
487
460
  </YStack>
488
461
  );
489
462
  };
@@ -508,16 +481,9 @@ export const Accessibility: Story = {
508
481
  ...Basic.args,
509
482
  },
510
483
  render: (args) => (
511
- <View>
484
+ <YStack gap="$4">
512
485
  <DataTablePagination {...args} />
513
- <View
514
- style={{
515
- marginTop: "20px",
516
- padding: "16px",
517
- background: "#f8f9fa",
518
- borderRadius: "4px",
519
- }}
520
- >
486
+ <Card>
521
487
  <Text fontSize="$5" fontWeight="600">
522
488
  Accessibility Features:
523
489
  </Text>
@@ -529,8 +495,8 @@ export const Accessibility: Story = {
529
495
  <Text>• Focus management between controls</Text>
530
496
  <Text>• Proper semantic markup for pagination info</Text>
531
497
  </YStack>
532
- </View>
533
- </View>
498
+ </Card>
499
+ </YStack>
534
500
  ),
535
501
  };
536
502
 
@@ -8,8 +8,8 @@ import {
8
8
  import { getGroupPosition, stackRadiusProps, useResolvedKnobs } from "@multiplatform.one/theme";
9
9
  import React, { useState } from "react";
10
10
  import { Button, FloatingPanel, Input, Slider, Switch } from "@multiplatform.one/forms";
11
- import { DropdownMenu } from "@multiplatform.one/components";
12
- import { ScrollView, Separator, Text, View, XStack, YStack, useTheme } from "tamagui";
11
+ import { DropdownMenu, ScrollView } from "@multiplatform.one/components";
12
+ import { Separator, Text, View, XStack, YStack, useTheme } from "tamagui";
13
13
  import type { TableView } from "../../types";
14
14
  import { t } from "../../shared/t";
15
15
  import { iconSizeForToken } from "../../utils/chromeIcons";
@@ -6,6 +6,7 @@
6
6
  * borderRadius, elevation, and sizing across the table package.
7
7
  */
8
8
 
9
+ import { DialogShowFocus } from "@multiplatform.one/components";
9
10
  import { useResolvedKnobs } from "@multiplatform.one/theme";
10
11
  import type React from "react";
11
12
  import { Dialog } from "tamagui";
@@ -62,6 +63,7 @@ export function KnobDialogContent({ children, minWidth }: KnobDialogContentProps
62
63
  {...(minWidth ? { minWidth } : {})}
63
64
  >
64
65
  {children}
66
+ <DialogShowFocus />
65
67
  </Dialog.Content>
66
68
  </Dialog.Portal>
67
69
  );