@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/components",
3
- "version": "7.6.3",
3
+ "version": "7.7.0",
4
4
  "description": "multiplatform.one ui components",
5
5
  "keywords": [
6
6
  "components",
@@ -75,11 +75,11 @@
75
75
  "link": "^2.1.2",
76
76
  "shiki": "^4.0.2",
77
77
  "tamagui": "2.7.6",
78
- "@multiplatform.one/forms": "7.6.3",
79
- "@multiplatform.one/i18n": "7.6.3",
80
- "@multiplatform.one/platform": "7.6.3",
81
- "@multiplatform.one/router": "7.6.3",
82
- "@multiplatform.one/theme": "7.6.3"
78
+ "@multiplatform.one/forms": "7.7.0",
79
+ "@multiplatform.one/i18n": "7.7.0",
80
+ "@multiplatform.one/platform": "7.7.0",
81
+ "@multiplatform.one/router": "7.7.0",
82
+ "@multiplatform.one/theme": "7.7.0"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@tamagui/build": "2.7.6",
@@ -99,9 +99,9 @@
99
99
  "react-native-svg": "15.15.4",
100
100
  "react-native-web": "^0.21.2",
101
101
  "vitest": "^4.1.5",
102
- "@multiplatform.one/config": "7.6.3",
103
- "@multiplatform.one/storybook": "7.6.3",
104
- "@multiplatform.one/test-utils": "7.6.3"
102
+ "@multiplatform.one/config": "7.7.0",
103
+ "@multiplatform.one/storybook": "7.7.0",
104
+ "@multiplatform.one/test-utils": "7.7.0"
105
105
  },
