@multiplatform.one/components 7.6.3 → 7.7.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 (232) hide show
  1. package/dist/cjs/Badge/index.cjs +4 -3
  2. package/dist/cjs/Badge/index.native.js +7 -6
  3. package/dist/cjs/Badge/index.native.js.map +1 -1
  4. package/dist/cjs/Card.cjs +1 -9
  5. package/dist/cjs/Card.native.js +2 -7
  6. package/dist/cjs/Card.native.js.map +1 -1
  7. package/dist/cjs/CardFooter.cjs +1 -9
  8. package/dist/cjs/CardFooter.native.js +2 -7
  9. package/dist/cjs/CardFooter.native.js.map +1 -1
  10. package/dist/cjs/CardHeader.cjs +1 -9
  11. package/dist/cjs/CardHeader.native.js +2 -7
  12. package/dist/cjs/CardHeader.native.js.map +1 -1
  13. package/dist/cjs/ProgressSteps/index.cjs +13 -6
  14. package/dist/cjs/ProgressSteps/index.native.js +14 -6
  15. package/dist/cjs/ProgressSteps/index.native.js.map +1 -1
  16. package/dist/cjs/SheetModal/detents.cjs +46 -0
  17. package/dist/cjs/SheetModal/detents.native.js +54 -0
  18. package/dist/cjs/SheetModal/detents.native.js.map +1 -0
  19. package/dist/cjs/SheetModal/index.native.js +8 -11
  20. package/dist/cjs/SheetModal/index.native.js.map +1 -1
  21. package/dist/cjs/SortableList/index.cjs +1 -1
  22. package/dist/cjs/SortableList/index.native.js +1 -1
  23. package/dist/cjs/SortableList/index.native.js.map +1 -1
  24. package/dist/cjs/code/Code/index.cjs +2 -15
  25. package/dist/cjs/code/Code/index.native.js +3 -13
  26. package/dist/cjs/code/Code/index.native.js.map +1 -1
  27. package/dist/cjs/code/HighlightedCode/index.cjs +0 -5
  28. package/dist/cjs/code/HighlightedCode/index.native.js +0 -5
  29. package/dist/cjs/code/HighlightedCode/index.native.js.map +1 -1
  30. package/dist/cjs/knobImmuneExports.cjs +8 -0
  31. package/dist/cjs/knobImmuneExports.native.js +8 -0
  32. package/dist/cjs/knobImmuneExports.native.js.map +1 -1
  33. package/dist/cjs/layouts/breadcrumbs.cjs +3 -1
  34. package/dist/cjs/layouts/breadcrumbs.native.js +3 -1
  35. package/dist/cjs/layouts/breadcrumbs.native.js.map +1 -1
  36. package/dist/cjs/tamagui.cjs +5 -0
  37. package/dist/cjs/tamagui.native.js +5 -0
  38. package/dist/cjs/tamagui.native.js.map +1 -1
  39. package/dist/cjs/views/Dashboard.cjs +3 -3
  40. package/dist/cjs/views/Dashboard.native.js +3 -3
  41. package/dist/cjs/views/Dashboard.native.js.map +1 -1
  42. package/dist/cjs/views/List.cjs +4 -5
  43. package/dist/cjs/views/List.native.js +4 -5
  44. package/dist/cjs/views/List.native.js.map +1 -1
  45. package/dist/cjs/views/TreeView.cjs +4 -1
  46. package/dist/cjs/views/TreeView.native.js +3 -0
  47. package/dist/cjs/views/TreeView.native.js.map +1 -1
  48. package/dist/esm/Badge/index.mjs +4 -3
  49. package/dist/esm/Badge/index.mjs.map +1 -1
  50. package/dist/esm/Badge/index.native.js +7 -6
  51. package/dist/esm/Badge/index.native.js.map +1 -1
  52. package/dist/esm/Card.mjs +3 -11
  53. package/dist/esm/Card.mjs.map +1 -1
  54. package/dist/esm/Card.native.js +7 -12
  55. package/dist/esm/Card.native.js.map +1 -1
  56. package/dist/esm/CardFooter.mjs +3 -11
  57. package/dist/esm/CardFooter.mjs.map +1 -1
  58. package/dist/esm/CardFooter.native.js +6 -11
  59. package/dist/esm/CardFooter.native.js.map +1 -1
  60. package/dist/esm/CardHeader.mjs +3 -11
  61. package/dist/esm/CardHeader.mjs.map +1 -1
  62. package/dist/esm/CardHeader.native.js +6 -11
  63. package/dist/esm/CardHeader.native.js.map +1 -1
  64. package/dist/esm/ProgressSteps/index.mjs +14 -7
  65. package/dist/esm/ProgressSteps/index.mjs.map +1 -1
  66. package/dist/esm/ProgressSteps/index.native.js +15 -7
  67. package/dist/esm/ProgressSteps/index.native.js.map +1 -1
  68. package/dist/esm/SheetModal/detents.mjs +19 -0
  69. package/dist/esm/SheetModal/detents.mjs.map +1 -0
  70. package/dist/esm/SheetModal/detents.native.js +24 -0
  71. package/dist/esm/SheetModal/detents.native.js.map +1 -0
  72. package/dist/esm/SheetModal/index.mjs.map +1 -1
  73. package/dist/esm/SheetModal/index.native.js +7 -10
  74. package/dist/esm/SheetModal/index.native.js.map +1 -1
  75. package/dist/esm/SortableList/index.mjs +1 -1
  76. package/dist/esm/SortableList/index.mjs.map +1 -1
  77. package/dist/esm/SortableList/index.native.js +1 -1
  78. package/dist/esm/SortableList/index.native.js.map +1 -1
  79. package/dist/esm/code/Code/index.mjs +2 -15
  80. package/dist/esm/code/Code/index.mjs.map +1 -1
  81. package/dist/esm/code/Code/index.native.js +6 -16
  82. package/dist/esm/code/Code/index.native.js.map +1 -1
  83. package/dist/esm/code/HighlightedCode/index.mjs +0 -5
  84. package/dist/esm/code/HighlightedCode/index.mjs.map +1 -1
  85. package/dist/esm/code/HighlightedCode/index.native.js +0 -5
  86. package/dist/esm/code/HighlightedCode/index.native.js.map +1 -1
  87. package/dist/esm/knobImmuneExports.mjs +8 -0
  88. package/dist/esm/knobImmuneExports.mjs.map +1 -1
  89. package/dist/esm/knobImmuneExports.native.js +8 -0
  90. package/dist/esm/knobImmuneExports.native.js.map +1 -1
  91. package/dist/esm/layouts/breadcrumbs.mjs +3 -1
  92. package/dist/esm/layouts/breadcrumbs.mjs.map +1 -1
  93. package/dist/esm/layouts/breadcrumbs.native.js +3 -1
  94. package/dist/esm/layouts/breadcrumbs.native.js.map +1 -1
  95. package/dist/esm/tamagui.mjs +3 -2
  96. package/dist/esm/tamagui.mjs.map +1 -1
  97. package/dist/esm/tamagui.native.js +3 -2
  98. package/dist/esm/tamagui.native.js.map +1 -1
  99. package/dist/esm/views/Dashboard.mjs +3 -3
  100. package/dist/esm/views/Dashboard.mjs.map +1 -1
  101. package/dist/esm/views/Dashboard.native.js +3 -3
  102. package/dist/esm/views/Dashboard.native.js.map +1 -1
  103. package/dist/esm/views/List.mjs +4 -5
  104. package/dist/esm/views/List.mjs.map +1 -1
  105. package/dist/esm/views/List.native.js +4 -5
  106. package/dist/esm/views/List.native.js.map +1 -1
  107. package/dist/esm/views/TreeView.mjs +4 -1
  108. package/dist/esm/views/TreeView.mjs.map +1 -1
  109. package/dist/esm/views/TreeView.native.js +3 -0
  110. package/dist/esm/views/TreeView.native.js.map +1 -1
  111. package/dist/jsx/Badge/index.mjs +4 -3
  112. package/dist/jsx/Badge/index.mjs.map +1 -1
  113. package/dist/jsx/Badge/index.native.js +7 -6
  114. package/dist/jsx/Badge/index.native.js.map +1 -1
  115. package/dist/jsx/Card.mjs +3 -11
  116. package/dist/jsx/Card.mjs.map +1 -1
  117. package/dist/jsx/Card.native.js +2 -7
  118. package/dist/jsx/Card.native.js.map +1 -1
  119. package/dist/jsx/CardFooter.mjs +3 -11
  120. package/dist/jsx/CardFooter.mjs.map +1 -1
  121. package/dist/jsx/CardFooter.native.js +2 -7
  122. package/dist/jsx/CardFooter.native.js.map +1 -1
  123. package/dist/jsx/CardHeader.mjs +3 -11
  124. package/dist/jsx/CardHeader.mjs.map +1 -1
  125. package/dist/jsx/CardHeader.native.js +2 -7
  126. package/dist/jsx/CardHeader.native.js.map +1 -1
  127. package/dist/jsx/ProgressSteps/index.mjs +14 -7
  128. package/dist/jsx/ProgressSteps/index.mjs.map +1 -1
  129. package/dist/jsx/ProgressSteps/index.native.js +14 -6
  130. package/dist/jsx/ProgressSteps/index.native.js.map +1 -1
  131. package/dist/jsx/SheetModal/detents.mjs +19 -0
  132. package/dist/jsx/SheetModal/detents.mjs.map +1 -0
  133. package/dist/jsx/SheetModal/detents.native.js +54 -0
  134. package/dist/jsx/SheetModal/detents.native.js.map +1 -0
  135. package/dist/jsx/SheetModal/index.mjs.map +1 -1
  136. package/dist/jsx/SheetModal/index.native.js +8 -11
  137. package/dist/jsx/SheetModal/index.native.js.map +1 -1
  138. package/dist/jsx/SortableList/index.mjs +1 -1
  139. package/dist/jsx/SortableList/index.mjs.map +1 -1
  140. package/dist/jsx/SortableList/index.native.js +1 -1
  141. package/dist/jsx/SortableList/index.native.js.map +1 -1
  142. package/dist/jsx/code/Code/index.mjs +2 -15
  143. package/dist/jsx/code/Code/index.mjs.map +1 -1
  144. package/dist/jsx/code/Code/index.native.js +3 -13
  145. package/dist/jsx/code/Code/index.native.js.map +1 -1
  146. package/dist/jsx/code/CodeBlock/index.native.js +1 -1
  147. package/dist/jsx/code/HighlightedCode/index.mjs +0 -5
  148. package/dist/jsx/code/HighlightedCode/index.mjs.map +1 -1
  149. package/dist/jsx/code/HighlightedCode/index.native.js +0 -5
  150. package/dist/jsx/code/HighlightedCode/index.native.js.map +1 -1
  151. package/dist/jsx/code/index.native.js +1 -1
  152. package/dist/jsx/knobImmuneExports.mjs +8 -0
  153. package/dist/jsx/knobImmuneExports.mjs.map +1 -1
  154. package/dist/jsx/knobImmuneExports.native.js +8 -0
  155. package/dist/jsx/knobImmuneExports.native.js.map +1 -1
  156. package/dist/jsx/layouts/breadcrumbs.mjs +3 -1
  157. package/dist/jsx/layouts/breadcrumbs.mjs.map +1 -1
  158. package/dist/jsx/layouts/breadcrumbs.native.js +3 -1
  159. package/dist/jsx/layouts/breadcrumbs.native.js.map +1 -1
  160. package/dist/jsx/tamagui.mjs +3 -2
  161. package/dist/jsx/tamagui.mjs.map +1 -1
  162. package/dist/jsx/tamagui.native.js +5 -0
  163. package/dist/jsx/tamagui.native.js.map +1 -1
  164. package/dist/jsx/views/Dashboard.mjs +3 -3
  165. package/dist/jsx/views/Dashboard.mjs.map +1 -1
  166. package/dist/jsx/views/Dashboard.native.js +3 -3
  167. package/dist/jsx/views/Dashboard.native.js.map +1 -1
  168. package/dist/jsx/views/List.mjs +4 -5
  169. package/dist/jsx/views/List.mjs.map +1 -1
  170. package/dist/jsx/views/List.native.js +4 -5
  171. package/dist/jsx/views/List.native.js.map +1 -1
  172. package/dist/jsx/views/TreeView.mjs +4 -1
  173. package/dist/jsx/views/TreeView.mjs.map +1 -1
  174. package/dist/jsx/views/TreeView.native.js +3 -0
  175. package/dist/jsx/views/TreeView.native.js.map +1 -1
  176. package/package.json +9 -9
  177. package/src/Badge/Badge.spec.tsx +10 -11
  178. package/src/Badge/index.tsx +7 -6
  179. package/src/Card.tsx +8 -3
  180. package/src/CardFooter.spec.tsx +1 -1
  181. package/src/CardFooter.tsx +6 -3
  182. package/src/CardHeader.spec.tsx +1 -1
  183. package/src/CardHeader.tsx +6 -3
  184. package/src/Chip/Chip.spec.tsx +1 -1
  185. package/src/Pagination.spec.tsx +1 -1
  186. package/src/ProgressSteps/ProgressSteps.spec.tsx +29 -2
  187. package/src/ProgressSteps/index.tsx +21 -7
  188. package/src/SheetModal/SheetModal.spec.tsx +88 -0
  189. package/src/SheetModal/detents.ts +55 -0
  190. package/src/SheetModal/index.native.tsx +13 -12
  191. package/src/SheetModal/index.tsx +5 -2
  192. package/src/Skeleton/Skeleton.spec.tsx +1 -1
  193. package/src/SortableList/SortableList.spec.tsx +41 -0
  194. package/src/SortableList/SortableList.stories.tsx +35 -2
  195. package/src/SortableList/index.tsx +1 -1
  196. package/src/TreeSelect/TreeSelect.stories.tsx +76 -1
  197. package/src/Wheel/Wheel.spec.tsx +1 -1
  198. package/src/code/Code/code.spec.tsx +27 -1
  199. package/src/code/Code/index.tsx +9 -2
  200. package/src/code/HighlightedCode/index.tsx +0 -1
  201. package/src/code/highlightedCode.spec.tsx +10 -1
  202. package/src/knobImmuneExports.ts +11 -0
  203. package/src/layouts/AsyncBoundary.spec.tsx +1 -1
  204. package/src/layouts/breadcrumbs.spec.tsx +17 -0
  205. package/src/layouts/breadcrumbs.tsx +19 -2
  206. package/src/layouts/tags.r7.spec.tsx +3 -1
  207. package/src/tamagui.ts +8 -0
  208. package/src/views/Dashboard.tsx +3 -3
  209. package/src/views/List.spec.tsx +42 -1
  210. package/src/views/List.tsx +6 -5
  211. package/src/views/TreeView.spec.tsx +26 -0
  212. package/src/views/TreeView.tsx +3 -0
  213. package/types/Badge/index.d.ts.map +1 -1
  214. package/types/Card.d.ts.map +1 -1
  215. package/types/CardFooter.d.ts.map +1 -1
  216. package/types/CardHeader.d.ts.map +1 -1
  217. package/types/ProgressSteps/index.d.ts.map +1 -1
  218. package/types/SheetModal/detents.d.ts +38 -0
  219. package/types/SheetModal/detents.d.ts.map +1 -0
  220. package/types/SheetModal/index.d.ts +1 -1
  221. package/types/SheetModal/index.d.ts.map +1 -1
  222. package/types/SheetModal/index.native.d.ts +1 -1
  223. package/types/SheetModal/index.native.d.ts.map +1 -1
  224. package/types/code/Code/index.d.ts.map +1 -1
  225. package/types/code/HighlightedCode/index.d.ts.map +1 -1
  226. package/types/knobImmuneExports.d.ts.map +1 -1
  227. package/types/layouts/breadcrumbs.d.ts +7 -1
  228. package/types/layouts/breadcrumbs.d.ts.map +1 -1
  229. package/types/tamagui.d.ts +3 -1
  230. package/types/tamagui.d.ts.map +1 -1
  231. package/types/views/List.d.ts.map +1 -1
  232. package/types/views/TreeView.d.ts.map +1 -1
