@multiplatform.one/components 7.7.0 → 7.7.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 (198) hide show
  1. package/dist/cjs/Accordion/index.native.js +1 -1
  2. package/dist/cjs/Accordion/index.native.js.map +1 -1
  3. package/dist/cjs/Alert/index.cjs +13 -29
  4. package/dist/cjs/Alert/index.native.js +17 -37
  5. package/dist/cjs/Alert/index.native.js.map +1 -1
  6. package/dist/cjs/AnimateHeight/index.native.js.map +1 -1
  7. package/dist/cjs/Avatar/getInitials.native.js.map +1 -1
  8. package/dist/cjs/Avatar/index.native.js.map +1 -1
  9. package/dist/cjs/Badge/index.native.js.map +1 -1
  10. package/dist/cjs/Button.native.js.map +1 -1
  11. package/dist/cjs/Card.native.js.map +1 -1
  12. package/dist/cjs/CardFooter.native.js.map +1 -1
  13. package/dist/cjs/CardHeader.native.js.map +1 -1
  14. package/dist/cjs/Carousel/index.native.js +1 -1
  15. package/dist/cjs/Carousel/index.native.js.map +1 -1
  16. package/dist/cjs/Chip/index.native.js.map +1 -1
  17. package/dist/cjs/ContextMenu/index.native.js.map +1 -1
  18. package/dist/cjs/CopyField/index.native.js.map +1 -1
  19. package/dist/cjs/DotIndicator/index.native.js.map +1 -1
  20. package/dist/cjs/DropdownMenu/index.native.js.map +1 -1
  21. package/dist/cjs/ErrorBoundary/index.native.js.map +1 -1
  22. package/dist/cjs/Heading.native.js.map +1 -1
  23. package/dist/cjs/Meter/index.native.js +1 -1
  24. package/dist/cjs/Meter/index.native.js.map +1 -1
  25. package/dist/cjs/Meter/meter.native.js.map +1 -1
  26. package/dist/cjs/Pagination.native.js +1 -1
  27. package/dist/cjs/Pagination.native.js.map +1 -1
  28. package/dist/cjs/ProgressSteps/index.native.js.map +1 -1
  29. package/dist/cjs/Quote/index.native.js.map +1 -1
  30. package/dist/cjs/Separator/index.native.js.map +1 -1
  31. package/dist/cjs/SheetModal/detents.native.js.map +1 -1
  32. package/dist/cjs/Skeleton/index.native.js.map +1 -1
  33. package/dist/cjs/SkipLink/index.native.js.map +1 -1
  34. package/dist/cjs/SortableList/SortableHandle.native.js.map +1 -1
  35. package/dist/cjs/SortableList/index.native.js.map +1 -1
  36. package/dist/cjs/SortableList/useSortableRows.native.js.map +1 -1
  37. package/dist/cjs/Tabs/index.native.js.map +1 -1
  38. package/dist/cjs/Text.native.js.map +1 -1
  39. package/dist/cjs/Toast/index.native.js.map +1 -1
  40. package/dist/cjs/Toast/store.native.js.map +1 -1
  41. package/dist/cjs/Tooltip/nativePress.native.js.map +1 -1
  42. package/dist/cjs/Tooltip/tooltipShared.native.js.map +1 -1
  43. package/dist/cjs/TreeSelect/index.native.js +1 -1
  44. package/dist/cjs/TreeSelect/index.native.js.map +1 -1
  45. package/dist/cjs/TreeSelect/treeSelectModel.native.js.map +1 -1
  46. package/dist/cjs/Video/PlayerChrome.native.js.map +1 -1
  47. package/dist/cjs/Video/VideoFrame.native.js.map +1 -1
  48. package/dist/cjs/Video/types.native.js.map +1 -1
  49. package/dist/cjs/Wheel/index.native.js.map +1 -1
  50. package/dist/cjs/Wizard/index.native.js +2 -2
  51. package/dist/cjs/Wizard/index.native.js.map +1 -1
  52. package/dist/cjs/actions/ActionBar.cjs +3 -1
  53. package/dist/cjs/actions/ActionBar.native.js +11 -2
  54. package/dist/cjs/actions/ActionBar.native.js.map +1 -1
  55. package/dist/cjs/actions/DestructiveAction.native.js.map +1 -1
  56. package/dist/cjs/actions/devWarn.native.js.map +1 -1
  57. package/dist/cjs/actions/index.native.js.map +1 -1
  58. package/dist/cjs/actions/severity.native.js.map +1 -1
  59. package/dist/cjs/charts/Axis.native.js.map +1 -1
  60. package/dist/cjs/charts/BarChart.native.js.map +1 -1
  61. package/dist/cjs/charts/ChartSurface.native.js.map +1 -1
  62. package/dist/cjs/charts/LineChart.native.js.map +1 -1
  63. package/dist/cjs/charts/PieChart.native.js.map +1 -1
  64. package/dist/cjs/charts/Sparkline.native.js.map +1 -1
  65. package/dist/cjs/charts/accessibility.native.js.map +1 -1
  66. package/dist/cjs/charts/composeChart.native.js.map +1 -1
  67. package/dist/cjs/charts/index.native.js.map +1 -1
  68. package/dist/cjs/charts/math.native.js.map +1 -1
  69. package/dist/cjs/charts/series.native.js.map +1 -1
  70. package/dist/cjs/charts/svg.native.js.map +1 -1
  71. package/dist/cjs/charts/types.native.js.map +1 -1
  72. package/dist/cjs/charts/useChartTheme.native.js.map +1 -1
  73. package/dist/cjs/code/Code/index.native.js.map +1 -1
  74. package/dist/cjs/code/CodeBlock/index.native.js +6 -6
  75. package/dist/cjs/code/CodeBlock/index.native.js.map +1 -1
  76. package/dist/cjs/code/HighlightedCode/index.native.js.map +1 -1
  77. package/dist/cjs/code/Highlighter/index.native.js.map +1 -1
  78. package/dist/cjs/code/Pre/index.native.js.map +1 -1
  79. package/dist/cjs/code/index.native.js +5 -5
  80. package/dist/cjs/code/index.native.js.map +1 -1
  81. package/dist/cjs/componentColors.native.js.map +1 -1
  82. package/dist/cjs/feedback/ConfirmDialog.native.js.map +1 -1
  83. package/dist/cjs/feedback/NotifyHost.native.js +2 -2
  84. package/dist/cjs/feedback/NotifyHost.native.js.map +1 -1
  85. package/dist/cjs/feedback/copy.native.js.map +1 -1
  86. package/dist/cjs/feedback/index.native.js.map +1 -1
  87. package/dist/cjs/feedback/notify.native.js.map +1 -1
  88. package/dist/cjs/feedback/route.native.js.map +1 -1
  89. package/dist/cjs/feedback/store.native.js.map +1 -1
  90. package/dist/cjs/hooks/index.native.js +1 -1
  91. package/dist/cjs/hooks/index.native.js.map +1 -1
  92. package/dist/cjs/hooks/useClipboard.native.js.map +1 -1
  93. package/dist/cjs/hooks/useDocumentTitle.native.js.map +1 -1
  94. package/dist/cjs/icons/CheckRegular.native.js.map +1 -1
  95. package/dist/cjs/icons/MinusRegular.native.js.map +1 -1
  96. package/dist/cjs/icons/index.native.js.map +1 -1
  97. package/dist/cjs/icons/lucide.native.js.map +1 -1
  98. package/dist/cjs/icons/themed.native.js.map +1 -1
  99. package/dist/cjs/images/Image/ImageFrame.native.js +1 -1
  100. package/dist/cjs/images/Image/ImageFrame.native.js.map +1 -1
  101. package/dist/cjs/images/Image/index.native.js +1 -1
  102. package/dist/cjs/images/Image/shared.native.js +1 -1
  103. package/dist/cjs/images/Image/shared.native.js.map +1 -1
  104. package/dist/cjs/images/Image/types.native.js.map +1 -1
  105. package/dist/cjs/images/index.native.js.map +1 -1
  106. package/dist/cjs/index.native.js +29 -29
  107. package/dist/cjs/index.native.js.map +1 -1
  108. package/dist/cjs/knobImmuneExports.native.js.map +1 -1
  109. package/dist/cjs/layouts/AsyncBoundary.native.js +2 -2
  110. package/dist/cjs/layouts/AsyncBoundary.native.js.map +1 -1
  111. package/dist/cjs/layouts/Debug/index.native.js.map +1 -1
  112. package/dist/cjs/layouts/EmptyState.native.js.map +1 -1
  113. package/dist/cjs/layouts/KeyboardAvoidingWrapper/behavior.native.js.map +1 -1
  114. package/dist/cjs/layouts/PageTemplates.native.js.map +1 -1
  115. package/dist/cjs/layouts/PaneScaffold.native.js.map +1 -1
  116. package/dist/cjs/layouts/SafeAreaWrapper/insets.native.js.map +1 -1
  117. package/dist/cjs/layouts/ViewSwitcher.native.js.map +1 -1
  118. package/dist/cjs/layouts/attachments.native.js +1 -1
  119. package/dist/cjs/layouts/attachments.native.js.map +1 -1
  120. package/dist/cjs/layouts/breadcrumbs.native.js.map +1 -1
  121. package/dist/cjs/layouts/index.native.js +3 -3
  122. package/dist/cjs/layouts/index.native.js.map +1 -1
  123. package/dist/cjs/layouts/page.native.js.map +1 -1
  124. package/dist/cjs/layouts/pageHeadingScope.native.js.map +1 -1
  125. package/dist/cjs/layouts/sidebar.native.js.map +1 -1
  126. package/dist/cjs/layouts/tags.native.js +1 -1
  127. package/dist/cjs/layouts/tags.native.js.map +1 -1
  128. package/dist/cjs/layouts/timeline.native.js +2 -2
  129. package/dist/cjs/layouts/timeline.native.js.map +1 -1
  130. package/dist/cjs/layouts/toolbar.native.js.map +1 -1
  131. package/dist/cjs/shared/actionScan.native.js.map +1 -1
  132. package/dist/cjs/shared/i18n.native.js.map +1 -1
  133. package/dist/cjs/shared/t.native.js.map +1 -1
  134. package/dist/cjs/shared/textRidesText.native.js.map +1 -1
  135. package/dist/cjs/shared/useKeyboardInset.native.js.map +1 -1
  136. package/dist/cjs/shared/useReturnFocusOnClose.native.js.map +1 -1
  137. package/dist/cjs/surfaces.native.js +1 -1
  138. package/dist/cjs/surfaces.native.js.map +1 -1
  139. package/dist/cjs/tamagui.native.js.map +1 -1
  140. package/dist/cjs/testSetup.native.js.map +1 -1
  141. package/dist/cjs/utils/setTinted.native.js.map +1 -1
  142. package/dist/cjs/utils/unwrapText.native.js.map +1 -1
  143. package/dist/cjs/views/Calendar.native.js.map +1 -1
  144. package/dist/cjs/views/Dashboard.native.js +1 -1
  145. package/dist/cjs/views/Dashboard.native.js.map +1 -1
  146. package/dist/cjs/views/Gantt.native.js +1 -1
  147. package/dist/cjs/views/Gantt.native.js.map +1 -1
  148. package/dist/cjs/views/ImageGrid.native.js +1 -1
  149. package/dist/cjs/views/ImageGrid.native.js.map +1 -1
  150. package/dist/cjs/views/List.native.js.map +1 -1
  151. package/dist/cjs/views/ReportBuilder.native.js +1 -1
  152. package/dist/cjs/views/ReportBuilder.native.js.map +1 -1
  153. package/dist/cjs/views/TreeView.native.js.map +1 -1
  154. package/dist/cjs/views/calendarMath.native.js.map +1 -1
  155. package/dist/cjs/views/ganttMath.native.js.map +1 -1
  156. package/dist/cjs/views/index.native.js +1 -1
  157. package/dist/cjs/views/index.native.js.map +1 -1
  158. package/dist/cjs/views/kanbanDnd.native.js.map +1 -1
  159. package/dist/cjs/views/treeViewDnd.native.js.map +1 -1
  160. package/dist/cjs/views/treeViewLayout.native.js.map +1 -1
  161. package/dist/cjs/views/treeViewMotion.native.js.map +1 -1
  162. package/dist/esm/Alert/index.mjs +14 -31
  163. package/dist/esm/Alert/index.mjs.map +1 -1
  164. package/dist/esm/Alert/index.native.js +36 -57
  165. package/dist/esm/Alert/index.native.js.map +1 -1
  166. package/dist/esm/actions/ActionBar.mjs +6 -4
  167. package/dist/esm/actions/ActionBar.mjs.map +1 -1
  168. package/dist/esm/actions/ActionBar.native.js +13 -4
  169. package/dist/esm/actions/ActionBar.native.js.map +1 -1
  170. package/dist/jsx/Alert/index.mjs +14 -31
  171. package/dist/jsx/Alert/index.mjs.map +1 -1
  172. package/dist/jsx/Alert/index.native.js +17 -37
  173. package/dist/jsx/Alert/index.native.js.map +1 -1
  174. package/dist/jsx/actions/ActionBar.mjs +6 -4
  175. package/dist/jsx/actions/ActionBar.mjs.map +1 -1
  176. package/dist/jsx/actions/ActionBar.native.js +11 -2
  177. package/dist/jsx/actions/ActionBar.native.js.map +1 -1
  178. package/dist/jsx/feedback/NotifyHost.native.js +1 -1
  179. package/dist/jsx/index.native.js +1 -1
  180. package/dist/jsx/layouts/AsyncBoundary.native.js +1 -1
  181. package/dist/jsx/layouts/AsyncBoundary.native.js.map +1 -1
  182. package/package.json +9 -9
  183. package/src/Alert/index.tsx +9 -4
  184. package/src/AnimateHeight/AnimateHeight.stories.spec.tsx +37 -0
  185. package/src/AnimateHeight/AnimateHeight.stories.tsx +95 -0
  186. package/src/Avatar/Avatar.spec.tsx +4 -1
  187. package/src/Button.spec.tsx +26 -15
  188. package/src/SheetModal/SheetModal.stories.spec.tsx +54 -0
  189. package/src/SheetModal/SheetModal.stories.tsx +109 -0
  190. package/src/actions/ActionBar.stories.spec.tsx +45 -0
  191. package/src/actions/ActionBar.stories.tsx +161 -0
  192. package/src/actions/ActionBar.tsx +8 -1
  193. package/src/charts/Axis.stories.spec.tsx +86 -0
  194. package/src/charts/Axis.stories.tsx +186 -0
  195. package/src/charts/ChartSurface.stories.spec.tsx +43 -0
  196. package/src/charts/ChartSurface.stories.tsx +148 -0
  197. package/types/Alert/index.d.ts.map +1 -1
  198. package/types/actions/ActionBar.d.ts.map +1 -1
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The story file itself is the artifact under test here: MPO-193 landed
3
+ * AnimateHeight's first Kitchen Sink title, and a story that never mounts is
4
+ * the failure mode the previous wave shipped. These render every exported
5
+ * story through the real providers so a broken import or a bad arg fails the
6
+ * package tests instead of the gallery.
7
+ */
8
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
9
+ import { cleanup, screen } from "@testing-library/react";
10
+ import { afterEach, describe, expect, it } from "vitest";
11
+ import meta, { AlwaysOpen, Default, FollowsContent } from "./AnimateHeight.stories";
12
+
13
+ afterEach(cleanup);
14
+
15
+ describe("AnimateHeight stories", () => {
16
+ it("declares the Components/AnimateHeight title against the component", () => {
17
+ expect(meta.title).toBe("Components/AnimateHeight");
18
+ expect(meta.component).toBeDefined();
19
+ });
20
+
21
+ it("Main mounts collapsed with the expand affordance", () => {
22
+ renderWithProviders(<>{Default.render?.({} as never, {} as never)}</>);
23
+ expect(screen.getByText("Expand")).toBeTruthy();
24
+ });
25
+
26
+ it("Follower mode mounts with content already visible", () => {
27
+ renderWithProviders(<>{FollowsContent.render?.({} as never, {} as never)}</>);
28
+ expect(screen.getByText("Height is measured, not guessed.")).toBeTruthy();
29
+ });
30
+
31
+ it("Open renders its children when open is true", () => {
32
+ renderWithProviders(
33
+ <>{AlwaysOpen.render?.(AlwaysOpen.args as never, { args: AlwaysOpen.args } as never)}</>,
34
+ );
35
+ expect(screen.getByText("Toggle `open` in the controls panel.")).toBeTruthy();
36
+ });
37
+ });
@@ -0,0 +1,95 @@
1
+ import { Button } from "@multiplatform.one/forms";
2
+ import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
+ import { useState } from "react";
4
+ import { Paragraph, SizableText, YStack } from "tamagui";
5
+ import { Card } from "../surfaces";
6
+ import { AnimateHeight } from "./index";
7
+
8
+ const lines = [
9
+ "Height is measured, not guessed.",
10
+ "CSS cannot transition to height: auto, so the box drives a numeric height.",
11
+ "Web measures with a ResizeObserver; native measures with onLayout.",
12
+ "Padding belongs inside the children, never on the container.",
13
+ ];
14
+
15
+ function Collapsible() {
16
+ const [open, setOpen] = useState(false);
17
+ return (
18
+ <YStack gap="$3" maxWidth={420}>
19
+ <Button onPress={() => setOpen((value) => !value)}>{open ? "Collapse" : "Expand"}</Button>
20
+ <AnimateHeight open={open}>
21
+ <Card>
22
+ <YStack gap="$2">
23
+ {lines.map((line) => (
24
+ <Paragraph key={line}>{line}</Paragraph>
25
+ ))}
26
+ </YStack>
27
+ </Card>
28
+ </AnimateHeight>
29
+ </YStack>
30
+ );
31
+ }
32
+
33
+ function Follower() {
34
+ const [count, setCount] = useState(1);
35
+ return (
36
+ <YStack gap="$3" maxWidth={420}>
37
+ <Button onPress={() => setCount((value) => (value >= lines.length ? 1 : value + 1))}>
38
+ {`Add a line (${count}/${lines.length})`}
39
+ </Button>
40
+ <AnimateHeight>
41
+ <Card>
42
+ <YStack gap="$2">
43
+ {lines.slice(0, count).map((line) => (
44
+ <Paragraph key={line}>{line}</Paragraph>
45
+ ))}
46
+ </YStack>
47
+ </Card>
48
+ </AnimateHeight>
49
+ </YStack>
50
+ );
51
+ }
52
+
53
+ const meta: Meta<typeof AnimateHeight> = {
54
+ title: "Components/AnimateHeight",
55
+ component: AnimateHeight,
56
+ parameters: {
57
+ status: { type: "beta" },
58
+ docs: {
59
+ description: {
60
+ component:
61
+ 'Auto-height container. CSS never interpolates `height: auto`, so this measures the content (ResizeObserver on web, onLayout on native) and tweens a numeric height through the knob `transition`. Two modes: omit `open` for follower (height tracks content while mounted), pass `open` for collapsible (children unmount once the collapse settles). Under reduced motion or `animation="none"` the knob transition is undefined and heights apply instantly, so state changes still land.',
62
+ },
63
+ },
64
+ },
65
+ argTypes: {
66
+ open: { control: "boolean" },
67
+ },
68
+ };
69
+ export default meta;
70
+
71
+ type Story = StoryObj<typeof AnimateHeight>;
72
+
73
+ export const Default: Story = {
74
+ name: "Main",
75
+ render: () => <Collapsible />,
76
+ };
77
+
78
+ export const FollowsContent: Story = {
79
+ name: "Follower mode",
80
+ render: () => <Follower />,
81
+ };
82
+
83
+ export const AlwaysOpen: Story = {
84
+ name: "Open",
85
+ args: { open: true },
86
+ render: (args) => (
87
+ <YStack maxWidth={420}>
88
+ <AnimateHeight {...args}>
89
+ <Card>
90
+ <SizableText>Toggle `open` in the controls panel.</SizableText>
91
+ </Card>
92
+ </AnimateHeight>
93
+ </YStack>
94
+ ),
95
+ };
@@ -67,7 +67,10 @@ describe("Avatar", () => {
67
67
  const compact = renderWithProviders(<Avatar name="Ada Lovelace" compact />);
68
68
  expect(pxOf(page.container)).toBe(36);
69
69
  expect(pxOf(nested.container)).toBe(32);
70
- expect(pxOf(compact.container)).toBe(28);
70
+ // LC-76: density is not size. `compact` steps SPACE only (see
71
+ // resolveKnobs.applyDensity / compactSpaceMap) — it must not shrink a
72
+ // painted control. Only `nested` and an explicit size token move the face.
73
+ expect(pxOf(compact.container)).toBe(36);
71
74
  expect(
72
75
  nested.container.querySelector("[data-avatar-nested]")?.getAttribute("data-avatar-nested"),
73
76
  ).toBe("true");
@@ -119,21 +119,32 @@ describe("components Button — elevation and size knobs reach the frame", () =>
119
119
  expect(screen.getByRole("button").className).toMatch(/_bxsh-/);
120
120
  });
121
121
 
122
- it("size knob drives the frame height token", () => {
123
- renderWithProviders(
124
- <Preset overrides={{ size: "small" }}>
125
- <Button>Save</Button>
126
- </Preset>,
127
- );
128
- expect(screen.getByRole("button").className).toContain("_h-t-size-3");
129
- cleanup();
130
-
131
- renderWithProviders(
132
- <Preset overrides={{ size: "large" }}>
133
- <Button>Save</Button>
134
- </Preset>,
135
- );
136
- expect(screen.getByRole("button").className).toContain("_h-t-size-5");
122
+ it("size knob drives the frame height", () => {
123
+ // The generated size recipes resolve a control height to a concrete px
124
+ // atom, not a `$size` token class — asserting `_h-t-size-N` tested the
125
+ // pre-recipe emission shape, not the knob. Read the height back instead:
126
+ // small/medium/large must be three distinct, ascending heights.
127
+ const heightOf = () => {
128
+ const atom = screen
129
+ .getByRole("button")
130
+ .className.split(" ")
131
+ .find((c) => /^_h-\d+px$/.test(c));
132
+ expect(atom).toBeDefined();
133
+ return Number(atom!.replace(/^_h-|px$/g, ""));
134
+ };
135
+
136
+ const heights: number[] = [];
137
+ for (const size of ["small", "medium", "large"] as const) {
138
+ renderWithProviders(
139
+ <Preset overrides={{ size }}>
140
+ <Button>Save</Button>
141
+ </Preset>,
142
+ );
143
+ heights.push(heightOf());
144
+ cleanup();
145
+ }
146
+ expect(heights[0]).toBeLessThan(heights[1]);
147
+ expect(heights[1]).toBeLessThan(heights[2]);
137
148
  });
138
149
  });
139
150
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * SheetModal was the harness inside SheetFrame.stories and had no title of its
3
+ * own, so the gallery listed the frame and not the sheet (MPO-193). These
4
+ * mount every exported story so an unrenderable one fails here, not in the
5
+ * Kitchen Sink.
6
+ *
7
+ * Presence is asserted by accessible NAME, not by counting dialogs: tamagui's
8
+ * Sheet keeps its portal in the tree either side of `open`, and happy-dom runs
9
+ * no animation, so `role="dialog"` matches more than one node whatever the
10
+ * open state is.
11
+ */
12
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
13
+ import { cleanup, screen } from "@testing-library/react";
14
+ import { afterEach, describe, expect, it } from "vitest";
15
+ import meta, { Default, OpenOnMount, Scrollable, TallDetent } from "./SheetModal.stories";
16
+
17
+ afterEach(cleanup);
18
+
19
+ describe("SheetModal stories", () => {
20
+ it("declares the Components/SheetModal title against the component", () => {
21
+ expect(meta.title).toBe("Components/SheetModal");
22
+ expect(meta.component).toBeDefined();
23
+ });
24
+
25
+ it("every exported story mounts its trigger", () => {
26
+ for (const story of [Default, Scrollable, TallDetent, OpenOnMount]) {
27
+ renderWithProviders(<>{story.render?.({} as never, {} as never)}</>);
28
+ expect(screen.getAllByText("Open sheet").length).toBeGreaterThan(0);
29
+ cleanup();
30
+ }
31
+ });
32
+
33
+ it("the sheet frame is a labelled modal dialog", () => {
34
+ renderWithProviders(<>{OpenOnMount.render?.({} as never, {} as never)}</>);
35
+ // tamagui renders the sheet through a portal AND inline, so the frame
36
+ // appears more than once with the same accessible name; every copy has to
37
+ // carry the modal semantics.
38
+ const frames = screen.getAllByRole("dialog", { name: "Move to project" });
39
+ expect(frames.length).toBeGreaterThan(0);
40
+ for (const frame of frames) expect(frame.getAttribute("aria-modal")).toBe("true");
41
+ });
42
+
43
+ it("the action row inside the sheet carries cancel and primary", () => {
44
+ renderWithProviders(<>{OpenOnMount.render?.({} as never, {} as never)}</>);
45
+ expect(screen.getAllByText("Cancel").length).toBeGreaterThan(0);
46
+ expect(screen.getAllByText("Move").length).toBeGreaterThan(0);
47
+ });
48
+
49
+ it("the scrollable story fills the body with rows", () => {
50
+ renderWithProviders(<>{Scrollable.render?.({} as never, {} as never)}</>);
51
+ expect(screen.getAllByText("Project 1").length).toBeGreaterThan(0);
52
+ expect(screen.getAllByText("Project 30").length).toBeGreaterThan(0);
53
+ });
54
+ });
@@ -0,0 +1,109 @@
1
+ import { Button } from "@multiplatform.one/forms";
2
+ import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
+ import { useState } from "react";
4
+ import { Paragraph, SizableText, YStack } from "tamagui";
5
+ import { ActionBar } from "../actions/ActionBar";
6
+ import { SheetModal } from "./index";
7
+
8
+ function Harness({
9
+ scrollable = false,
10
+ snapPoint,
11
+ startOpen = false,
12
+ }: {
13
+ scrollable?: boolean;
14
+ snapPoint?: number;
15
+ startOpen?: boolean;
16
+ }) {
17
+ const [open, setOpen] = useState(startOpen);
18
+ return (
19
+ <YStack minHeight={480} gap="$3">
20
+ <Button onPress={() => setOpen(true)}>Open sheet</Button>
21
+ <Paragraph size="$2">
22
+ Dismiss by tapping the scrim, dragging to the bottom, or pressing Escape.
23
+ </Paragraph>
24
+ <SheetModal
25
+ open={open}
26
+ onOpenChange={setOpen}
27
+ scrollable={scrollable}
28
+ {...(snapPoint === undefined ? {} : { snapPoint })}
29
+ header={<SizableText fontWeight="600">Move to project</SizableText>}
30
+ >
31
+ {scrollable ? (
32
+ <YStack gap="$2">
33
+ {Array.from({ length: 30 }).map((_, index) => (
34
+ <Paragraph key={index}>Project {index + 1}</Paragraph>
35
+ ))}
36
+ </YStack>
37
+ ) : (
38
+ <YStack gap="$3">
39
+ <Paragraph>
40
+ The sheet is a dialog region, so the action row inside it caps at two actions.
41
+ </Paragraph>
42
+ <ActionBar
43
+ cancel={
44
+ <Button chromeless onPress={() => setOpen(false)}>
45
+ Cancel
46
+ </Button>
47
+ }
48
+ primary={
49
+ <Button accent onPress={() => setOpen(false)}>
50
+ Move
51
+ </Button>
52
+ }
53
+ />
54
+ </YStack>
55
+ )}
56
+ </SheetModal>
57
+ </YStack>
58
+ );
59
+ }
60
+
61
+ const meta: Meta<typeof SheetModal> = {
62
+ title: "Components/SheetModal",
63
+ component: SheetModal,
64
+ parameters: {
65
+ status: { type: "beta" },
66
+ layout: "fullscreen",
67
+ docs: {
68
+ description: {
69
+ component:
70
+ "Modal bottom sheet. Anatomy follows the Apple HIG sheet (grabber, percent detent, swipe to dismiss) with Radix Dialog semantics: `role=dialog`, `aria-modal`, focus moved in, trapped, and returned on close, and Escape dismissal wired here because tamagui's Sheet has no key handling of its own. The frame opens a dialog region, so an ActionBar inside it inherits the two-action cap. Web renders tamagui's Sheet; the native twin is an RN Modal, because a raw Sheet cannot re-present from a trigger tap on device.",
71
+ },
72
+ },
73
+ },
74
+ argTypes: {
75
+ snapPoint: { control: { type: "range", min: 30, max: 95, step: 5 } },
76
+ scrollable: { control: "boolean" },
77
+ },
78
+ };
79
+ export default meta;
80
+
81
+ type Story = StoryObj<typeof SheetModal>;
82
+
83
+ export const Default: Story = {
84
+ name: "Main",
85
+ render: () => <Harness />,
86
+ };
87
+
88
+ export const Scrollable: Story = {
89
+ name: "Scrollable body",
90
+ render: () => <Harness scrollable />,
91
+ };
92
+
93
+ export const TallDetent: Story = {
94
+ name: "Tall detent",
95
+ parameters: {
96
+ docs: {
97
+ description: {
98
+ story:
99
+ "`snapPoint` is a percent of travel. 92 is the tallest height real callers pass (the command palette); every one of those was a no-op on device until the detent resolver landed.",
100
+ },
101
+ },
102
+ },
103
+ render: () => <Harness snapPoint={92} />,
104
+ };
105
+
106
+ export const OpenOnMount: Story = {
107
+ name: "Open",
108
+ render: () => <Harness startOpen />,
109
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * ActionBar was rendered inside DestructiveAction.stories but had no title of
3
+ * its own, so the gallery never listed it (MPO-193). These mount every
4
+ * exported story so an unrenderable one fails here, not in the Kitchen Sink.
5
+ */
6
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
7
+ import { cleanup, screen } from "@testing-library/react";
8
+ import { afterEach, describe, expect, it } from "vitest";
9
+ import meta, { Alignments, CancelNeverDisabled, Default, WithSecondary } from "./ActionBar.stories";
10
+
11
+ afterEach(cleanup);
12
+
13
+ describe("ActionBar stories", () => {
14
+ it("declares the Components/ActionBar title against the component", () => {
15
+ expect(meta.title).toBe("Components/ActionBar");
16
+ expect(meta.component).toBeDefined();
17
+ });
18
+
19
+ it("Main renders the cancel and primary slots", () => {
20
+ renderWithProviders(
21
+ <>{Default.render?.(Default.args as never, { args: Default.args } as never)}</>,
22
+ );
23
+ expect(screen.getByText("Cancel")).toBeTruthy();
24
+ expect(screen.getByText("Save changes")).toBeTruthy();
25
+ });
26
+
27
+ it("Cancel + secondary + primary renders every slot", () => {
28
+ renderWithProviders(<>{WithSecondary.render?.({} as never, {} as never)}</>);
29
+ expect(screen.getByText("Cancel")).toBeTruthy();
30
+ expect(screen.getByText("Save draft")).toBeTruthy();
31
+ expect(screen.getByText("Preview")).toBeTruthy();
32
+ expect(screen.getByText("Publish")).toBeTruthy();
33
+ });
34
+
35
+ it("Alignments renders one bar per align value", () => {
36
+ renderWithProviders(<>{Alignments.render?.({} as never, {} as never)}</>);
37
+ expect(screen.getAllByText("Confirm")).toHaveLength(3);
38
+ expect(screen.getByText('align="between"')).toBeTruthy();
39
+ });
40
+
41
+ it("the cancel-disabled story still paints a reachable cancel", () => {
42
+ renderWithProviders(<>{CancelNeverDisabled.render?.({} as never, {} as never)}</>);
43
+ expect(screen.getByText("Cancel")).toBeTruthy();
44
+ });
45
+ });
@@ -0,0 +1,161 @@
1
+ import { Button } from "@multiplatform.one/forms";
2
+ import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
+ import { useState } from "react";
4
+ import { Paragraph, YStack } from "tamagui";
5
+ import { Card } from "../surfaces";
6
+ import { ActionBar, type ActionBarProps } from "./ActionBar";
7
+
8
+ /**
9
+ * Presses land in the story itself rather than the Actions panel: the panel's
10
+ * `action()` pulls the Storybook preview runtime into the module graph, which
11
+ * the package's own vitest run cannot load, and the colocated spec mounts
12
+ * these stories for real.
13
+ */
14
+ function useLastPressed() {
15
+ const [last, setLast] = useState<string | null>(null);
16
+ return {
17
+ last,
18
+ press: (label: string) => () => setLast(label),
19
+ readout: (
20
+ <Paragraph size="$2" color="$color11">
21
+ {last === null ? "No action pressed yet." : `Last pressed: ${last}`}
22
+ </Paragraph>
23
+ ),
24
+ };
25
+ }
26
+
27
+ const meta: Meta<typeof ActionBar> = {
28
+ title: "Components/ActionBar",
29
+ component: ActionBar,
30
+ parameters: {
31
+ status: { type: "beta" },
32
+ docs: {
33
+ description: {
34
+ component:
35
+ "The action row. Slots (`primary`, `secondary`, `cancel`) keep the primary singular by construction and order the row cancel → secondary → primary, so the trailing default lands under the thumb. `align` picks the justification: `end` for dialogs and toolbars, `start` for page-level forms. DG-ACT-05 strips `disabled` off the cancel slot and DEV-warns; DG-ACT-01 DEV-warns on a second primary, including one composed into free children; DG-OVL-04 DEV-warns past two actions inside a dialog region unless `allowManyActions` ejects.",
36
+ },
37
+ },
38
+ },
39
+ argTypes: {
40
+ align: { control: "radio", options: ["start", "end", "between"] },
41
+ allowManyActions: { control: "boolean" },
42
+ },
43
+ };
44
+ export default meta;
45
+
46
+ type Story = StoryObj<typeof ActionBar>;
47
+
48
+ function Basic(args: Partial<ActionBarProps>) {
49
+ const { press, readout } = useLastPressed();
50
+ return (
51
+ <Card maxWidth={480}>
52
+ <YStack gap="$3">
53
+ {readout}
54
+ <ActionBar
55
+ {...args}
56
+ cancel={
57
+ <Button chromeless onPress={press("Cancel")}>
58
+ Cancel
59
+ </Button>
60
+ }
61
+ primary={
62
+ <Button accent onPress={press("Save changes")}>
63
+ Save changes
64
+ </Button>
65
+ }
66
+ />
67
+ </YStack>
68
+ </Card>
69
+ );
70
+ }
71
+
72
+ function FullRow() {
73
+ const { press, readout } = useLastPressed();
74
+ return (
75
+ <Card maxWidth={520}>
76
+ <YStack gap="$3">
77
+ {readout}
78
+ <ActionBar
79
+ cancel={
80
+ <Button chromeless onPress={press("Cancel")}>
81
+ Cancel
82
+ </Button>
83
+ }
84
+ secondary={[
85
+ <Button key="draft" onPress={press("Save draft")}>
86
+ Save draft
87
+ </Button>,
88
+ <Button key="preview" onPress={press("Preview")}>
89
+ Preview
90
+ </Button>,
91
+ ]}
92
+ primary={
93
+ <Button accent onPress={press("Publish")}>
94
+ Publish
95
+ </Button>
96
+ }
97
+ />
98
+ </YStack>
99
+ </Card>
100
+ );
101
+ }
102
+
103
+ export const Default: Story = {
104
+ name: "Main",
105
+ args: { align: "end" },
106
+ render: (args) => <Basic {...args} />,
107
+ };
108
+
109
+ export const WithSecondary: Story = {
110
+ name: "Cancel + secondary + primary",
111
+ render: () => <FullRow />,
112
+ };
113
+
114
+ export const Alignments: Story = {
115
+ render: () => (
116
+ <YStack gap="$4" maxWidth={520}>
117
+ {(["start", "end", "between"] as const).map((align) => (
118
+ <Card key={align}>
119
+ <YStack gap="$2">
120
+ <Paragraph size="$2">{`align="${align}"`}</Paragraph>
121
+ <ActionBar
122
+ align={align}
123
+ cancel={<Button chromeless>Cancel</Button>}
124
+ primary={<Button accent>Confirm</Button>}
125
+ />
126
+ </YStack>
127
+ </Card>
128
+ ))}
129
+ </YStack>
130
+ ),
131
+ };
132
+
133
+ export const CancelNeverDisabled: Story = {
134
+ name: "Cancel is never disabled",
135
+ parameters: {
136
+ docs: {
137
+ description: {
138
+ story:
139
+ "DG-ACT-05: the cancel slot is handed `disabled`, the prop is stripped, and DEV logs `[actions] cancel-disabled`. A dismissal a user cannot reach is a trap, so the escape hatch stays live even while the primary is blocked. Open the console to see the warn.",
140
+ },
141
+ },
142
+ },
143
+ render: () => {
144
+ return (
145
+ <Card maxWidth={480}>
146
+ <ActionBar
147
+ cancel={
148
+ <Button chromeless disabled>
149
+ Cancel
150
+ </Button>
151
+ }
152
+ primary={
153
+ <Button accent disabled>
154
+ Save changes
155
+ </Button>
156
+ }
157
+ />
158
+ </Card>
159
+ );
160
+ },
161
+ };
@@ -10,6 +10,7 @@
10
10
 
11
11
  import {
12
12
  Children,
13
+ Fragment,
13
14
  cloneElement,
14
15
  isValidElement,
15
16
  useEffect,
@@ -194,10 +195,16 @@ export function ActionBar({
194
195
  width="100%"
195
196
  {...stackProps}
196
197
  >
198
+ {/* `secondaryItems` is a plain array (asArray), so rendering it bare made
199
+ React warn "Each child in a list should have a unique key" and name
200
+ the CALLER — the screen that passed a single element into the slot —
201
+ instead of this file. Key it here, where the array is built. */}
197
202
  {hasSlots ? (
198
203
  <>
199
204
  {cancelItems}
200
- {secondaryItems}
205
+ {secondaryItems.map((node, index) => (
206
+ <Fragment key={`mp-action-secondary-${index}`}>{node}</Fragment>
207
+ ))}
201
208
  {primary}
202
209
  </>
203
210
  ) : null}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * XAxis and YAxis are exported from the charts barrel but had no title of
3
+ * their own — only the composed charts had stories (MPO-193). happy-dom runs
4
+ * no layout, so a measured ChartSurface stays at width 0 and draws nothing;
5
+ * these specs render the same axis frame at an explicit pixel size so the
6
+ * marks the stories show are actually asserted.
7
+ */
8
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
9
+ import { cleanup, screen } from "@testing-library/react";
10
+ import { afterEach, describe, expect, it } from "vitest";
11
+ import { ChartSurface } from "./ChartSurface";
12
+ import { XAxis, YAxis } from "./Axis";
13
+ import { computeCartesianPlot } from "./composeChart";
14
+ import { createBandScale, createLinearScale } from "./math";
15
+ import meta, { AxisLineAndTicks, Default, LabelTruncation, WideValueRange } from "./Axis.stories";
16
+
17
+ afterEach(cleanup);
18
+
19
+ const categories = ["Mon", "Tue", "Wed", "Thu", "Fri"];
20
+
21
+ function Frame({ showGrid = true }: { showGrid?: boolean }) {
22
+ return (
23
+ <ChartSurface label="Axis frame" width={400} height={200}>
24
+ {(size) => {
25
+ const plot = computeCartesianPlot(size, {
26
+ showXAxis: true,
27
+ showYAxis: true,
28
+ yTickLabels: ["0", "25", "50", "75", "100"],
29
+ fontSize: 12,
30
+ });
31
+ const xScale = createBandScale({
32
+ domain: categories,
33
+ range: [plot.x, plot.x + plot.width],
34
+ });
35
+ const yScale = createLinearScale({
36
+ domain: [0, 100],
37
+ range: [plot.y + plot.height, plot.y],
38
+ });
39
+ return (
40
+ <>
41
+ <YAxis plot={plot} scale={yScale} ticks={4} showGrid={showGrid} />
42
+ <XAxis plot={plot} scale={xScale} />
43
+ </>
44
+ );
45
+ }}
46
+ </ChartSurface>
47
+ );
48
+ }
49
+
50
+ describe("Axis stories", () => {
51
+ it("declares the Components/Axis title against the component", () => {
52
+ expect(meta.title).toBe("Components/Axis");
53
+ expect(meta.component).toBeDefined();
54
+ });
55
+
56
+ it("every exported story mounts", () => {
57
+ for (const story of [Default, AxisLineAndTicks, LabelTruncation, WideValueRange]) {
58
+ renderWithProviders(<>{story.render?.({} as never, {} as never)}</>);
59
+ cleanup();
60
+ }
61
+ expect(true).toBe(true);
62
+ });
63
+
64
+ it("Long labels truncate renders both container widths", () => {
65
+ renderWithProviders(<>{LabelTruncation.render?.({} as never, {} as never)}</>);
66
+ expect(screen.getByText("520px container")).toBeTruthy();
67
+ expect(screen.getByText("280px container")).toBeTruthy();
68
+ });
69
+
70
+ it("draws one x label per category and the y grid lines at a fixed size", () => {
71
+ const { container } = renderWithProviders(<Frame />);
72
+ const labels = Array.from(container.querySelectorAll("text")).map((node) => node.textContent);
73
+ for (const category of categories) expect(labels).toContain(category);
74
+ // 4-tick hint, plus the x baseline and one tick mark per category.
75
+ expect(container.querySelectorAll("line").length).toBeGreaterThan(categories.length);
76
+ });
77
+
78
+ it("showGrid=false drops the horizontal rules", () => {
79
+ const withGrid = renderWithProviders(<Frame />).container.querySelectorAll("line").length;
80
+ cleanup();
81
+ const withoutGrid = renderWithProviders(<Frame showGrid={false} />).container.querySelectorAll(
82
+ "line",
83
+ ).length;
84
+ expect(withoutGrid).toBeLessThan(withGrid);
85
+ });
86
+ });