@multiplatform.one/components 6.4.3 → 6.6.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.
Files changed (262) hide show
  1. package/dist/cjs/Avatar/index.cjs +4 -0
  2. package/dist/cjs/Avatar/index.native.js +4 -0
  3. package/dist/cjs/Avatar/index.native.js.map +1 -1
  4. package/dist/cjs/Badge/index.cjs +3 -0
  5. package/dist/cjs/Badge/index.native.js +3 -0
  6. package/dist/cjs/Badge/index.native.js.map +1 -1
  7. package/dist/cjs/Carousel/index.cjs +28 -15
  8. package/dist/cjs/Carousel/index.native.js +34 -17
  9. package/dist/cjs/Carousel/index.native.js.map +1 -1
  10. package/dist/cjs/DotIndicator/index.cjs +92 -35
  11. package/dist/cjs/DotIndicator/index.native.js +93 -35
  12. package/dist/cjs/DotIndicator/index.native.js.map +1 -1
  13. package/dist/cjs/Pagination.cjs +112 -167
  14. package/dist/cjs/Pagination.native.js +125 -180
  15. package/dist/cjs/Pagination.native.js.map +1 -1
  16. package/dist/cjs/ProgressSteps/index.cjs +2 -1
  17. package/dist/cjs/ProgressSteps/index.native.js +2 -1
  18. package/dist/cjs/ProgressSteps/index.native.js.map +1 -1
  19. package/dist/cjs/charts/PieChart.cjs +1 -1
  20. package/dist/cjs/charts/PieChart.native.js +7 -2
  21. package/dist/cjs/charts/PieChart.native.js.map +1 -1
  22. package/dist/cjs/feedback/ConfirmDialog.cjs +1 -3
  23. package/dist/cjs/feedback/ConfirmDialog.native.js +1 -3
  24. package/dist/cjs/feedback/ConfirmDialog.native.js.map +1 -1
  25. package/dist/cjs/index.cjs +1 -0
  26. package/dist/cjs/index.native.js +1 -0
  27. package/dist/cjs/index.native.js.map +1 -1
  28. package/dist/cjs/layouts/ViewSwitcher.cjs +4 -5
  29. package/dist/cjs/layouts/ViewSwitcher.native.js +13 -7
  30. package/dist/cjs/layouts/ViewSwitcher.native.js.map +1 -1
  31. package/dist/cjs/layouts/attachments.cjs +4 -8
  32. package/dist/cjs/layouts/attachments.native.js +5 -8
  33. package/dist/cjs/layouts/attachments.native.js.map +1 -1
  34. package/dist/cjs/layouts/page.cjs +1 -0
  35. package/dist/cjs/layouts/page.native.js +4 -1
  36. package/dist/cjs/layouts/page.native.js.map +1 -1
  37. package/dist/cjs/layouts/tags.cjs +1 -0
  38. package/dist/cjs/layouts/tags.native.js +2 -0
  39. package/dist/cjs/layouts/tags.native.js.map +1 -1
  40. package/dist/cjs/layouts/timeline.cjs +0 -6
  41. package/dist/cjs/layouts/timeline.native.js +0 -6
  42. package/dist/cjs/layouts/timeline.native.js.map +1 -1
  43. package/dist/cjs/surfaces.cjs +21 -0
  44. package/dist/cjs/surfaces.native.js +22 -0
  45. package/dist/cjs/surfaces.native.js.map +1 -1
  46. package/dist/cjs/views/Calendar.cjs +2 -0
  47. package/dist/cjs/views/Calendar.native.js +8 -2
  48. package/dist/cjs/views/Calendar.native.js.map +1 -1
  49. package/dist/cjs/views/Kanban.cjs +22 -13
  50. package/dist/cjs/views/Kanban.native.js +1 -0
  51. package/dist/cjs/views/Kanban.native.js.map +1 -1
  52. package/dist/cjs/views/ReportBuilder.cjs +12 -2
  53. package/dist/cjs/views/ReportBuilder.native.js +12 -2
  54. package/dist/cjs/views/ReportBuilder.native.js.map +1 -1
  55. package/dist/esm/Avatar/index.mjs +5 -1
  56. package/dist/esm/Avatar/index.mjs.map +1 -1
  57. package/dist/esm/Avatar/index.native.js +5 -1
  58. package/dist/esm/Avatar/index.native.js.map +1 -1
  59. package/dist/esm/Badge/index.mjs +4 -1
  60. package/dist/esm/Badge/index.mjs.map +1 -1
  61. package/dist/esm/Badge/index.native.js +4 -1
  62. package/dist/esm/Badge/index.native.js.map +1 -1
  63. package/dist/esm/Carousel/index.mjs +28 -15
  64. package/dist/esm/Carousel/index.mjs.map +1 -1
  65. package/dist/esm/Carousel/index.native.js +34 -17
  66. package/dist/esm/Carousel/index.native.js.map +1 -1
  67. package/dist/esm/DotIndicator/index.mjs +93 -36
  68. package/dist/esm/DotIndicator/index.mjs.map +1 -1
  69. package/dist/esm/DotIndicator/index.native.js +94 -36
  70. package/dist/esm/DotIndicator/index.native.js.map +1 -1
  71. package/dist/esm/Pagination.mjs +113 -168
  72. package/dist/esm/Pagination.mjs.map +1 -1
  73. package/dist/esm/Pagination.native.js +126 -181
  74. package/dist/esm/Pagination.native.js.map +1 -1
  75. package/dist/esm/ProgressSteps/index.mjs +2 -1
  76. package/dist/esm/ProgressSteps/index.mjs.map +1 -1
  77. package/dist/esm/ProgressSteps/index.native.js +2 -1
  78. package/dist/esm/ProgressSteps/index.native.js.map +1 -1
  79. package/dist/esm/charts/PieChart.mjs +1 -1
  80. package/dist/esm/charts/PieChart.mjs.map +1 -1
  81. package/dist/esm/charts/PieChart.native.js +7 -2
  82. package/dist/esm/charts/PieChart.native.js.map +1 -1
  83. package/dist/esm/feedback/ConfirmDialog.mjs +2 -4
  84. package/dist/esm/feedback/ConfirmDialog.mjs.map +1 -1
  85. package/dist/esm/feedback/ConfirmDialog.native.js +2 -4
  86. package/dist/esm/feedback/ConfirmDialog.native.js.map +1 -1
  87. package/dist/esm/index.mjs +2 -2
  88. package/dist/esm/index.mjs.map +1 -1
  89. package/dist/esm/index.native.js +2 -2
  90. package/dist/esm/index.native.js.map +1 -1
  91. package/dist/esm/layouts/ViewSwitcher.mjs +4 -5
  92. package/dist/esm/layouts/ViewSwitcher.mjs.map +1 -1
  93. package/dist/esm/layouts/ViewSwitcher.native.js +13 -7
  94. package/dist/esm/layouts/ViewSwitcher.native.js.map +1 -1
  95. package/dist/esm/layouts/attachments.mjs +4 -8
  96. package/dist/esm/layouts/attachments.mjs.map +1 -1
  97. package/dist/esm/layouts/attachments.native.js +5 -8
  98. package/dist/esm/layouts/attachments.native.js.map +1 -1
  99. package/dist/esm/layouts/page.mjs +2 -1
  100. package/dist/esm/layouts/page.mjs.map +1 -1
  101. package/dist/esm/layouts/page.native.js +5 -2
  102. package/dist/esm/layouts/page.native.js.map +1 -1
  103. package/dist/esm/layouts/tags.mjs +1 -0
  104. package/dist/esm/layouts/tags.mjs.map +1 -1
  105. package/dist/esm/layouts/tags.native.js +2 -0
  106. package/dist/esm/layouts/tags.native.js.map +1 -1
  107. package/dist/esm/layouts/timeline.mjs +0 -6
  108. package/dist/esm/layouts/timeline.mjs.map +1 -1
  109. package/dist/esm/layouts/timeline.native.js +0 -6
  110. package/dist/esm/layouts/timeline.native.js.map +1 -1
  111. package/dist/esm/surfaces.mjs +21 -1
  112. package/dist/esm/surfaces.mjs.map +1 -1
  113. package/dist/esm/surfaces.native.js +22 -1
  114. package/dist/esm/surfaces.native.js.map +1 -1
  115. package/dist/esm/views/Calendar.mjs +3 -1
  116. package/dist/esm/views/Calendar.mjs.map +1 -1
  117. package/dist/esm/views/Calendar.native.js +9 -3
  118. package/dist/esm/views/Calendar.native.js.map +1 -1
  119. package/dist/esm/views/Kanban.mjs +23 -14
  120. package/dist/esm/views/Kanban.mjs.map +1 -1
  121. package/dist/esm/views/Kanban.native.js +2 -1
  122. package/dist/esm/views/Kanban.native.js.map +1 -1
  123. package/dist/esm/views/ReportBuilder.mjs +13 -3
  124. package/dist/esm/views/ReportBuilder.mjs.map +1 -1
  125. package/dist/esm/views/ReportBuilder.native.js +13 -3
  126. package/dist/esm/views/ReportBuilder.native.js.map +1 -1
  127. package/dist/jsx/Avatar/index.mjs +5 -1
  128. package/dist/jsx/Avatar/index.mjs.map +1 -1
  129. package/dist/jsx/Avatar/index.native.js +4 -0
  130. package/dist/jsx/Avatar/index.native.js.map +1 -1
  131. package/dist/jsx/Badge/index.mjs +4 -1
  132. package/dist/jsx/Badge/index.mjs.map +1 -1
  133. package/dist/jsx/Badge/index.native.js +3 -0
  134. package/dist/jsx/Badge/index.native.js.map +1 -1
  135. package/dist/jsx/Carousel/index.mjs +28 -15
  136. package/dist/jsx/Carousel/index.mjs.map +1 -1
  137. package/dist/jsx/Carousel/index.native.js +34 -17
  138. package/dist/jsx/Carousel/index.native.js.map +1 -1
  139. package/dist/jsx/DotIndicator/index.mjs +93 -36
  140. package/dist/jsx/DotIndicator/index.mjs.map +1 -1
  141. package/dist/jsx/DotIndicator/index.native.js +93 -35
  142. package/dist/jsx/DotIndicator/index.native.js.map +1 -1
  143. package/dist/jsx/Pagination.mjs +113 -168
  144. package/dist/jsx/Pagination.mjs.map +1 -1
  145. package/dist/jsx/Pagination.native.js +125 -180
  146. package/dist/jsx/Pagination.native.js.map +1 -1
  147. package/dist/jsx/ProgressSteps/index.mjs +2 -1
  148. package/dist/jsx/ProgressSteps/index.mjs.map +1 -1
  149. package/dist/jsx/ProgressSteps/index.native.js +2 -1
  150. package/dist/jsx/ProgressSteps/index.native.js.map +1 -1
  151. package/dist/jsx/charts/PieChart.mjs +1 -1
  152. package/dist/jsx/charts/PieChart.mjs.map +1 -1
  153. package/dist/jsx/charts/PieChart.native.js +7 -2
  154. package/dist/jsx/charts/PieChart.native.js.map +1 -1
  155. package/dist/jsx/feedback/ConfirmDialog.mjs +2 -4
  156. package/dist/jsx/feedback/ConfirmDialog.mjs.map +1 -1
  157. package/dist/jsx/feedback/ConfirmDialog.native.js +1 -3
  158. package/dist/jsx/feedback/ConfirmDialog.native.js.map +1 -1
  159. package/dist/jsx/index.js +2 -2
  160. package/dist/jsx/index.js.map +1 -1
  161. package/dist/jsx/index.mjs +2 -2
  162. package/dist/jsx/index.mjs.map +1 -1
  163. package/dist/jsx/index.native.js +1 -0
  164. package/dist/jsx/index.native.js.map +1 -1
  165. package/dist/jsx/layouts/ViewSwitcher.mjs +4 -5
  166. package/dist/jsx/layouts/ViewSwitcher.mjs.map +1 -1
  167. package/dist/jsx/layouts/ViewSwitcher.native.js +13 -7
  168. package/dist/jsx/layouts/ViewSwitcher.native.js.map +1 -1
  169. package/dist/jsx/layouts/attachments.mjs +4 -8
  170. package/dist/jsx/layouts/attachments.mjs.map +1 -1
  171. package/dist/jsx/layouts/attachments.native.js +5 -8
  172. package/dist/jsx/layouts/attachments.native.js.map +1 -1
  173. package/dist/jsx/layouts/page.mjs +2 -1
  174. package/dist/jsx/layouts/page.mjs.map +1 -1
  175. package/dist/jsx/layouts/page.native.js +4 -1
  176. package/dist/jsx/layouts/page.native.js.map +1 -1
  177. package/dist/jsx/layouts/tags.mjs +1 -0
  178. package/dist/jsx/layouts/tags.mjs.map +1 -1
  179. package/dist/jsx/layouts/tags.native.js +2 -0
  180. package/dist/jsx/layouts/tags.native.js.map +1 -1
  181. package/dist/jsx/layouts/timeline.mjs +0 -6
  182. package/dist/jsx/layouts/timeline.mjs.map +1 -1
  183. package/dist/jsx/layouts/timeline.native.js +0 -6
  184. package/dist/jsx/layouts/timeline.native.js.map +1 -1
  185. package/dist/jsx/surfaces.mjs +21 -1
  186. package/dist/jsx/surfaces.mjs.map +1 -1
  187. package/dist/jsx/surfaces.native.js +22 -0
  188. package/dist/jsx/surfaces.native.js.map +1 -1
  189. package/dist/jsx/views/Calendar.mjs +3 -1
  190. package/dist/jsx/views/Calendar.mjs.map +1 -1
  191. package/dist/jsx/views/Calendar.native.js +8 -2
  192. package/dist/jsx/views/Calendar.native.js.map +1 -1
  193. package/dist/jsx/views/Kanban.mjs +23 -14
  194. package/dist/jsx/views/Kanban.mjs.map +1 -1
  195. package/dist/jsx/views/Kanban.native.js +1 -0
  196. package/dist/jsx/views/Kanban.native.js.map +1 -1
  197. package/dist/jsx/views/ReportBuilder.mjs +13 -3
  198. package/dist/jsx/views/ReportBuilder.mjs.map +1 -1
  199. package/dist/jsx/views/ReportBuilder.native.js +12 -2
  200. package/dist/jsx/views/ReportBuilder.native.js.map +1 -1
  201. package/package.json +9 -9
  202. package/src/Accordion/Accordion.stories.tsx +7 -2
  203. package/src/Avatar/Avatar.spec.tsx +31 -0
  204. package/src/Avatar/index.tsx +19 -1
  205. package/src/Badge/Badge.stories.tsx +3 -1
  206. package/src/Badge/index.tsx +4 -1
  207. package/src/Carousel/Carousel.spec.tsx +271 -0
  208. package/src/Carousel/Carousel.stories.tsx +35 -0
  209. package/src/Carousel/index.tsx +45 -20
  210. package/src/DialogContent.stories.tsx +2 -2
  211. package/src/DotIndicator/dotIndicator.spec.tsx +43 -1
  212. package/src/DotIndicator/index.tsx +169 -35
  213. package/src/Pagination.api.spec.tsx +345 -0
  214. package/src/Pagination.tsx +138 -176
  215. package/src/Panel.stories.tsx +4 -4
  216. package/src/ProgressSteps/ProgressSteps.spec.tsx +40 -1
  217. package/src/ProgressSteps/ProgressSteps.stories.tsx +3 -1
  218. package/src/ProgressSteps/index.tsx +6 -1
  219. package/src/charts/PieChart.tsx +4 -1
  220. package/src/feedback/ConfirmDialog.tsx +2 -2
  221. package/src/index.ts +1 -0
  222. package/src/layouts/PageHeader.stories.tsx +21 -3
  223. package/src/layouts/ScreenToolbar.stories.tsx +12 -5
  224. package/src/layouts/ViewSwitcher.tsx +25 -6
  225. package/src/layouts/attachments.tsx +4 -1
  226. package/src/layouts/layouts.spec.tsx +77 -1
  227. package/src/layouts/page.tsx +5 -1
  228. package/src/layouts/pageTitleScale.spec.tsx +70 -0
  229. package/src/layouts/tags.tsx +2 -0
  230. package/src/layouts/timeline-radius.spec.tsx +21 -0
  231. package/src/layouts/timeline.tsx +7 -18
  232. package/src/shared/t.spec.ts +47 -0
  233. package/src/surfaces.tsx +33 -0
  234. package/src/views/Calendar.stories.tsx +4 -2
  235. package/src/views/Calendar.tsx +6 -1
  236. package/src/views/Gantt.stories.tsx +2 -2
  237. package/src/views/Kanban.native.tsx +8 -2
  238. package/src/views/Kanban.stories.tsx +3 -1
  239. package/src/views/Kanban.tsx +42 -13
  240. package/src/views/ReportBuilder.tsx +15 -2
  241. package/types/Avatar/index.d.ts.map +1 -1
  242. package/types/Badge/index.d.ts.map +1 -1
  243. package/types/Carousel/index.d.ts.map +1 -1
  244. package/types/DotIndicator/index.d.ts +44 -3
  245. package/types/DotIndicator/index.d.ts.map +1 -1
  246. package/types/Pagination.d.ts +6 -1
  247. package/types/Pagination.d.ts.map +1 -1
  248. package/types/ProgressSteps/index.d.ts.map +1 -1
  249. package/types/charts/PieChart.d.ts.map +1 -1
  250. package/types/index.d.ts +1 -1
  251. package/types/index.d.ts.map +1 -1
  252. package/types/layouts/ViewSwitcher.d.ts.map +1 -1
  253. package/types/layouts/attachments.d.ts.map +1 -1
  254. package/types/layouts/page.d.ts.map +1 -1
  255. package/types/layouts/tags.d.ts.map +1 -1
  256. package/types/layouts/timeline.d.ts.map +1 -1
  257. package/types/surfaces.d.ts +17 -0
  258. package/types/surfaces.d.ts.map +1 -1
  259. package/types/views/Calendar.d.ts.map +1 -1
  260. package/types/views/Kanban.d.ts +13 -1
  261. package/types/views/Kanban.d.ts.map +1 -1
  262. package/types/views/ReportBuilder.d.ts.map +1 -1