@@ -1,9 +1,14 @@
1
1
  import { fireEvent } from "@testing-library/react";
2
2
  import { renderWithProviders } from "@multiplatform.one/test-utils";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import { useState } from "react";
4
7
  import { describe, expect, it, vi } from "vitest";
5
8
  import { SortableList } from "./index";
6
9
 
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+
7
12
  const ITEMS = [
8
13
  { id: "mon", label: "Monday standup" },
9
14
  { id: "tue", label: "Tuesday review" },
@@ -14,10 +19,14 @@ const ITEMS = [
14
19
  function Harness({
15
20
  onChange = vi.fn(),
16
21
  readOnly,
22
+ disabled,
23
+ compact,
17
24
  canReorder,
18
25
  }: {
19
26
  onChange?: (next: typeof ITEMS) => void;
20
27
  readOnly?: boolean;
28
+ disabled?: boolean;
29
+ compact?: boolean;
21
30
  canReorder?: (item: (typeof ITEMS)[number], index: number) => boolean;
22
31
  }) {
23
32
  const [items, setItems] = useState(ITEMS);
@@ -28,6 +37,8 @@ function Harness({
28
37
  getLabel={(item) => item.label}
29
38
  canReorder={canReorder}
30
39
  readOnly={readOnly}
40
+ disabled={disabled}
41
+ compact={compact}
31
42
  onChange={(next) => {
32
43
  setItems(next);
33
44
  onChange(next);
@@ -119,4 +130,34 @@ describe("SortableList", () => {
119
130
  const { container } = renderWithProviders(<Harness readOnly />);
120
131
  expect(container.querySelectorAll("[data-sortable-handle]")).toHaveLength(0);
121
132
  });
133
+
134
+ it("renders no handles when disabled (board 04)", () => {
135
+ const { container } = renderWithProviders(<Harness disabled />);
136
+ expect(container.querySelectorAll("[data-sortable-handle]")).toHaveLength(0);
137
+ });
138
+
139
+ it("renders emptyMessage when there are no rows (board 04)", () => {
140
+ const { container } = renderWithProviders(
141
+ <SortableList
142
+ items={[]}
143
+ getId={(item: { id: string }) => item.id}
144
+ getLabel={(item: { label: string }) => item.label}
145
+ emptyMessage="No rows yet"
146
+ />,
147
+ );
148
+ expect(container.textContent).toContain("No rows yet");
149
+ expect(container.querySelectorAll("[data-sortable-row]")).toHaveLength(0);
150
+ });
151
+
152
+ it("compact still renders grab handles (LC-76: density does not drop controls)", () => {
153
+ const { container } = renderWithProviders(<Harness compact />);
154
+ expect(container.querySelectorAll("[data-sortable-handle]")).toHaveLength(4);
155
+ expect(container.querySelector('[role="list"]')).toBeTruthy();
156
+ });
157
+
158
+ it("spreads the outer radius fragment on the list frame, not the nested alias", () => {
159
+ const src = readFileSync(join(here, "index.tsx"), "utf8");
160
+ expect(src).toContain("{...knobProps.borderRadius}");
161
+ expect(src).not.toContain("knobProps.borderRadiusOuter");
162
+ });
122
163
  });
@@ -21,13 +21,27 @@ const meta: Meta<typeof SortableList> = {
21
21
  },
22
22
  },
23
23
  },
24
+ argTypes: {
25
+ disabled: { control: "boolean" },
26
+ readOnly: { control: "boolean" },
27
+ compact: { control: "boolean" },
28
+ disabledStyle: { control: "select", options: ["keepLabel", "dimWhole"] },
29
+ emptyMessage: { control: "text" },
30
+ },
24
31
  };
25
32
  export default meta;
26
33
 
27
34
  type Story = StoryObj<typeof SortableList>;
28
35
 
29
- function Demo(props: { readOnly?: boolean; locked?: string }) {
30
- const [items, setItems] = useState(SEED);
36
+ function Demo(props: {
37
+ readOnly?: boolean;
38
+ locked?: string;
39
+ disabled?: boolean;
40
+ compact?: boolean;
41
+ empty?: boolean;
42
+ disabledStyle?: "keepLabel" | "dimWhole";
43
+ }) {
44
+ const [items, setItems] = useState(props.empty ? [] : SEED);
31
45
  return (
32
46
  <SortableList
33
47
  items={items}
@@ -35,6 +49,10 @@ function Demo(props: { readOnly?: boolean; locked?: string }) {
35
49
  getLabel={(item) => item.label}
36
50
  canReorder={props.locked ? (item) => item.id !== props.locked : undefined}
37
51
  readOnly={props.readOnly}
52
+ disabled={props.disabled}
53
+ compact={props.compact}
54
+ disabledStyle={props.disabledStyle}
55
+ emptyMessage="No rows yet"
38
56
  onChange={setItems}
39
57
  aria-label="Sprint rituals"
40
58
  />
@@ -53,3 +71,18 @@ export const ReadOnly: Story = {
53
71
  export const LockedRow: Story = {
54
72
  render: () => <Demo locked="tue" />,
55
73
  };
74
+
75
+ /** Board 04 — empty is a real state, not an error. */
76
+ export const Empty: Story = {
77
+ render: () => <Demo empty />,
78
+ };
79
+
80
+ /** Board 04 — disabled list keeps labels, drops the handles. */
81
+ export const Disabled: Story = {
82
+ render: () => <Demo disabled />,
83
+ };
84
+
85
+ /** Board 04 — compact nested well. */
86
+ export const Compact: Story = {
87
+ render: () => <Demo compact />,
88
+ };
@@ -90,7 +90,7 @@ export function SortableList<T>({
90
90
  <YStack
91
91
  ref={containerRef as never}
92
92
  {...knobProps.surface}
93
- {...knobProps.borderRadiusOuter}
93
+ {...knobProps.borderRadius}
94
94
  overflow="hidden"
95
95
  role="list"
96
96
  aria-label={ariaLabel}
@@ -47,7 +47,7 @@ export default meta;
47
47
 
48
48
  type Story = StoryObj<typeof TreeSelect>;
49
49
 
50
- export const Account: Story = {
50
+ export const Default: Story = {
51
51
  render: function Render() {
52
52
  const [value, setValue] = useState("hdfc");
53
53
  return (
@@ -67,6 +67,31 @@ export const Account: Story = {
67
67
  },
68
68
  };
69
69
 
70
+ export const Open: Story = {
71
+ render: function Render() {
72
+ const [value, setValue] = useState("hdfc");
73
+ const [open, setOpen] = useState(true);
74
+ return (
75
+ <YStack width={280} padding="$4">
76
+ <TreeSelect
77
+ label="Account"
78
+ nodes={accounts}
79
+ value={value}
80
+ onChange={setValue}
81
+ open={open}
82
+ onOpenChange={setOpen}
83
+ placeholder="Select account"
84
+ searchPlaceholder="Filter accounts"
85
+ emptyMessage="No matching accounts"
86
+ defaultExpandedIds={["assets", "current", "bank"]}
87
+ />
88
+ </YStack>
89
+ );
90
+ },
91
+ };
92
+
93
+ export const Account: Story = Default;
94
+
70
95
  export const Empty: Story = {
71
96
  render: () => (
72
97
  <YStack width={280} padding="$4">
@@ -74,3 +99,53 @@ export const Empty: Story = {
74
99
  </YStack>
75
100
  ),
76
101
  };
102
+
103
+ export const Disabled: Story = {
104
+ render: () => (
105
+ <YStack width={280} padding="$4">
106
+ <TreeSelect
107
+ label="Account"
108
+ nodes={accounts}
109
+ value="hdfc"
110
+ disabled
111
+ placeholder="Select account"
112
+ />
113
+ </YStack>
114
+ ),
115
+ };
116
+
117
+ export const WithError: Story = {
118
+ render: () => (
119
+ <YStack width={280} padding="$4">
120
+ <TreeSelect
121
+ label="Account"
122
+ nodes={accounts}
123
+ required
124
+ error="An account is required"
125
+ placeholder="Select account"
126
+ />
127
+ </YStack>
128
+ ),
129
+ };
130
+
131
+ export const ReadOnly: Story = {
132
+ render: () => (
133
+ <YStack width={280} padding="$4">
134
+ <TreeSelect
135
+ label="Account"
136
+ nodes={accounts}
137
+ value="hdfc"
138
+ readOnly
139
+ placeholder="Select account"
140
+ />
141
+ </YStack>
142
+ ),
143
+ };
144
+
145
+ export const Skeleton: Story = {
146
+ render: () => (
147
+ <YStack width={280} padding="$4">
148
+ <TreeSelect label="Account" nodes={accounts} skeleton placeholder="Select account" />
149
+ </YStack>
150
+ ),
151
+ };
@@ -272,7 +272,7 @@ describe("Wheel design-law (MPO-60)", () => {
272
272
  it("exposes named exports and no default", async () => {
273
273
  const mod = await import("./index");
274
274
  expect(mod.Wheel).toBe(Wheel);
275
- expect(mod.default).toBeUndefined();
275
+ expect("default" in mod).toBe(false);
276
276
  });
277
277
 
278
278
  it("selected row type stays on body weight 400, never 600 (R7 / design-audit)", () => {
@@ -1,5 +1,5 @@
1
1
  import { renderWithProviders } from "@multiplatform.one/test-utils";
2
- import { describe, expect, it } from "vitest";
2
+ import { describe, expect, it, vi } from "vitest";
3
3
  import { Code, CodeInline } from "./index";
4
4
 
5
5
  describe("Code", () => {
@@ -21,6 +21,15 @@ describe("Code", () => {
21
21
  const codeElement = container.querySelector("code");
22
22
  expect(codeElement).toBeDefined();
23
23
  });
24
+
25
+ it("keeps data-code-font and does not emit a dataset attribute", () => {
26
+ const { container } = renderWithProviders(<Code>const test = "hello"</Code>);
27
+ const codeElement = container.querySelector("code");
28
+ expect(codeElement).toBeTruthy();
29
+ expect(codeElement!.getAttribute("data-code-font")).toBe("mono");
30
+ // React lowercases an unknown dataSet prop to dataset="[object Object]".
31
+ expect(codeElement!.hasAttribute("dataset")).toBe(false);
32
+ });
24
33
  });
25
34
 
26
35
  describe("CodeInline", () => {
@@ -42,4 +51,21 @@ describe("CodeInline", () => {
42
51
  const codeElement = container.querySelector("code");
43
52
  expect(codeElement).toBeDefined();
44
53
  });
54
+
55
+ it("emits data-code-font on web without a dataSet host-prop leak", () => {
56
+ const leak = /does not recognize the `dataSet` prop on a DOM element/;
57
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
58
+ try {
59
+ const { container } = renderWithProviders(<CodeInline>npm install</CodeInline>);
60
+ const codeElement = container.querySelector("code");
61
+ expect(codeElement).toBeDefined();
62
+ expect(codeElement?.getAttribute("data-code-font")).toBe("mono");
63
+ const leaks = errorSpy.mock.calls
64
+ .map((args) => args.map(String).join(" "))
65
+ .filter((message) => leak.test(message));
66
+ expect(leaks).toEqual([]);
67
+ } finally {
68
+ errorSpy.mockRestore();
69
+ }
70
+ });
45
71
  });
@@ -40,7 +40,6 @@ export const Code = CodeFrame.styleable((props, ref) => {
40
40
  fontFamily="$mono"
41
41
  letterSpacing={0}
42
42
  data-code-font="mono"
43
- {...({ dataSet: { codeFont: "mono" } } as object)}
44
43
  />
45
44
  );
46
45
  });
@@ -57,6 +56,9 @@ const CodeInlineFrame = styled(Paragraph, {
57
56
  backgroundColor: componentColors.code.background,
58
57
  cursor: "inherit",
59
58
  borderRadius: "$3",
59
+ // Pin (dial: surrounding prose): 0.9× size / 0.7× leading. Size reaches
60
+ // the chip through context, not sizeToken. Lettered in spec § Code and
61
+ // CodeInline; these ratios are the house metrics already shipping.
60
62
  fontSize: "90%" as CSSProperties["fontSize"],
61
63
  lineHeight: "70%" as CSSProperties["lineHeight"],
62
64
  paddingHorizontal: "0.6%",
@@ -79,7 +81,12 @@ export const CodeInline = CodeInlineFrame.styleable((props, ref) => {
79
81
  fontFamily="$mono"
80
82
  letterSpacing={0}
81
83
  data-code-font="mono"
82
- {...({ dataSet: { codeFont: "mono" } } as object)}
84
+ // react-native-web reads dataSet; React-DOM warns on the camel host
85
+ // prop. Keep the kebab attribute on every target; spread dataSet
86
+ // only when TAMAGUI_TARGET is native.
87
+ {...(process.env.TAMAGUI_TARGET === "native"
88
+ ? ({ dataSet: { codeFont: "mono" } } as object)
89
+ : {})}
83
90
  />
84
91
  );
85
92
  });
@@ -74,7 +74,6 @@ export function HighlightedCode({
74
74
  fontFamily="$mono"
75
75
  letterSpacing={0}
76
76
  data-code-font="mono"
77
- {...({ dataSet: { codeFont: "mono" } } as object)}
78
77
  >
79
78
  {showLineNumbers && (
80
79
  <Text
@@ -115,9 +115,18 @@ describe("HighlightedCode", () => {
115
115
  <HighlightedCode tokens={tokens} />
116
116
  </Preset>,
117
117
  );
118
- const line = container.querySelector("span");
118
+ // querySelector("span") used to land on renderWithProviders' Theme
119
+ // wrapper (`t_light _dsp_contents`), never the code line, so this guard
120
+ // asserted null and had been red on main. Select the line itself.
121
+ const line = container.querySelector("[data-code-font]") as HTMLElement | null;
119
122
  expect(line).toBeTruthy();
120
123
  expect(container.textContent).toContain("const");
121
124
  expect(line!.getAttribute("data-code-font")).toBe("mono");
125
+ // The actual LC-07 pin: bodyFont serif / fontWeight bold must not reach
126
+ // code text, so the line keeps the mono family class.
127
+ expect(line!.className).toContain("font_mono");
128
+ // React does not recognise `dataSet`; leaving it on the line logged an
129
+ // unknown-prop error and wrote dataset="[object Object]" on every node.
130
+ expect(container.querySelectorAll("[dataset]")).toHaveLength(0);
122
131
  });
123
132
  });
@@ -38,6 +38,17 @@ export const knobImmuneExports: Readonly<Record<string, string>> = {
38
38
  // qualifies for a row (house implementations ARE the knob story). The
39
39
  // weight-knob question stays an ADJUDICATION-CANDIDATE on the component
40
40
  // header (MPO-21 G10 / G0-clay R9).
41
+ // MPO-21 G1 — Fold's substrate. Logic shell only; Accordion (and Fold)
42
+ // own the painted chrome. Restored with the three below after MPO-21 R9 G8
43
+ // (the Adapt drop) rewrote this file from a pre-MPO-21 copy and took them
44
+ // out as collateral; Adapt itself stays dropped.
45
+ Collapsible:
46
+ "composition root (open/close shell); knobs land on composing surfaces (Accordion, Fold)",
47
+ Unspaced:
48
+ "spacing opt-out marker; paints nothing, tells the parent stack to skip gap on its children",
49
+ VisuallyHidden: "a11y hide utility; paints nothing visible, no chrome for knobs to reach",
50
+ LinearGradient:
51
+ "paint primitive (color stops are content); no opinionated chrome, consumers own the stops",
41
52
  ListItem:
42
53
  "raw list-row primitive consumed as lego by house list surfaces (List, sidebar, tags) which own the knob story. ADJUDICATION-CANDIDATE + BACKLOG: painted row with own padding/press states — deserves a house shadow or Tamagui* demotion once consumers migrate",
43
54
 
@@ -229,7 +229,7 @@ function SchemeWrap({
229
229
  children: ReactNode;
230
230
  }) {
231
231
  return (
232
- <TamaguiProvider config={houseConfig} defaultTheme={scheme} disableInjectCSS>
232
+ <TamaguiProvider config={houseConfig.tamagui} defaultTheme={scheme} disableInjectCSS>
233
233
  <YStack backgroundColor="$background">{children}</YStack>
234
234
  </TamaguiProvider>
235
235
  );
@@ -1,5 +1,7 @@
1
1
  import { renderWithProviders } from "@multiplatform.one/test-utils";
2
2
  import { fireEvent } from "@testing-library/react";
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
3
5
  import { describe, expect, it, vi } from "vitest";
4
6
  import { Breadcrumbs } from "./breadcrumbs";
5
7
 
@@ -28,6 +30,21 @@ describe("Breadcrumbs", () => {
28
30
  expect(["400", "normal"]).toContain(String(linkWeight));
29
31
  });
30
32
 
33
+ it("hides the slash separators from AT and paints them as T-HELPER ink", () => {
34
+ const { container } = renderWithProviders(<Breadcrumbs items={items} />);
35
+ const separators = [...container.querySelectorAll('[data-testid="breadcrumb-separator"]')];
36
+ expect(separators).toHaveLength(2);
37
+ for (const sep of separators) {
38
+ expect(sep.getAttribute("aria-hidden")).toBe("true");
39
+ expect(sep.textContent).toBe("/");
40
+ }
41
+ // Distinct token from MPO-40: separators ride textAccentColor (AA floor
42
+ // at $color11), not componentColors.text.subtle ($color8).
43
+ const source = readFileSync(join(import.meta.dirname, "breadcrumbs.tsx"), "utf8");
44
+ expect(source).toMatch(/color=\{knobProps\.textAccentColor\}/);
45
+ expect(source).not.toMatch(/color=\{componentColors\.text\.subtle\}/);
46
+ });
47
+
31
48
  it("calls onNavigate from a path crumb and keeps the current page inert", () => {
32
49
  const onNavigate = vi.fn();
33
50
  const { container } = renderWithProviders(
@@ -2,9 +2,15 @@
2
2
  * Breadcrumbs — wayfinding trail (Polaris / Primer / GOV.UK).
3
3
  *
4
4
  * Landmark `<nav>` + ordered list; last crumb is the current page
5
- * (`aria-current="page"`, not a link). Separators stay the RTL-neutral `/`.
5
+ * (`aria-current="page"`, not a link). Separators stay the RTL-neutral `/`,
6
+ * `aria-hidden` (the list already names the trail), and T-HELPER ink via
7
+ * `knobProps.textAccentColor` — not `componentColors.text.subtle` ($color8),
8
+ * which measured 1.89:1 / 2.21:1 against the 4.5 floor. Distinct from
9
+ * MPO-40 / !272 (outlined intent `$color11` on the warning surface).
6
10
  * LC-71: keyboard ring on each link (and the overflow control), never the
7
11
  * trail container. LC-03 / LC-35: tight chromeless trail.
12
+ * Crumb `minHeight` is a Vocabulary `pin` at 44, not a size `floor` — see
13
+ * `docs/design/letters/breadcrumbs-size-pin.md`.
8
14
  */
9
15
 
10
16
  import {
@@ -120,12 +126,17 @@ export function Breadcrumbs({ items, onNavigate, label, maxVisible }: Breadcrumb
120
126
  // Tight trail: crumbs hug the "/" separators so the path reads as one unit.
121
127
  // The space knob still scales the gap, but from a tight base — not the
122
128
  // panel-level knobProps.gap (which is sized for stacked layout sections).
129
+ // Spec: separators are T-HELPER (`textAccentColor`), never the $color8
130
+ // `text.subtle` ramp that fails AA on the page.
123
131
  const spaceGap = knobProps.gap.gap;
124
132
  const trailGap =
125
133
  spaceGap === "$2" ? ("$1" as const) : spaceGap === "$5" ? ("$2" as const) : ("$1.5" as const);
126
134
  // Vertical pad only (theme-propagation SP-FIXED / DG-LAY-04 / LC-03): grow
127
135
  // the clickable box to ≥44 without horizontal padding that would re-spread
128
136
  // the trail. No minWidth floor — the ring must hug the link, not a 44px well.
137
+ // Height is PINNED at MIN_PRESS_TARGET across size stops (font still remaps).
138
+ // That is Vocabulary `pin (forced: WCAG 2.5.5)`, not `floor`. Do not raise
139
+ // the box at large — lettered in docs/design/letters/breadcrumbs-size-pin.md.
129
140
  const crumbPadY = Math.max(0, Math.ceil((MIN_PRESS_TARGET - CRUMB_LINE_PX) / 2));
130
141
  const crumbRadius = knobProps.borderRadius.borderRadius;
131
142
 
@@ -238,7 +249,13 @@ export function Breadcrumbs({ items, onNavigate, label, maxVisible }: Breadcrumb
238
249
  </Text>
239
250
  )}
240
251
  {!isLastSlot && (
241
- <Text color={componentColors.text.subtle} fontSize={separatorSize} userSelect="none">
252
+ <Text
253
+ aria-hidden
254
+ color={knobProps.textAccentColor}
255
+ data-testid="breadcrumb-separator"
256
+ fontSize={separatorSize}
257
+ userSelect="none"
258
+ >
242
259
  /
243
260
  </Text>
244
261
  )}
@@ -4,7 +4,9 @@ import { Tags } from "./tags";
4
4
 
5
5
  describe("Tags design law", () => {
6
6
  it("paints the field label at weight 400 (R7 / §6.1)", () => {
7
- const { container } = renderWithProviders(<Tags tags={["alpha"]} label="Topics" />);
7
+ const { container } = renderWithProviders(
8
+ <Tags tags={[{ id: "alpha", label: "alpha" }]} label="Topics" />,
9
+ );
8
10
  const label = container.querySelector("label") as HTMLElement | null;
9
11
  const node = label ?? (container.querySelector("*") as HTMLElement);
10
12
  expect(container.textContent).toContain("Topics");
package/src/tamagui.ts CHANGED
@@ -30,18 +30,26 @@
30
30
  // quiet = opacity 0.5). Raw stays reachable as TamaguiSeparator below.
31
31
  export {
32
32
  Circle,
33
+ Collapsible,
33
34
  Group,
34
35
  ListItem,
35
36
  ScrollView,
36
37
  Spacer,
37
38
  Square,
39
+ Unspaced,
38
40
  View,
41
+ VisuallyHidden,
39
42
  XGroup,
40
43
  XStack,
41
44
  YGroup,
42
45
  YStack,
43
46
  } from "tamagui";
44
47
 
48
+ // MPO-21 G2: LinearGradient is NOT on the main tamagui barrel — it lives
49
+ // on the `tamagui/linear-gradient` subpath.
50
+ export { LinearGradient } from "tamagui/linear-gradient";
51
+ export type { LinearGradientProps } from "tamagui/linear-gradient";
52
+
45
53
  // Text primitives: NONE re-exported raw anymore (MPO-20). The former
46
54
  // "typography substrate" registry rows were the single biggest knob hole in
47
55
  // the catalog: FontKnobStyles rescues family/leading/tracking on WEB ONLY,
@@ -379,7 +379,7 @@ export function KPICard({ config, ...stackProps }: KPICardProps) {
379
379
  {...stackProps}
380
380
  >
381
381
  <XStack justifyContent="space-between" alignItems="flex-start">
382
- <Text {...knobProps.label} color={componentColors.text.muted} flex={1}>
382
+ <Text {...knobProps.label} color={knobProps.textAccentColor} flex={1}>
383
383
  {config.title}
384
384
  </Text>
385
385
  {config.icon ? (
@@ -408,7 +408,7 @@ export function KPICard({ config, ...stackProps }: KPICardProps) {
408
408
  label={knobProps.label}
409
409
  />
410
410
  {config.previousValue !== undefined && (
411
- <Text {...knobProps.label} color={componentColors.text.muted}>
411
+ <Text {...knobProps.label} color={knobProps.textAccentColor}>
412
412
  {t("vs {{previousValue}}", { previousValue: formatMetric(config.previousValue) })}
413
413
  </Text>
414
414
  )}
@@ -689,7 +689,7 @@ export function Dashboard({
689
689
  </H3>
690
690
  )}
691
691
  {description && (
692
- <Text {...knobProps.body} {...knobProps.label} color={componentColors.text.muted}>
692
+ <Text {...knobProps.body} {...knobProps.label} color={knobProps.textAccentColor}>
693
693
  {description}
694
694
  </Text>
695
695
  )}
@@ -1,6 +1,6 @@
1
1
  import { cleanup, fireEvent, screen } from "@testing-library/react";
2
2
  import { renderWithProviders } from "@multiplatform.one/test-utils";
3
- import { defaultKnobs, resolveKnobs } from "@multiplatform.one/theme";
3
+ import { defaultKnobs, Preset, resolveKnobs } from "@multiplatform.one/theme";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
5
  import {
6
6
  List,
@@ -91,3 +91,44 @@ describe("List keyboard selection chrome (LC-71)", () => {
91
91
  expect(focused!.textContent).toContain("Q3 Vendor Contract");
92
92
  });
93
93
  });
94
+
95
+ describe("ListRow ink — textAccent reaches the TEXT NODE (F11 / data-chrome honour)", () => {
96
+ function titleColor(title: string, accent: "high" | "low" | "medium"): string[] {
97
+ const { getByText } = renderWithProviders(
98
+ <Preset overrides={{ textAccent: accent }}>
99
+ <ListRow title={title} />
100
+ </Preset>,
101
+ );
102
+ return atoms(getByText(title), ["_col-"]);
103
+ }
104
+
105
+ it("changes title colour on a textAccent flip", () => {
106
+ const high = titleColor("ink-high", "high");
107
+ const low = titleColor("ink-low", "low");
108
+ expect(high).not.toHaveLength(0);
109
+ expect(low).not.toHaveLength(0);
110
+ expect(high).not.toEqual(low);
111
+ });
112
+
113
+ it("low and medium share the AA legibility floor on the title", () => {
114
+ const low = titleColor("ink-floor-low", "low");
115
+ const medium = titleColor("ink-floor-medium", "medium");
116
+ expect(low).toEqual(medium);
117
+ });
118
+
119
+ it("moves ListRowMeta default ink with textAccent", () => {
120
+ function metaColor(label: string, accent: "high" | "low"): string[] {
121
+ const { getByText } = renderWithProviders(
122
+ <Preset overrides={{ textAccent: accent }}>
123
+ <ListRowMeta>{label}</ListRowMeta>
124
+ </Preset>,
125
+ );
126
+ return atoms(getByText(label), ["_col-"]);
127
+ }
128
+ const high = metaColor("meta-high", "high");
129
+ const low = metaColor("meta-low", "low");
130
+ expect(high).not.toHaveLength(0);
131
+ expect(low).not.toHaveLength(0);
132
+ expect(high).not.toEqual(low);
133
+ });
134
+ });
@@ -103,7 +103,7 @@ export function ListRowMeta({
103
103
  }) {
104
104
  const { knobProps } = useResolvedKnobs();
105
105
  const { selected, onAccent } = useListItemState();
106
- const resolved = selected && onAccent ? onAccent : (color ?? componentColors.text.muted);
106
+ const resolved = selected && onAccent ? onAccent : (color ?? knobProps.textAccentColor);
107
107
  return (
108
108
  <Text
109
109
  {...knobProps.label}
@@ -121,8 +121,9 @@ export function ListRowMeta({
121
121
  export function ListRow({ icon, title, subtitle, meta }: ListRowProps) {
122
122
  const { knobProps } = useResolvedKnobs();
123
123
  const { selected, onAccent } = useListItemState();
124
- const ink = selected && onAccent ? onAccent : componentColors.text.primary;
125
- const muted = selected && onAccent ? onAccent : componentColors.text.muted;
124
+ // data-chrome family: textAccent = honour (F11). Same channel as house Text —
125
+ // componentColors.text.primary/muted pin $color/$color11 and ignore the knob.
126
+ const ink = selected && onAccent ? onAccent : knobProps.textAccentColor;
126
127
  const iconSize = knobProps.controlIcon.width;
127
128
  const leading = React.isValidElement(icon)
128
129
  ? React.cloneElement(icon as React.ReactElement<{ size?: number; color?: string }>, {
@@ -139,7 +140,7 @@ export function ListRow({ icon, title, subtitle, meta }: ListRowProps) {
139
140
  flexShrink={0}
140
141
  alignItems="center"
141
142
  justifyContent="center"
142
- {...({ "data-list-slot": "icon", color: muted } as Record<string, unknown>)}
143
+ {...({ "data-list-slot": "icon", color: ink } as Record<string, unknown>)}
143
144
  >
144
145
  {leading}
145
146
  </View>
@@ -157,7 +158,7 @@ export function ListRow({ icon, title, subtitle, meta }: ListRowProps) {
157
158
  {subtitle != null && subtitle !== false ? (
158
159
  <RowText
159
160
  {...knobProps.label}
160
- color={muted}
161
+ color={ink}
161
162
  opacity={selected ? 0.85 : 1}
162
163
  numberOfLines={1}
163
164
  userSelect="none"