106
106
  "peerDependencies": {
107
107
  "@mdx-js/mdx": "^3.1.1",
@@ -127,14 +127,14 @@ describe("Badge named exports", () => {
127
127
  describe("Badge design-law — T-BODY count text (SB-M-101)", () => {
128
128
  afterEach(cleanup);
129
129
 
130
- it("keeps count text on the 400/700 weight ladder, never 600", () => {
130
+ it("pins count text at the badge-owned 600", () => {
131
131
  const { container } = renderWithProviders(<Badge count={5} />);
132
132
  const weight = atoms(countText(container), ["_fow-"]);
133
133
  expect(weight.length).toBeGreaterThan(0);
134
- expect(weight.join(" ")).not.toMatch(/600|weight-6|fow-6/);
134
+ expect(weight.join(" ")).toMatch(/600|weight-6|fow-6/);
135
135
  });
136
136
 
137
- it("lets the fontWeight knob restyle the count text node", () => {
137
+ it("ignores the fontWeight knob so count text stays at 600 (SB-M-101)", () => {
138
138
  const regular = renderWithProviders(
139
139
  <Preset overrides={{ fontWeight: "regular" }}>
140
140
  <Badge count={5} />
@@ -149,10 +149,9 @@ describe("Badge design-law — T-BODY count text (SB-M-101)", () => {
149
149
  </Preset>,
150
150
  );
151
151
  const boldWeight = atoms(countText(bold.container), ["_fow-"]).join(" ");
152
- expect(regularWeight).not.toMatch(/600|weight-6|fow-6/);
153
- expect(boldWeight).not.toMatch(/600|weight-6|fow-6/);
154
- expect(boldWeight).not.toEqual(regularWeight);
155
- expect(boldWeight).toMatch(/700|weight-7|fow-7/);
152
+ expect(regularWeight).toMatch(/600|weight-6|fow-6/);
153
+ expect(boldWeight).toMatch(/600|weight-6|fow-6/);
154
+ expect(boldWeight).toEqual(regularWeight);
156
155
  });
157
156
 
158
157
  it("lets the bodyFont knob restyle the count text node", () => {
@@ -162,6 +161,7 @@ describe("Badge design-law — T-BODY count text (SB-M-101)", () => {
162
161
  </Preset>,
163
162
  );
164
163
  const sansFamily = atoms(countText(sans.container), ["_ff-"]);
164
+ const sansBodyFont = countText(sans.container).getAttribute("data-body-font");
165
165
  cleanup();
166
166
 
167
167
  const mono = renderWithProviders(
@@ -169,11 +169,10 @@ describe("Badge design-law — T-BODY count text (SB-M-101)", () => {
169
169
  <Badge count={5} />
170
170
  </Preset>,
171
171
  );
172
+ const monoBodyFont = countText(mono.container).getAttribute("data-body-font");
172
173
  expect(sansFamily.length).toBeGreaterThan(0);
173
- expect(countText(sans.container).getAttribute("data-body-font")).not.toBe(
174
- countText(mono.container).getAttribute("data-body-font"),
175
- );
176
- expect(countText(mono.container).getAttribute("data-body-font")).toBe("$mono");
174
+ expect(sansBodyFont).not.toBe(monoBodyFont);
175
+ expect(monoBodyFont).toBe("$mono");
177
176
  });
178
177
  });
179
178
 
@@ -100,9 +100,9 @@ const BadgeIndicator = styled(View, {
100
100
 
101
101
  const BadgeText = styled(Text, {
102
102
  name: "BadgeText",
103
- // T-BODY / design-audit: 400 or 700 only. The fontWeight knob supplies the
104
- // live value via `{...knobProps.body}` never pin 600 (SB-M-101).
105
- fontWeight: "400",
103
+ // SB-M-101: Badge owns weight 600. bodyFont may restyle family; fontWeight
104
+ // is re-pinned after `{...knobProps.body}` so the knob cannot leak.
105
+ fontWeight: "600",
106
106
  textAlign: "center",
107
107
  pointerEvents: "none",
108
108
  });
@@ -206,14 +206,15 @@ export function Badge({
206
206
  ...(overflowed ? { "data-overflow": "true" } : {}),
207
207
  } as Record<string, string>)}
208
208
  >
209
- {/* T-BODY: body family + weight last among type props so the knobs
210
- restyle the count (SB-M-101). Size token still owns fontSize;
211
- the badge palette owns color (textAccent has no target). */}
209
+ {/* T-BODY family from the body knob; weight re-pinned at 600 so the
210
+ fontWeight knob cannot leak over the badge-owned weight (SB-M-101).
211
+ Size token still owns fontSize; the badge palette owns color. */}
212
212
  {!dot && effectiveCount !== undefined && (
213
213
  <BadgeText
214
214
  fontSize={geometry.font}
215
215
  lineHeight={Math.max(1, painted - CONTRAST_RING * 2)}
216
216
  {...knobProps.body}
217
+ fontWeight="600"
217
218
  color={colors.text as ColorTokens}
218
219
  {...({ "data-body-font": knobProps.body.fontFamily } as Record<string, string>)}
219
220
  >
package/src/Card.tsx CHANGED
@@ -39,16 +39,21 @@ function NestedScale({ density, children }: { density: string; children: ReactNo
39
39
  }
40
40
 
41
41
  /**
42
- * Tamagui styled hosts drop JSX `data-*` (Toast gallery hole). RN-web reads
43
- * `dataSet`; jsdom still accepts the kebab attributes. Write both.
42
+ * Tamagui styled hosts drop JSX `data-*` on native (Toast gallery hole), so
43
+ * native needs the RN `dataSet` map. On web, spreading `dataSet` onto a DOM
44
+ * host is a React invalid-prop warning (`dataSet` → `dataset`) and is what
45
+ * the knob-fuzz harness recorded as a probe/console failure on every Card
46
+ * consumer. jsdom and Storybook both keep kebab `data-*`, so web writes
47
+ * those only.
44
48
  */
45
49
  function hostData(attrs: Record<string, string>): Record<string, unknown> {
50
+ if (process.env.TAMAGUI_TARGET !== "native") return attrs;
46
51
  const dataSet: Record<string, string> = {};
47
52
  for (const [key, value] of Object.entries(attrs)) {
48
53
  const camel = key.replace(/^data-/, "").replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
49
54
  dataSet[camel] = value;
50
55
  }
51
- return { ...attrs, dataSet };
56
+ return { dataSet };
52
57
  }
53
58
 
54
59
  function CardRoot({ children, tier = "content", ...props }: CardProps) {
@@ -28,7 +28,7 @@ describe("CardFooter named exports", () => {
28
28
  expect(Card.Footer).toBe(CardFooter);
29
29
  const mod = await import("./CardFooter");
30
30
  expect(mod.CardFooter).toBe(CardFooter);
31
- expect(mod.default).toBeUndefined();
31
+ expect("default" in mod).toBe(false);
32
32
  });
33
33
  });
34
34
 
@@ -14,16 +14,19 @@ import { wrapBareTextChildren } from "./shared/textRidesText";
14
14
  export type CardFooterProps = ViewProps & { children?: ReactNode };
15
15
 
16
16
  /**
17
- * Tamagui styled hosts drop JSX `data-*` (Toast gallery hole). RN-web reads
18
- * `dataSet`; jsdom still accepts the kebab attributes. Write both.
17
+ * Tamagui styled hosts drop JSX `data-*` on native (Toast gallery hole), so
18
+ * native needs the RN `dataSet` map. On web, spreading `dataSet` onto a DOM
19
+ * host is a React invalid-prop warning (`dataSet` → `dataset`). jsdom and
20
+ * Storybook keep kebab `data-*`, so web writes those only.
19
21
  */
20
22
  function hostData(attrs: Record<string, string>): Record<string, unknown> {
23
+ if (process.env.TAMAGUI_TARGET !== "native") return attrs;
21
24
  const dataSet: Record<string, string> = {};
22
25
  for (const [key, value] of Object.entries(attrs)) {
23
26
  const camel = key.replace(/^data-/, "").replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
24
27
  dataSet[camel] = value;
25
28
  }
26
- return { ...attrs, dataSet };
29
+ return { dataSet };
27
30
  }
28
31
 
29
32
  export function CardFooter({ children, ...props }: CardFooterProps) {
@@ -28,7 +28,7 @@ describe("CardHeader named exports", () => {
28
28
  expect(Card.Header).toBe(CardHeader);
29
29
  const mod = await import("./CardHeader");
30
30
  expect(mod.CardHeader).toBe(CardHeader);
31
- expect(mod.default).toBeUndefined();
31
+ expect("default" in mod).toBe(false);
32
32
  });
33
33
  });
34
34
 
@@ -14,16 +14,19 @@ import { wrapBareTextChildren } from "./shared/textRidesText";
14
14
  export type CardHeaderProps = ViewProps & { children?: ReactNode };
15
15
 
16
16
  /**
17
- * Tamagui styled hosts drop JSX `data-*` (Toast gallery hole). RN-web reads
18
- * `dataSet`; jsdom still accepts the kebab attributes. Write both.
17
+ * Tamagui styled hosts drop JSX `data-*` on native (Toast gallery hole), so
18
+ * native needs the RN `dataSet` map. On web, spreading `dataSet` onto a DOM
19
+ * host is a React invalid-prop warning (`dataSet` → `dataset`). jsdom and
20
+ * Storybook keep kebab `data-*`, so web writes those only.
19
21
  */
20
22
  function hostData(attrs: Record<string, string>): Record<string, unknown> {
23
+ if (process.env.TAMAGUI_TARGET !== "native") return attrs;
21
24
  const dataSet: Record<string, string> = {};
22
25
  for (const [key, value] of Object.entries(attrs)) {
23
26
  const camel = key.replace(/^data-/, "").replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
24
27
  dataSet[camel] = value;
25
28
  }
26
- return { ...attrs, dataSet };
29
+ return { dataSet };
27
30
  }
28
31
 
29
32
  export function CardHeader({ children, ...props }: CardHeaderProps) {
@@ -150,7 +150,7 @@ describe("Chip", () => {
150
150
  expect(mod.chipTone).toBe(chipTone);
151
151
  expect(typeof mod.chipIconSize).toBe("function");
152
152
  expect(chipIconSize("$3")).toBeGreaterThan(0);
153
- expect(mod.default).toBeUndefined();
153
+ expect("default" in mod).toBe(false);
154
154
  });
155
155
 
156
156
  it("label TEXT NODE weight is 400, never 500/600", () => {
@@ -163,7 +163,7 @@ describe("Pagination design-law (MPO-60)", () => {
163
163
  const mod = await import("./Pagination");
164
164
  expect(mod.Pagination).toBe(Pagination);
165
165
  expect(mod.getPaginationItems).toBeTypeOf("function");
166
- expect(mod.default).toBeUndefined();
166
+ expect("default" in mod).toBe(false);
167
167
  });
168
168
 
169
169
  it("LC-68: animation none publishes data-animation=none (no quick fallback)", () => {
@@ -52,6 +52,18 @@ describe("ProgressSteps", () => {
52
52
  expect(current.className).toContain("_bg-accentBackg");
53
53
  });
54
54
 
55
+ it("walks the current fill off the dark mid-solid so the numeral can clear 4.5", () => {
56
+ const { container } = render(
57
+ <TamaguiProvider config={houseConfig} defaultTheme="dark" disableInjectCSS>
58
+ <ProgressSteps steps={steps} current={1} />
59
+ </TamaguiProvider>,
60
+ );
61
+ const current = container.querySelector('[aria-current="step"]') as HTMLElement;
62
+ expect(current).toBeTruthy();
63
+ expect(current.className).toMatch(/_bg-accent/);
64
+ expect(current.className).not.toContain("_bg-accentBackg");
65
+ });
66
+
55
67
  it("calls onStepChange when jumping back to a completed step", () => {
56
68
  const onStepChange = vi.fn();
57
69
  const { container } = renderWithProviders(
@@ -110,10 +122,25 @@ describe("ProgressSteps", () => {
110
122
  expect(target.getAttribute("data-press-min")).toBe("44");
111
123
  });
112
124
 
113
- it("steps nested marker size down under compact density", () => {
125
+ it("declares the marker R-PILL and tracks the borderWidth knob for the ring", () => {
126
+ const { container } = renderWithProviders(<ProgressSteps steps={steps} current={1} />);
127
+ const marker = container.querySelector("[data-nested-px]") as HTMLElement;
128
+ expect(marker.getAttribute("data-radius-class")).toBe("R-PILL");
129
+ expect(marker.getAttribute("data-radius-part")).toBe("ProgressSteps marker");
130
+ // Default knobs: borderWidth medium = 1. The previous 2px pin is gone.
131
+ const upcoming = container.querySelector('[data-step-state="upcoming"]') as HTMLElement;
132
+ expect(upcoming.getAttribute("data-marker-ring")).toBe("1");
133
+ const current = container.querySelector('[data-step-state="current"]') as HTMLElement;
134
+ expect(current.getAttribute("data-marker-ring")).toBe("1");
135
+ });
136
+
137
+ it("keeps nested marker size on the size channel under compact (LC-76)", () => {
114
138
  const { container } = renderWithProviders(<ProgressSteps steps={steps} current={0} compact />);
139
+ const root = container.querySelector('[role="list"]') as HTMLElement;
140
+ expect(root.getAttribute("data-density")).toBe("compact");
115
141
  const marker = container.querySelector("[data-nested-px]") as HTMLElement;
116
- expect(marker.getAttribute("data-nested-px")).toBe("28");
142
+ // Compact steps space only — nestedControl follows size (medium = 32).
143
+ expect(marker.getAttribute("data-nested-px")).toBe("32");
117
144
  });
118
145
 
119
146
  it("hides visible step labels when labels is hidden", () => {
@@ -3,6 +3,8 @@ import {
3
3
  MIN_PRESS_TARGET,
4
4
  ensureKeyboardModalityTracking,
5
5
  pressTargetHitSlop,
6
+ radiusClassProps,
7
+ useAaSolidFill,
6
8
  useReadableTextOn,
7
9
  useResolvedKnobs,
8
10
  wasKeyboardFocus,
@@ -76,8 +78,11 @@ const Marker = styled(View, {
76
78
  name: "ProgressStepMarker",
77
79
  alignItems: "center",
78
80
  justifyContent: "center",
79
- borderWidth: 2,
80
81
  flexShrink: 0,
82
+ // R-PILL (D-09 / LC-60): the numbered disc is a circle at every radius
83
+ // stop — the knob moves size, never shape. Not K1 (Progress/Meter rails).
84
+ ...radiusClassProps("R-PILL", "ProgressSteps marker"),
85
+ borderRadius: 1000,
81
86
  });
82
87
 
83
88
  const StepButton = styled(View, {
@@ -148,14 +153,18 @@ export function ProgressSteps({
148
153
  );
149
154
  if (isWeb) ensureKeyboardModalityTracking();
150
155
  const [kbFocusIndex, setKbFocusIndex] = useState<number | null>(null);
151
- // LC-05: current marker rides the accent fill; completed/error fills pick
152
- // paper/ink from luminance rather than an assumed white glyph.
153
- const onSelected = useReadableTextOn(componentColors.indicator.selected);
156
+ // LC-05: current marker rides an accent solid; completed/error fills pick
157
+ // paper/ink from luminance rather than an assumed white glyph. The stock
158
+ // dark `$accentBackground` tops out at 4.02:1 against either scheme
159
+ // anchor, under the in-repo 4.5 text floor, so the current fill walks to
160
+ // the nearest accent solid that can carry the numeral.
161
+ const currentFill = useAaSolidFill(componentColors.indicator.selected);
162
+ const onSelected = useReadableTextOn(currentFill);
154
163
  const onCompleted = useReadableTextOn(componentColors.indicator.completed);
155
164
  const onError = useReadableTextOn("$red9");
156
165
  const { px: markerPx, hitSlop, box: nestedBox } = splitNestedControl(knobProps.nestedControl);
157
166
  const markerIconSize = Math.max(12, Math.round(markerPx * 0.45));
158
- const markerRadius = knobProps.pointy ? 0 : 1000;
167
+ const ringWidth = knobProps.borderRadius.borderWidth ?? 0;
159
168
  const connectorThickness = 2;
160
169
  const pressOutset = Math.max(0, Math.ceil((MIN_PRESS_TARGET - markerPx) / 2));
161
170
  const interactive = !!onStepChange && allowJump;
@@ -192,7 +201,7 @@ export function ProgressSteps({
192
201
  : kind === "completed"
193
202
  ? componentColors.indicator.completed
194
203
  : kind === "current"
195
- ? componentColors.indicator.selected
204
+ ? currentFill
196
205
  : "transparent";
197
206
  const markerBorder = kind === "upcoming" ? componentColors.indicator.track : markerBg;
198
207
  const markerFg =
@@ -207,15 +216,20 @@ export function ProgressSteps({
207
216
  step.description ?? (step.optional && kind !== "error" ? t("Optional") : null);
208
217
  const connectorFill =
209
218
  index < current ? componentColors.indicator.completed : componentColors.indicator.track;
219
+ // Upcoming is outlined-by-state: chromeless `borderWidth:none` would
220
+ // erase the empty disc, so it keeps the OUTLINED-HAIRLINE floor.
221
+ const markerRing = kind === "upcoming" ? Math.max(ringWidth, 0.5) : ringWidth;
210
222
 
211
223
  const marker = (
212
224
  <Marker
213
225
  {...nestedBox}
214
- borderRadius={markerRadius}
226
+ borderRadius={1000}
227
+ borderWidth={markerRing}
215
228
  backgroundColor={markerBg}
216
229
  borderColor={markerBorder}
217
230
  data-nested-px={markerPx}
218
231
  data-step-state={kind}
232
+ data-marker-ring={markerRing}
219
233
  transition={knobProps.transition}
220
234
  {...(state === "current"
221
235
  ? ({ "aria-current": "step" } as Record<string, unknown>)
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mediumDetent, minSheetHeight, resolveSheetDetents } from "./detents";
3
+
4
+ /*
5
+ * MPO-68 AC-4 — "SheetModal's snapPoint prop either drives a detent or is
6
+ * removed". It drives one. These lock that in, because it has already been
7
+ * lost once: 184746910 (MPO-71) rewrote the detents block from a pre-MPO-68
8
+ * copy and dropped the snapPoint push, leaving the prop declared, documented
9
+ * and passed by four callers while doing nothing on device.
10
+ *
11
+ * Geometry under test is a pure height list, largest first. The component's
12
+ * PanResponder turns each height h into a resting translate of `travel - h`.
13
+ */
14
+
15
+ const travel = 600;
16
+
17
+ describe("resolveSheetDetents", () => {
18
+ it("a plain sheet has exactly one rest: fully expanded", () => {
19
+ expect(resolveSheetDetents({ travel })).toEqual([600]);
20
+ });
21
+
22
+ it("REGRESSION: snapPoint creates a detent instead of being ignored", () => {
23
+ // The bug this spec exists for: main returned [600] here.
24
+ expect(resolveSheetDetents({ travel, snapPoint: 85 })).toEqual([600, 510]);
25
+ });
26
+
27
+ it("reads snapPoint as a percent of the full height, matching the web Sheet", () => {
28
+ expect(resolveSheetDetents({ travel, snapPoint: 50 })).toEqual([600, 300]);
29
+ expect(resolveSheetDetents({ travel, snapPoint: 60 })).toEqual([600, 360]);
30
+ expect(resolveSheetDetents({ travel, snapPoint: 92 })).toEqual([600, 552]);
31
+ });
32
+
33
+ it("honours the four heights real callers pass", () => {
34
+ // CommandPalette 92, list-tools-sheet 85, ListThemePlayground 85,
35
+ // Toast.stories 70 — every one of these was a no-op on device.
36
+ for (const snapPoint of [92, 85, 70]) {
37
+ const detents = resolveSheetDetents({ travel, snapPoint });
38
+ expect(detents).toContain(Math.round(travel * (snapPoint / 100)));
39
+ expect(detents.length).toBeGreaterThan(1);
40
+ }
41
+ });
42
+
43
+ it("a scrollable sheet keeps its half-open detent", () => {
44
+ expect(resolveSheetDetents({ travel, scrollable: true })).toEqual([
45
+ 600,
46
+ Math.round(travel * mediumDetent),
47
+ ]);
48
+ });
49
+
50
+ it("scrollable and snapPoint stack, largest first, no duplicates", () => {
51
+ // 600 expanded, 330 medium, 510 from snapPoint.
52
+ expect(resolveSheetDetents({ travel, scrollable: true, snapPoint: 85 })).toEqual([
53
+ 600, 510, 330,
54
+ ]);
55
+ });
56
+
57
+ it("dedupes a snapPoint that lands on a detent already present", () => {
58
+ expect(resolveSheetDetents({ travel, snapPoint: 100 })).toEqual([600]);
59
+ expect(resolveSheetDetents({ travel, scrollable: true, snapPoint: 55 })).toEqual([600, 330]);
60
+ });
61
+
62
+ it("clamps a snapPoint above 100 to fully expanded", () => {
63
+ expect(resolveSheetDetents({ travel, snapPoint: 140 })).toEqual([600]);
64
+ });
65
+
66
+ it("drops a detent shorter than the floor rather than resting near-dismissed", () => {
67
+ // 600 * 0.10 = 60, well under minSheetHeight.
68
+ expect(resolveSheetDetents({ travel, snapPoint: 10 })).toEqual([600]);
69
+ expect(minSheetHeight).toBe(180);
70
+ });
71
+
72
+ it("a sheet shorter than the floor still keeps its own full height", () => {
73
+ expect(resolveSheetDetents({ travel: 120, scrollable: true, snapPoint: 85 })).toEqual([120]);
74
+ });
75
+
76
+ it("ignores a snapPoint that is not a usable number", () => {
77
+ for (const snapPoint of [0, -20, Number.NaN, Number.POSITIVE_INFINITY]) {
78
+ expect(resolveSheetDetents({ travel, snapPoint })).toEqual([600]);
79
+ }
80
+ expect(resolveSheetDetents({ travel, snapPoint: undefined })).toEqual([600]);
81
+ });
82
+
83
+ it("always returns descending heights, so the pan release can scan in order", () => {
84
+ const detents = resolveSheetDetents({ travel, scrollable: true, snapPoint: 40 });
85
+ expect(detents).toEqual([...detents].sort((a, b) => b - a));
86
+ expect(new Set(detents).size).toBe(detents.length);
87
+ });
88
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * SheetModal detent geometry — the pure half, split out so it can be specced
3
+ * without standing up a React Native renderer.
4
+ *
5
+ * A detent is a VISIBLE HEIGHT in points. The native sheet's `translateY`
6
+ * measures DOWN from fully-expanded, so a detent of `h` rests at `travel - h`,
7
+ * and `travel` itself is the fully-expanded sheet.
8
+ */
9
+
10
+ /** Smallest height a native sheet is ever laid out at. */
11
+ export const minSheetHeight = 180;
12
+
13
+ /**
14
+ * Half-open detent for a sheet that owns a scrollport, as a fraction of its
15
+ * full height. Mirrors FloatingPanel so both native sheets drag alike.
16
+ */
17
+ export const mediumDetent = 0.55;
18
+
19
+ export interface ResolveSheetDetentsOptions {
20
+ /** Full (expanded) height of the sheet, in points. */
21
+ travel: number;
22
+ /** The sheet owns a scrollport, so a half-open rest is meaningful. */
23
+ scrollable?: boolean;
24
+ /** Percent detent from the caller — the same prop the web Sheet takes. */
25
+ snapPoint?: number;
26
+ }
27
+
28
+ /**
29
+ * The heights the sheet is allowed to rest at, largest first.
30
+ *
31
+ * MPO-68: `snapPoint` is a percent on the web Sheet (`snapPoints={[snapPoint]}`).
32
+ * Native honours the same prop by turning that percent into a detent, so a
33
+ * caller that writes `snapPoint={85}` gets an 85%-height rest on both twins
34
+ * rather than the prop being silently dropped on device.
35
+ *
36
+ * Anything shorter than `minSheetHeight` is discarded, because a sheet that
37
+ * rests below that is indistinguishable from a dismissed one. A sheet whose
38
+ * full height is already under the floor keeps its own height, otherwise it
39
+ * would have no detents at all.
40
+ */
41
+ export function resolveSheetDetents({
42
+ travel,
43
+ scrollable,
44
+ snapPoint,
45
+ }: ResolveSheetDetentsOptions): number[] {
46
+ const points = [travel];
47
+ if (scrollable) {
48
+ points.push(Math.round(travel * mediumDetent));
49
+ }
50
+ if (typeof snapPoint === "number" && Number.isFinite(snapPoint) && snapPoint > 0) {
51
+ points.push(Math.round(travel * (Math.min(snapPoint, 100) / 100)));
52
+ }
53
+ const floor = Math.min(minSheetHeight, travel);
54
+ return Array.from(new Set(points.filter((h) => h >= floor))).sort((a, b) => b - a);
55
+ }
@@ -24,6 +24,7 @@ import {
24
24
  useWindowDimensions,
25
25
  } from "react-native";
26
26
  import { useResolvedKnobs } from "@multiplatform.one/theme";
27
+ import { minSheetHeight, resolveSheetDetents } from "./detents";
27
28
  import { useTranslation } from "../shared/i18n";
28
29
  import { useKeyboardInset } from "../shared/useKeyboardInset";
29
30
  import { View, YStack } from "tamagui";
@@ -36,10 +37,6 @@ const sheetMaxHeightRatio = 0.85;
36
37
  const bottomInset = Platform.OS === "ios" ? 24 : 16;
37
38
  // Backdrop left visible above the sheet, so the scrim stays tappable.
38
39
  const backdropStrip = 48;
39
- const minSheetHeight = 180;
40
- // Half-open detent for a sheet that owns a scrollport, as a fraction of its
41
- // full height. Mirrors FloatingPanel so both native sheets drag alike.
42
- const mediumDetent = 0.55;
43
40
  // Released more than this far below the smallest detent, the sheet dismisses.
44
41
  const dismissThreshold = 56;
45
42
  const IN_DIALOG_REGION = { inDialogRegion: true } as const;
@@ -49,7 +46,7 @@ export function SheetModal({
49
46
  onOpenChange,
50
47
  children,
51
48
  header,
52
- snapPoint: _snapPoint,
49
+ snapPoint,
53
50
  scrollable,
54
51
  }: SheetModalProps) {
55
52
  const { t } = useTranslation();
@@ -95,16 +92,20 @@ export function SheetModal({
95
92
  *
96
93
  * translateY measures DOWN from fully-expanded: 0 is the sheet at full
97
94
  * height, `travel` is fully off-screen.
95
+ *
96
+ * The detent heights themselves live in ./detents.ts, pure, so the geometry
97
+ * is specced without a native renderer. 184746910 (MPO-71) rewrote this
98
+ * block from a pre-MPO-68 copy and silently dropped the `snapPoint` push,
99
+ * leaving the prop declared, documented and passed by four callers while
100
+ * doing nothing on device. Keep the math in one place so a rewrite of the
101
+ * animation cannot take the geometry with it again.
98
102
  */
99
103
  const [measuredHeight, setMeasuredHeight] = useState(0);
100
104
  const travel = measuredHeight || maxHeight;
101
- const detents = useMemo(() => {
102
- if (!scrollable) return [travel];
103
- const points = [travel, Math.round(travel * mediumDetent)].filter(
104
- (h) => h >= Math.min(minSheetHeight, travel),
105
- );
106
- return Array.from(new Set(points)).sort((a, b) => b - a);
107
- }, [scrollable, travel]);
105
+ const detents = useMemo(
106
+ () => resolveSheetDetents({ travel, scrollable, snapPoint }),
107
+ [travel, scrollable, snapPoint],
108
+ );
108
109
 
109
110
  /*
110
111
  * MPO-71 — park the sheet OFF-SCREEN before the modal ever presents.
@@ -21,7 +21,7 @@ export interface SheetModalProps {
21
21
  children?: ReactNode;
22
22
  /** Fixed content above the (scrollable) body, e.g. the sheet title. */
23
23
  header?: ReactNode;
24
- /** Percent snap point for the web Sheet (native sizes to content). */
24
+ /** Percent snap point. Web: the Sheet's snap point. Native: a drag detent. */
25
25
  snapPoint?: number;
26
26
  /** Wrap children in the sheet's scroll container. */
27
27
  scrollable?: boolean;
@@ -59,7 +59,10 @@ function useSheetFrameFocus(open: boolean, frameRef: RefObject<HTMLElement | nul
59
59
  if (!target || !target.isConnected) return;
60
60
  const ae = document.activeElement;
61
61
  const dropped =
62
- !ae || ae === document.body || ae === document.documentElement || !!frameRef.current?.contains(ae);
62
+ !ae ||
63
+ ae === document.body ||
64
+ ae === document.documentElement ||
65
+ !!frameRef.current?.contains(ae);
63
66
  if (dropped) {
64
67
  target.focus({
65
68
  preventScroll: true,
@@ -22,7 +22,7 @@ function radiusClasses(el: Element): string[] {
22
22
 
23
23
  describe("Skeleton named exports", () => {
24
24
  it("serves named exports only (no default) and keeps the namespace statics", () => {
25
- expect(SkeletonExports.default).toBeUndefined();
25
+ expect("default" in SkeletonExports).toBe(false);
26
26
  expect(SkeletonExports.Skeleton).toBe(Skeleton);
27
27
  expect(SkeletonExports.SkeletonText).toBe(SkeletonText);
28
28
  expect(SkeletonExports.SkeletonCircle).toBe(SkeletonCircle);