@@ -24,6 +24,7 @@ import {
24
24
  YStack,
25
25
  type YStackProps,
26
26
  } from "tamagui";
27
+ import { DialogOverlay } from "../surfaces";
27
28
  import { componentColors, sectionHeading } from "../componentColors";
28
29
  import { Skeleton } from "../Skeleton";
29
30
  import { AsyncBoundary, resolveAsyncStatus } from "./AsyncBoundary";
@@ -538,11 +539,13 @@ export function Attachments({
538
539
 
539
540
  <Dialog open={!!previewFile} onOpenChange={(open) => !open && setPreviewFile(null)}>
540
541
  <Dialog.Portal>
541
- <Dialog.Overlay opacity={0.5} enterStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }} />
542
+ <DialogOverlay opacity={0.5} />
542
543
  <Dialog.Content
543
544
  borderWidth={1}
544
545
  borderColor="$borderColor"
545
546
  elevation={knobProps.elevation}
547
+ // LC-24: enter/exit styles are dead code without a resolved token
548
+ transition={knobProps.transition}
546
549
  enterStyle={{ x: 0, y: -20, opacity: 0, scale: 0.9 }}
547
550
  exitStyle={{ x: 0, y: 10, opacity: 0, scale: 0.95 }}
548
551
  maxWidth={800}
@@ -1,6 +1,15 @@
1
- import { cleanup, fireEvent, screen } from "@testing-library/react";
1
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2
2
  import { renderWithProviders } from "@multiplatform.one/test-utils";
3
+ import {
4
+ createThemesBuilder,
5
+ defaultAccentTheme,
6
+ defaultBaseTheme,
7
+ defaultBuilderOptions,
8
+ } from "@multiplatform.one/theme";
9
+ import { configWithoutAnimations } from "@tamagui/config";
10
+ import { animationsCSS } from "@tamagui/config/v5-css";
3
11
  import * as React from "react";
12
+ import { TamaguiProvider, createTamagui } from "tamagui";
4
13
  import { afterEach, describe, expect, it, vi } from "vitest";
5
14
  import * as Debug from "./Debug";
6
15
  import * as Layouts from "./index";
@@ -10,6 +19,21 @@ import { ViewSwitcher, type ViewOption } from "./ViewSwitcher";
10
19
  import { Breadcrumbs, type BreadcrumbItem } from "./breadcrumbs";
11
20
  import { Tags, type TagItem } from "./tags";
12
21
 
22
+ // House builder themes for the LC-05 mark assertion — created at MODULE
23
+ // scope: tamagui registers a config's theme variables globally at
24
+ // createTamagui time, and a config created after the first render (inside a
25
+ // test body) never resolves its tokens (forms Progress.spec pattern).
26
+ const houseThemes = createThemesBuilder(
27
+ defaultBaseTheme,
28
+ defaultAccentTheme,
29
+ defaultBuilderOptions,
30
+ ).themes() as Record<string, Record<string, string>>;
31
+ const houseConfig = createTamagui({
32
+ ...configWithoutAnimations,
33
+ animations: animationsCSS,
34
+ themes: houseThemes as any,
35
+ });
36
+
13
37
  afterEach(cleanup);
14
38
 
15
39
  describe("Layouts Index", () => {
@@ -207,6 +231,33 @@ describe("ViewSwitcher", () => {
207
231
  expect(screen.getByText("Table")).toBeInTheDocument();
208
232
  expect(screen.getByText("Kanban")).toBeInTheDocument();
209
233
  });
234
+
235
+ // LC-05 ONE-EMPHASIS / D-01 rider (b): the selected chip is an
236
+ // accent-TINTED step (the accent ramp's surface tier — accent11 in a light
237
+ // scheme), never the neutral $color3/$color4 it used to take. Stock test
238
+ // themes carry no accent ramp, so mount the house builder themes.
239
+ it("fills the selected chip with the accent-tinted surface, not a neutral step", () => {
240
+ const tint = houseThemes.light.accent11;
241
+ expect(tint).toBeTruthy();
242
+ // Digit prefix Tamagui embeds in the atomic class for a concrete color.
243
+ const tintDigits = (tint.match(/\d+/g) ?? []).slice(0, 3).join("");
244
+
245
+ const views: ViewOption[] = [
246
+ { type: "list", label: "List" },
247
+ { type: "table", label: "Table" },
248
+ ];
249
+ const { container } = render(
250
+ <TamaguiProvider config={houseConfig} defaultTheme="light" disableInjectCSS>
251
+ <ViewSwitcher views={views} currentView="list" onViewChange={() => {}} />
252
+ </TamaguiProvider>,
253
+ );
254
+ const selected = container.querySelector('[data-state="on"]') as HTMLElement;
255
+ const idle = container.querySelector('[data-state="off"]') as HTMLElement;
256
+ expect(selected).toBeTruthy();
257
+ expect(idle).toBeTruthy();
258
+ expect(selected.className).toContain(`_bg-hsla${tintDigits}`);
259
+ expect(idle.className).not.toContain(`_bg-hsla${tintDigits}`);
260
+ });
210
261
  });
211
262
 
212
263
  // ---------------------------------------------------------------------------
@@ -406,4 +457,29 @@ describe("Timeline", () => {
406
457
  expect(screen.queryByText(/1 entry/)).toBeNull();
407
458
  expect(screen.queryByText("Stale entry")).toBeNull();
408
459
  });
460
+
461
+ it("bidi-isolates the entry-count badge (LC-58) so RTL contexts keep the run in order", () => {
462
+ const { Timeline } = Layouts;
463
+ const entries = [
464
+ {
465
+ id: "entry-1",
466
+ type: "comment" as const,
467
+ content: "First",
468
+ author: "Alice",
469
+ timestamp: "2026-01-15T10:00:00Z",
470
+ },
471
+ {
472
+ id: "entry-2",
473
+ type: "comment" as const,
474
+ content: "Second",
475
+ author: "Bob",
476
+ timestamp: "2026-01-15T11:00:00Z",
477
+ },
478
+ ];
479
+ renderWithProviders(<Timeline entries={entries} label="Activity" />);
480
+
481
+ // The whole formatted run rides FSI…PDI — under RTL "2 entries" must not
482
+ // reorder to "entries 2" (the measured pre-fix failure).
483
+ expect(screen.getByText("\u20682 entries\u2069")).toBeInTheDocument();
484
+ });
409
485
  });
@@ -18,6 +18,7 @@
18
18
  import {
19
19
  hairline,
20
20
  layoutBreakpoints,
21
+ radiusClassProps,
21
22
  readingWidthStyle,
22
23
  resolvePageTitleScale,
23
24
  useResolvedKnobs,
@@ -413,7 +414,10 @@ export function EmptyState({
413
414
  justifyContent="center"
414
415
  backgroundColor="$color3"
415
416
  // R-PILL icon well: always a circle; its inset scales with the
416
- // size knob (spec: empty icon well consumes sizeToken).
417
+ // size knob (spec: empty icon well consumes sizeToken). Declared so
418
+ // the `radius:none` sweep licenses the circle instead of guessing
419
+ // from its geometry (D-09 / LC-60).
420
+ {...radiusClassProps("R-PILL", "empty icon well")}
417
421
  padding={knobProps.sizeToken as any}
418
422
  borderRadius={1000}
419
423
  >
@@ -0,0 +1,70 @@
1
+ // D-11 hero-H1 dial: product screens take the moderate page-title step and the
2
+ // 64px display scale is an explicit opt-in. jsdom cannot cascade Tamagui's
3
+ // class-based CSS, so this asserts on the atomic font-size classes the size
4
+ // token emits (same technique as timeline-radius.spec.tsx).
5
+ import { cleanup, screen } from "@testing-library/react";
6
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
7
+ import { Preset } from "@multiplatform.one/theme";
8
+ import { afterEach, describe, expect, it } from "vitest";
9
+ import { PageHeader } from "./page";
10
+
11
+ afterEach(cleanup);
12
+
13
+ function titleFontClasses(): string[] {
14
+ const title = screen.getByText("Workspace Settings");
15
+ return String(title.className || "")
16
+ .split(" ")
17
+ .filter((c) => c.startsWith("_fos-") || c.startsWith("_lh-"));
18
+ }
19
+
20
+ function renderTitle(node: React.ReactElement): string[] {
21
+ renderWithProviders(node);
22
+ const classes = titleFontClasses();
23
+ cleanup();
24
+ return classes;
25
+ }
26
+
27
+ describe("PageHeader page-title scale (D-11)", () => {
28
+ it("defaults to the moderate step, not the display H1", () => {
29
+ const byDefault = renderTitle(<PageHeader title="Workspace Settings" />);
30
+ const moderate = renderTitle(<PageHeader title="Workspace Settings" titleScale="moderate" />);
31
+ const display = renderTitle(<PageHeader title="Workspace Settings" titleScale="display" />);
32
+
33
+ expect(byDefault).not.toHaveLength(0);
34
+ expect(byDefault).toEqual(moderate);
35
+ expect(byDefault).not.toEqual(display);
36
+ });
37
+
38
+ it("the pageTitleScale knob re-scales the title without touching the screen", () => {
39
+ const knobDisplay = renderTitle(
40
+ <Preset overrides={{ pageTitleScale: "display" }}>
41
+ <PageHeader title="Workspace Settings" />
42
+ </Preset>,
43
+ );
44
+ const propDisplay = renderTitle(<PageHeader title="Workspace Settings" titleScale="display" />);
45
+ const knobModerate = renderTitle(
46
+ <Preset overrides={{ pageTitleScale: "moderate" }}>
47
+ <PageHeader title="Workspace Settings" />
48
+ </Preset>,
49
+ );
50
+
51
+ // Both opt-in channels resolve through the one table, so they agree.
52
+ expect(knobDisplay).toEqual(propDisplay);
53
+ expect(knobDisplay).not.toEqual(knobModerate);
54
+ });
55
+
56
+ it("the per-instance eject still wins over the surface knob", () => {
57
+ const ejected = renderTitle(
58
+ <Preset overrides={{ pageTitleScale: "display" }}>
59
+ <PageHeader title="Workspace Settings" titleScale="moderate" />
60
+ </Preset>,
61
+ );
62
+ const moderate = renderTitle(<PageHeader title="Workspace Settings" titleScale="moderate" />);
63
+ expect(ejected).toEqual(moderate);
64
+ });
65
+
66
+ it("the heading level is semantics, not scale — h1 either way", () => {
67
+ renderWithProviders(<PageHeader title="Workspace Settings" titleScale="display" />);
68
+ expect(screen.getByText("Workspace Settings").tagName).toBe("H1");
69
+ });
70
+ });
@@ -445,6 +445,8 @@ export function Tags({
445
445
  borderColor="$borderColor"
446
446
  borderRadius={knobProps.borderRadius.borderRadius}
447
447
  elevation={knobProps.elevation}
448
+ // LC-24: enter/exit styles are dead code without a resolved token
449
+ transition={knobProps.transition}
448
450
  enterStyle={{ y: -10, opacity: 0 }}
449
451
  exitStyle={{ y: -10, opacity: 0 }}
450
452
  >
@@ -34,6 +34,27 @@ function entryRowRadiusClass(): string | undefined {
34
34
  return undefined;
35
35
  }
36
36
 
37
+ describe("Timeline avatar identity shape (LC-60 / D-09)", () => {
38
+ // Entry and composer avatars both keep their identity circle at
39
+ // `borderRadius: none`, so each one must carry the R-IDENTITY declaration the
40
+ // spec-generated allowlist reads. An undeclared circle is a KNOB-TOTALITY defect.
41
+ it("declares every knob-immune avatar circle at borderRadius:none", () => {
42
+ const { container } = renderWithProviders(
43
+ <Preset overrides={{ borderRadius: "none" }}>
44
+ <Timeline entries={entries} onCommentSubmit={() => {}} />
45
+ </Preset>,
46
+ );
47
+ // the composer avatar proves the composer branch rendered
48
+ expect(screen.getByPlaceholderText("Add a comment...")).toBeInTheDocument();
49
+
50
+ const declared = container.querySelectorAll('[data-radius-class="R-IDENTITY"]');
51
+ expect(declared.length).toBeGreaterThanOrEqual(2);
52
+ for (const el of declared) {
53
+ expect(el.getAttribute("data-radius-part")).toBe("Avatar");
54
+ }
55
+ });
56
+ });
57
+
37
58
  describe("Timeline entry radius knob (SB-M-301)", () => {
38
59
  it("entry row border radius follows the borderRadius knob", () => {
39
60
  renderWithProviders(
@@ -523,14 +523,13 @@ export function Timeline({
523
523
  </Text>
524
524
  );
525
525
 
526
- // Axiom 1 R-SCALE / Axiom 3 NULL: avatars are square at radius:none and a
527
- // circle at radius:fullthey follow the one radius channel, not a
528
- // hardcoded `circular` eject (DF-08). A token exceeding half the box still
529
- // renders a circle at the top knob.
526
+ // R-IDENTITY (D-09 / LC-60): the avatar slot inside a feed row is a
527
+ // declared identity shapea recognized object, not a styled box — so it
528
+ // keeps its circle at every radius value including `none`. This reverses
529
+ // the DF-08 `circular` eject, which read Avatar as R-SCALE back when the
530
+ // spec table still classed it that way; the table was the wrong half.
530
531
  const renderAvatar = (entry: TimelineEntry, size: string) => (
531
532
  <Avatar
532
- circular={false}
533
- borderRadius={radiusToken}
534
533
  size={size as any}
535
534
  name={getEntryDisplayName(entry)}
536
535
  fallbackBackgroundColor="$accentBackground"
@@ -669,12 +668,7 @@ export function Timeline({
669
668
  },
670
669
  ariaLabel: addCommentLabel,
671
670
  avatar: (
672
- <Avatar
673
- circular={false}
674
- borderRadius={radiusToken}
675
- size={avatarSize}
676
- backgroundColor={componentColors.interactive.hover}
677
- >
671
+ <Avatar size={avatarSize} backgroundColor={componentColors.interactive.hover}>
678
672
  <Avatar.Fallback
679
673
  backgroundColor={componentColors.interactive.hover}
680
674
  alignItems="center"
@@ -701,12 +695,7 @@ export function Timeline({
701
695
  expanded: true,
702
696
  ariaLabel: `${replyLabel} ${getEntryDisplayName(parentEntry)}`,
703
697
  avatar: (
704
- <Avatar
705
- circular={false}
706
- borderRadius={radiusToken}
707
- size={replyAvatarSize}
708
- backgroundColor={componentColors.interactive.hover}
709
- >
698
+ <Avatar size={replyAvatarSize} backgroundColor={componentColors.interactive.hover}>
710
699
  <Avatar.Fallback
711
700
  backgroundColor={componentColors.interactive.hover}
712
701
  alignItems="center"
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { bidiIsolate, withInterp } from "./t";
3
+
4
+ describe("withInterp", () => {
5
+ it("no-ops when the translation already interpolated", () => {
6
+ expect(withInterp("7 entries", { count: 7 })).toBe("7 entries");
7
+ });
8
+
9
+ it("interpolates the raw key when i18n is uninitialized", () => {
10
+ expect(withInterp("{{count}} entries", { count: 7 })).toBe("7 entries");
11
+ });
12
+
13
+ it("leaves unknown variables as-is", () => {
14
+ expect(withInterp("{{count}} entries", { other: 1 })).toBe("{{count}} entries");
15
+ });
16
+
17
+ it("handles multiple variables", () => {
18
+ expect(withInterp("{{a}} of {{b}}", { a: 1, b: 2 })).toBe("1 of 2");
19
+ });
20
+ });
21
+
22
+ describe("bidiIsolate (LC-58 BIDI-ISOLATION)", () => {
23
+ it("wraps the run in FSI…PDI so RTL contexts cannot reorder it", () => {
24
+ expect(bidiIsolate("1–5 of 5")).toBe("\u20681–5 of 5\u2069");
25
+ expect(bidiIsolate("7 entries")).toBe("\u20687 entries\u2069");
26
+ });
27
+
28
+ it("keeps LTR content byte-identical inside the isolates", () => {
29
+ // First-strong isolation adds only the invisible FSI/PDI frame — the
30
+ // visible run is unchanged, so LTR rendering is unaffected.
31
+ const isolated = bidiIsolate("1–25 of 312");
32
+ expect(isolated.slice(1, -1)).toBe("1–25 of 312");
33
+ expect(isolated.startsWith("\u2068")).toBe(true);
34
+ expect(isolated.endsWith("\u2069")).toBe(true);
35
+ });
36
+
37
+ it("preserves an RTL run's own base direction (first-strong semantics)", () => {
38
+ // An Arabic run keeps its first strong character (RTL) as the isolate's
39
+ // base direction — isolation never forces LTR on translated copy.
40
+ const rtl = "٧ إدخالات";
41
+ expect(bidiIsolate(rtl)).toBe(`\u2068${rtl}\u2069`);
42
+ });
43
+
44
+ it("leaves empty strings alone (nothing to isolate)", () => {
45
+ expect(bidiIsolate("")).toBe("");
46
+ });
47
+ });
package/src/surfaces.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  View,
10
10
  type CardProps as TamaguiCardProps,
11
11
  type DialogContentProps as TamaguiDialogContentProps,
12
+ type DialogOverlayProps as TamaguiDialogOverlayProps,
12
13
  type TamaguiElement,
13
14
  type ViewProps,
14
15
  withStaticProperties,
@@ -150,6 +151,38 @@ export const DialogContent = forwardRef<
150
151
  );
151
152
  });
152
153
 
154
+ // ── DialogOverlay ────────────────────────────────────────────────────────
155
+
156
+ // A scrim only fades — sliding or scaling a full-bleed backdrop reads as a
157
+ // glitch, so this stays opacity-only rather than reusing `overlayEnterExit`.
158
+ const scrimFade = { opacity: 0 } as const;
159
+
160
+ /**
161
+ * Knob-aware dialog scrim. Bare `Dialog.Overlay` carries no transition, so
162
+ * the backdrop popped in and out while the content faded (LC-24: enter/exit
163
+ * styles without a resolved token are dead code on the css driver). Pairs
164
+ * with `DialogContent`; `transition` drops to undefined at `animation=none`
165
+ * and under `prefers-reduced-motion`, which is the instant path.
166
+ */
167
+ export const DialogOverlay = forwardRef<
168
+ TamaguiElement,
169
+ TamaguiDialogOverlayProps & { children?: ReactNode }
170
+ >(function DialogOverlay({ children, ...props }, ref) {
171
+ const { knobProps } = useResolvedKnobs();
172
+ return (
173
+ <Dialog.Overlay
174
+ ref={ref}
175
+ backgroundColor="$shadow6"
176
+ transition={knobProps.transition}
177
+ enterStyle={scrimFade}
178
+ exitStyle={scrimFade}
179
+ {...props}
180
+ >
181
+ {children}
182
+ </Dialog.Overlay>
183
+ );
184
+ });
185
+
153
186
  // ── PopoverContent ───────────────────────────────────────────────────────
154
187
 
155
188
  export const PopoverContent = forwardRef<TamaguiElement, ViewProps & { children?: ReactNode }>(
@@ -1,7 +1,7 @@
1
1
  import type { Meta } from "@storybook/react-native-web-vite";
2
2
  import { action } from "@multiplatform.one/storybook";
3
3
  import { useState } from "react";
4
- import { resolveChartPalette } from "@multiplatform.one/theme";
4
+ import { formatAbsoluteDateTime, resolveChartPalette } from "@multiplatform.one/theme";
5
5
  import { Text, YStack } from "tamagui";
6
6
  import {
7
7
  Calendar,
@@ -75,7 +75,9 @@ function EventDragDemo() {
75
75
  onEventMove={(event: CalendarEvent, range: CalendarEventMoveRange) => {
76
76
  action("onEventMove")(event.id, range.start.toISOString(), range.end.toISOString());
77
77
  setLastMove(
78
- `Moved "${event.title}" to ${range.start.toLocaleString()} - ${range.end.toLocaleString()}`,
78
+ `Moved "${event.title}" to ${formatAbsoluteDateTime(range.start)} - ${formatAbsoluteDateTime(
79
+ range.end,
80
+ )}`,
79
81
  );
80
82
  setDragEvents((prev) =>
81
83
  prev.map((e) => (e.id === event.id ? { ...e, start: range.start, end: range.end } : e)),
@@ -25,6 +25,7 @@ import {
25
25
  hairline,
26
26
  hairlineWidth,
27
27
  pressTargetHitSlop,
28
+ radiusClassProps,
28
29
  useReadableTextOn,
29
30
  useResolvedKnobs,
30
31
  } from "@multiplatform.one/theme";
@@ -543,6 +544,7 @@ function NowIndicator({ hourHeight }: { hourHeight: number }) {
543
544
  {...{ "data-calendar-now": "true" }}
544
545
  >
545
546
  <YStack
547
+ {...radiusClassProps("R-PILL", "Calendar now-marker dot")}
546
548
  position="absolute"
547
549
  {...(isRTL ? { right: -3 } : { left: -3 })}
548
550
  top={-3}
@@ -1743,11 +1745,14 @@ export function Calendar({
1743
1745
  hoverStyle={{ backgroundColor: componentColors.surface.background }}
1744
1746
  >
1745
1747
  <XStack justifyContent="flex-end" paddingInlineEnd="$1">
1746
- {/* Today marker: accent disc (pill-by-anatomy, like Badge). */}
1748
+ {/* Today marker: accent disc (pill-by-anatomy, like Badge)
1749
+ declared R-PILL so the `radius:none` sweep licenses the
1750
+ disc instead of inferring it from geometry (D-09 / LC-60). */}
1747
1751
  <YStack
1748
1752
  width={DAY_DISC_PX}
1749
1753
  height={DAY_DISC_PX}
1750
1754
  borderRadius={DAY_DISC_PX / 2}
1755
+ {...radiusClassProps("R-PILL", "Calendar today marker")}
1751
1756
  alignItems="center"
1752
1757
  justifyContent="center"
1753
1758
  backgroundColor={today ? "$accentBackground" : "transparent"}
@@ -1,7 +1,7 @@
1
1
  import type { Meta } from "@storybook/react-native-web-vite";
2
2
  import { action } from "@multiplatform.one/storybook";
3
3
  import { useEffect, useState } from "react";
4
- import { resolveChartPalette } from "@multiplatform.one/theme";
4
+ import { formatAbsoluteDate, resolveChartPalette } from "@multiplatform.one/theme";
5
5
  import { Text, YStack } from "tamagui";
6
6
  import { GanttView, type GanttItemMoveRange, type GanttTask } from "./Gantt";
7
7
  import { addDays, startOfDay } from "./ganttMath";
@@ -89,7 +89,7 @@ function BarDragDemo() {
89
89
  onItemMove={(task: GanttTask, range: GanttItemMoveRange) => {
90
90
  action("onItemMove")(task.id, range.start.toISOString(), range.end.toISOString());
91
91
  setLastMove(
92
- `Moved "${task.title}" to ${range.start.toLocaleDateString()} – ${range.end.toLocaleDateString()}`,
92
+ `Moved "${task.title}" to ${formatAbsoluteDate(range.start)} – ${formatAbsoluteDate(range.end)}`,
93
93
  );
94
94
  setDragTasks((prev) =>
95
95
  prev.map((t) => (t.id === task.id ? { ...t, start: range.start, end: range.end } : t)),
@@ -1,5 +1,5 @@
1
1
  import { FlashList, type FlashListRef } from "@shopify/flash-list";
2
- import { animationConfig, useResolvedKnobs } from "@multiplatform.one/theme";
2
+ import { animationConfig, radiusClassProps, useResolvedKnobs } from "@multiplatform.one/theme";
3
3
  import React, {
4
4
  useCallback,
5
5
  useEffect,
@@ -837,7 +837,13 @@ function KanbanColumnView<T>({
837
837
  title={column.title}
838
838
  icon={
839
839
  column.color ? (
840
- <YStack width={8} height={8} borderRadius={4} backgroundColor={column.color as any} />
840
+ <YStack
841
+ {...radiusClassProps("R-PILL", "Kanban column dot")}
842
+ width={8}
843
+ height={8}
844
+ borderRadius={4}
845
+ backgroundColor={column.color as any}
846
+ />
841
847
  ) : undefined
842
848
  }
843
849
  iconAfter={
@@ -1,7 +1,9 @@
1
1
  import { action } from "@multiplatform.one/storybook";
2
+ // Catalog Button (LC-56 story honesty): stories compose catalog controls, never raw tamagui ones.
3
+ import { Button } from "@multiplatform.one/forms";
2
4
  import type { Meta } from "@storybook/react-native-web-vite";
3
5
  import { useCallback, useState } from "react";
4
- import { Button, Text, XStack } from "tamagui";
6
+ import { Text, XStack } from "tamagui";
5
7
  import { Kanban, type KanbanColumn } from "./Kanban";
6
8
 
7
9
  interface Task {
@@ -2,7 +2,7 @@ import { componentColors } from "../componentColors";
2
2
  import { AsyncBoundary, useAsyncCount } from "../layouts/AsyncBoundary";
3
3
  import { EmptyState } from "../layouts/page";
4
4
  import { Spinner } from "@multiplatform.one/forms";
5
- import { useResolvedKnobs } from "@multiplatform.one/theme";
5
+ import { radiusClassProps, useResolvedKnobs } from "@multiplatform.one/theme";
6
6
  import { useHotkey } from "@tanstack/react-hotkeys";
7
7
  import type { RegisterableHotkey } from "@tanstack/react-hotkeys";
8
8
  import { useVirtualizer } from "@tanstack/react-virtual";
@@ -101,6 +101,18 @@ export interface KanbanProps<T> extends Omit<YStackProps, "children"> {
101
101
  onCardReorder?: (item: T, columnId: string, fromIndex: number, toIndex: number) => void;
102
102
  /** Called when a card is selected via keyboard or click */
103
103
  onCardSelect?: (item: T, columnId: string) => void;
104
+ /**
105
+ * Per-column actions (quick-add, column menu…) anchored in the column
106
+ * header after the count. Icon-only actions must carry their own
107
+ * accessible names — the board renders the slot as-is.
108
+ */
109
+ renderColumnActions?: (column: KanbanColumn<T>) => ReactNode;
110
+ /**
111
+ * Rendered after the last column inside the horizontal scroller — the
112
+ * anchored home for board-level affordances like "Add column" (never a
113
+ * detached toolbar strip).
114
+ */
115
+ boardEnd?: ReactNode;
104
116
  /** Called when user scrolls near the bottom (infinite scroll) */
105
117
  onLoadMore?: () => void;
106
118
  /** Stable key extractor for virtualized cards (defaults to the item index) */
@@ -249,6 +261,8 @@ export function Kanban<T>({
249
261
  onCardMove,
250
262
  onCardReorder,
251
263
  onCardSelect,
264
+ renderColumnActions,
265
+ boardEnd,
252
266
  onLoadMore,
253
267
  getItemKey,
254
268
  getItemLabel,
@@ -955,12 +969,14 @@ export function Kanban<T>({
955
969
  registerColumn={registerColumn}
956
970
  drag={dragContext}
957
971
  settlingKey={settle?.itemKey}
972
+ renderColumnActions={renderColumnActions}
958
973
  onLoadMore={onLoadMore}
959
974
  getItemKey={getItemKey}
960
975
  hasMore={hasMore}
961
976
  isLoading={isLoading}
962
977
  />
963
978
  ))}
979
+ {boardEnd}
964
980
  </XStack>
965
981
  </ScrollView>
966
982
 
@@ -1110,6 +1126,7 @@ interface KanbanColumnViewProps<T> {
1110
1126
  registerColumn: (id: string, handle: ColumnHandle | null) => void;
1111
1127
  drag: KanbanDragRenderContext | null;
1112
1128
  settlingKey?: string | number;
1129
+ renderColumnActions?: (column: KanbanColumn<T>) => ReactNode;
1113
1130
  onLoadMore?: () => void;
1114
1131
  getItemKey?: (item: T, index: number) => string | number;
1115
1132
  hasMore?: boolean;
@@ -1134,6 +1151,7 @@ function KanbanColumnView<T>({
1134
1151
  registerColumn,
1135
1152
  drag,
1136
1153
  settlingKey,
1154
+ renderColumnActions,
1137
1155
  onLoadMore,
1138
1156
  getItemKey,
1139
1157
  hasMore,
@@ -1327,21 +1345,32 @@ function KanbanColumnView<T>({
1327
1345
  title={column.title}
1328
1346
  icon={
1329
1347
  column.color ? (
1330
- <YStack width={8} height={8} borderRadius={4} backgroundColor={column.color as any} />
1348
+ <YStack
1349
+ {...radiusClassProps("R-PILL", "Kanban column dot")}
1350
+ width={8}
1351
+ height={8}
1352
+ borderRadius={4}
1353
+ backgroundColor={column.color as any}
1354
+ />
1331
1355
  ) : undefined
1332
1356
  }
1333
1357
  iconAfter={
1334
- itemCount !== undefined ? (
1335
- <Text
1336
- color={componentColors.text.muted}
1337
- fontSize={knobProps.sizeToken}
1338
- {...{ backgroundColor: componentColors.surface.background }}
1339
- paddingHorizontal="$2"
1340
- borderRadius={knobProps.borderRadius.borderRadius}
1341
- data-async-count
1342
- >
1343
- {itemCount}
1344
- </Text>
1358
+ itemCount !== undefined || renderColumnActions ? (
1359
+ <XStack alignItems="center" gap="$1">
1360
+ {itemCount !== undefined && (
1361
+ <Text
1362
+ color={componentColors.text.muted}
1363
+ fontSize={knobProps.sizeToken}
1364
+ {...{ backgroundColor: componentColors.surface.background }}
1365
+ paddingHorizontal="$2"
1366
+ borderRadius={knobProps.borderRadius.borderRadius}
1367
+ data-async-count
1368
+ >
1369
+ {itemCount}
1370
+ </Text>
1371
+ )}
1372
+ {renderColumnActions?.(column)}
1373
+ </XStack>
1345
1374
  ) : undefined
1346
1375
  }
1347
1376
  size={knobProps.sizeToken}
@@ -3,9 +3,13 @@ import { componentColors } from "../componentColors";
3
3
  import { AsyncBoundary, AsyncSkeleton, resolveAsyncStatus } from "../layouts/AsyncBoundary";
4
4
  import { EmptyState } from "../layouts/page";
5
5
  import {
6
+ formatAbsoluteDate,
7
+ formatAbsoluteDateTime,
6
8
  formatCurrency,
7
9
  formatNumber,
8
10
  formatPercent,
11
+ formatTimeOfDay,
12
+ pressTargetStyle,
9
13
  useResolvedKnobs,
10
14
  } from "@multiplatform.one/theme";
11
15
  // House Button (not raw tamagui): carries the DG-LAY-04 / D-02 ≥44px press
@@ -135,6 +139,14 @@ function formatCellValue(value: unknown, fieldtype: string): string {
135
139
  return formatPercent(value as number, { precision: 1 });
136
140
  case "Rating":
137
141
  return `${formatNumber(Number(value) * 5, { precision: 1 })} / 5`;
142
+ // Date-class cells take the house absolute register instead of letting
143
+ // the wire string ("2026-01-15") reach the reader.
144
+ case "Date":
145
+ return formatAbsoluteDate(value as string);
146
+ case "Datetime":
147
+ return formatAbsoluteDateTime(value as string);
148
+ case "Time":
149
+ return formatTimeOfDay(value as string);
138
150
  default:
139
151
  return String(value);
140
152
  }
@@ -494,9 +506,10 @@ export function ReportBuilder({
494
506
  {/* Chart toggle */}
495
507
  {showChart && groupedData && (
496
508
  <Button
497
- size={reportFontSize}
509
+ size={knobProps.sizeToken}
498
510
  onPress={() => setChartVisible((v) => !v)}
499
511
  theme={chartVisible ? "active" : undefined}
512
+ {...pressTargetStyle()}
500
513
  >
501
514
  Chart
502
515
  </Button>
@@ -504,7 +517,7 @@ export function ReportBuilder({
504
517
 
505
518
  {/* Export */}
506
519
  {onExport && (
507
- <Button size={reportFontSize} onPress={handleExport}>
520
+ <Button size={knobProps.sizeToken} onPress={handleExport} {...pressTargetStyle()}>
508
521
  Export
509
522
  </Button>
510
523
  )}