@multiplatform.one/components 6.4.3 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/dist/cjs/Avatar/index.cjs +4 -0
  2. package/dist/cjs/Avatar/index.native.js +4 -0
  3. package/dist/cjs/Avatar/index.native.js.map +1 -1
  4. package/dist/cjs/Badge/index.cjs +3 -0
  5. package/dist/cjs/Badge/index.native.js +3 -0
  6. package/dist/cjs/Badge/index.native.js.map +1 -1
  7. package/dist/cjs/Carousel/index.cjs +28 -15
  8. package/dist/cjs/Carousel/index.native.js +34 -17
  9. package/dist/cjs/Carousel/index.native.js.map +1 -1
  10. package/dist/cjs/DotIndicator/index.cjs +92 -35
  11. package/dist/cjs/DotIndicator/index.native.js +93 -35
  12. package/dist/cjs/DotIndicator/index.native.js.map +1 -1
  13. package/dist/cjs/Pagination.cjs +112 -167
  14. package/dist/cjs/Pagination.native.js +125 -180
  15. package/dist/cjs/Pagination.native.js.map +1 -1
  16. package/dist/cjs/ProgressSteps/index.cjs +2 -1
  17. package/dist/cjs/ProgressSteps/index.native.js +2 -1
  18. package/dist/cjs/ProgressSteps/index.native.js.map +1 -1
  19. package/dist/cjs/charts/PieChart.cjs +1 -1
  20. package/dist/cjs/charts/PieChart.native.js +7 -2
  21. package/dist/cjs/charts/PieChart.native.js.map +1 -1
  22. package/dist/cjs/feedback/ConfirmDialog.cjs +1 -3
  23. package/dist/cjs/feedback/ConfirmDialog.native.js +1 -3
  24. package/dist/cjs/feedback/ConfirmDialog.native.js.map +1 -1
  25. package/dist/cjs/index.cjs +1 -0
  26. package/dist/cjs/index.native.js +1 -0
  27. package/dist/cjs/index.native.js.map +1 -1
  28. package/dist/cjs/layouts/ViewSwitcher.cjs +4 -5
  29. package/dist/cjs/layouts/ViewSwitcher.native.js +13 -7
  30. package/dist/cjs/layouts/ViewSwitcher.native.js.map +1 -1
  31. package/dist/cjs/layouts/attachments.cjs +4 -8
  32. package/dist/cjs/layouts/attachments.native.js +5 -8
  33. package/dist/cjs/layouts/attachments.native.js.map +1 -1
  34. package/dist/cjs/layouts/page.cjs +1 -0
  35. package/dist/cjs/layouts/page.native.js +4 -1
  36. package/dist/cjs/layouts/page.native.js.map +1 -1
  37. package/dist/cjs/layouts/tags.cjs +1 -0
  38. package/dist/cjs/layouts/tags.native.js +2 -0
  39. package/dist/cjs/layouts/tags.native.js.map +1 -1
  40. package/dist/cjs/layouts/timeline.cjs +0 -6
  41. package/dist/cjs/layouts/timeline.native.js +0 -6
  42. package/dist/cjs/layouts/timeline.native.js.map +1 -1
  43. package/dist/cjs/surfaces.cjs +21 -0
  44. package/dist/cjs/surfaces.native.js +22 -0
  45. package/dist/cjs/surfaces.native.js.map +1 -1
  46. package/dist/cjs/views/Calendar.cjs +2 -0
  47. package/dist/cjs/views/Calendar.native.js +8 -2
  48. package/dist/cjs/views/Calendar.native.js.map +1 -1
  49. package/dist/cjs/views/Kanban.cjs +22 -13
  50. package/dist/cjs/views/Kanban.native.js +1 -0
  51. package/dist/cjs/views/Kanban.native.js.map +1 -1
  52. package/dist/cjs/views/ReportBuilder.cjs +12 -2
  53. package/dist/cjs/views/ReportBuilder.native.js +12 -2
  54. package/dist/cjs/views/ReportBuilder.native.js.map +1 -1
  55. package/dist/esm/Avatar/index.mjs +5 -1
  56. package/dist/esm/Avatar/index.mjs.map +1 -1
  57. package/dist/esm/Avatar/index.native.js +5 -1
  58. package/dist/esm/Avatar/index.native.js.map +1 -1
  59. package/dist/esm/Badge/index.mjs +4 -1
  60. package/dist/esm/Badge/index.mjs.map +1 -1
  61. package/dist/esm/Badge/index.native.js +4 -1
  62. package/dist/esm/Badge/index.native.js.map +1 -1
  63. package/dist/esm/Carousel/index.mjs +28 -15
  64. package/dist/esm/Carousel/index.mjs.map +1 -1
  65. package/dist/esm/Carousel/index.native.js +34 -17
  66. package/dist/esm/Carousel/index.native.js.map +1 -1
  67. package/dist/esm/DotIndicator/index.mjs +93 -36
  68. package/dist/esm/DotIndicator/index.mjs.map +1 -1
  69. package/dist/esm/DotIndicator/index.native.js +94 -36
  70. package/dist/esm/DotIndicator/index.native.js.map +1 -1
  71. package/dist/esm/Pagination.mjs +113 -168
  72. package/dist/esm/Pagination.mjs.map +1 -1
  73. package/dist/esm/Pagination.native.js +126 -181
  74. package/dist/esm/Pagination.native.js.map +1 -1
  75. package/dist/esm/ProgressSteps/index.mjs +2 -1
  76. package/dist/esm/ProgressSteps/index.mjs.map +1 -1
  77. package/dist/esm/ProgressSteps/index.native.js +2 -1
  78. package/dist/esm/ProgressSteps/index.native.js.map +1 -1
  79. package/dist/esm/charts/PieChart.mjs +1 -1
  80. package/dist/esm/charts/PieChart.mjs.map +1 -1
  81. package/dist/esm/charts/PieChart.native.js +7 -2
  82. package/dist/esm/charts/PieChart.native.js.map +1 -1
  83. package/dist/esm/feedback/ConfirmDialog.mjs +2 -4
  84. package/dist/esm/feedback/ConfirmDialog.mjs.map +1 -1
  85. package/dist/esm/feedback/ConfirmDialog.native.js +2 -4
  86. package/dist/esm/feedback/ConfirmDialog.native.js.map +1 -1
  87. package/dist/esm/index.mjs +2 -2
  88. package/dist/esm/index.mjs.map +1 -1
  89. package/dist/esm/index.native.js +2 -2
  90. package/dist/esm/index.native.js.map +1 -1
  91. package/dist/esm/layouts/ViewSwitcher.mjs +4 -5
  92. package/dist/esm/layouts/ViewSwitcher.mjs.map +1 -1
  93. package/dist/esm/layouts/ViewSwitcher.native.js +13 -7
  94. package/dist/esm/layouts/ViewSwitcher.native.js.map +1 -1
  95. package/dist/esm/layouts/attachments.mjs +4 -8
  96. package/dist/esm/layouts/attachments.mjs.map +1 -1
  97. package/dist/esm/layouts/attachments.native.js +5 -8
  98. package/dist/esm/layouts/attachments.native.js.map +1 -1
  99. package/dist/esm/layouts/page.mjs +2 -1
  100. package/dist/esm/layouts/page.mjs.map +1 -1
  101. package/dist/esm/layouts/page.native.js +5 -2
  102. package/dist/esm/layouts/page.native.js.map +1 -1
  103. package/dist/esm/layouts/tags.mjs +1 -0
  104. package/dist/esm/layouts/tags.mjs.map +1 -1
  105. package/dist/esm/layouts/tags.native.js +2 -0
  106. package/dist/esm/layouts/tags.native.js.map +1 -1
  107. package/dist/esm/layouts/timeline.mjs +0 -6
  108. package/dist/esm/layouts/timeline.mjs.map +1 -1
  109. package/dist/esm/layouts/timeline.native.js +0 -6
  110. package/dist/esm/layouts/timeline.native.js.map +1 -1
  111. package/dist/esm/surfaces.mjs +21 -1
  112. package/dist/esm/surfaces.mjs.map +1 -1
  113. package/dist/esm/surfaces.native.js +22 -1
  114. package/dist/esm/surfaces.native.js.map +1 -1
  115. package/dist/esm/views/Calendar.mjs +3 -1
  116. package/dist/esm/views/Calendar.mjs.map +1 -1
  117. package/dist/esm/views/Calendar.native.js +9 -3
  118. package/dist/esm/views/Calendar.native.js.map +1 -1
  119. package/dist/esm/views/Kanban.mjs +23 -14
  120. package/dist/esm/views/Kanban.mjs.map +1 -1
  121. package/dist/esm/views/Kanban.native.js +2 -1
  122. package/dist/esm/views/Kanban.native.js.map +1 -1
  123. package/dist/esm/views/ReportBuilder.mjs +13 -3
  124. package/dist/esm/views/ReportBuilder.mjs.map +1 -1
  125. package/dist/esm/views/ReportBuilder.native.js +13 -3
  126. package/dist/esm/views/ReportBuilder.native.js.map +1 -1
  127. package/dist/jsx/Avatar/index.mjs +5 -1
  128. package/dist/jsx/Avatar/index.mjs.map +1 -1
  129. package/dist/jsx/Avatar/index.native.js +4 -0
  130. package/dist/jsx/Avatar/index.native.js.map +1 -1
  131. package/dist/jsx/Badge/index.mjs +4 -1
  132. package/dist/jsx/Badge/index.mjs.map +1 -1
  133. package/dist/jsx/Badge/index.native.js +3 -0
  134. package/dist/jsx/Badge/index.native.js.map +1 -1
  135. package/dist/jsx/Carousel/index.mjs +28 -15
  136. package/dist/jsx/Carousel/index.mjs.map +1 -1
  137. package/dist/jsx/Carousel/index.native.js +34 -17
  138. package/dist/jsx/Carousel/index.native.js.map +1 -1
  139. package/dist/jsx/DotIndicator/index.mjs +93 -36
  140. package/dist/jsx/DotIndicator/index.mjs.map +1 -1
  141. package/dist/jsx/DotIndicator/index.native.js +93 -35
  142. package/dist/jsx/DotIndicator/index.native.js.map +1 -1
  143. package/dist/jsx/Pagination.mjs +113 -168
  144. package/dist/jsx/Pagination.mjs.map +1 -1
  145. package/dist/jsx/Pagination.native.js +125 -180
  146. package/dist/jsx/Pagination.native.js.map +1 -1
  147. package/dist/jsx/ProgressSteps/index.mjs +2 -1
  148. package/dist/jsx/ProgressSteps/index.mjs.map +1 -1
  149. package/dist/jsx/ProgressSteps/index.native.js +2 -1
  150. package/dist/jsx/ProgressSteps/index.native.js.map +1 -1
  151. package/dist/jsx/charts/PieChart.mjs +1 -1
  152. package/dist/jsx/charts/PieChart.mjs.map +1 -1
  153. package/dist/jsx/charts/PieChart.native.js +7 -2
  154. package/dist/jsx/charts/PieChart.native.js.map +1 -1
  155. package/dist/jsx/feedback/ConfirmDialog.mjs +2 -4
  156. package/dist/jsx/feedback/ConfirmDialog.mjs.map +1 -1
  157. package/dist/jsx/feedback/ConfirmDialog.native.js +1 -3
  158. package/dist/jsx/feedback/ConfirmDialog.native.js.map +1 -1
  159. package/dist/jsx/index.js +2 -2
  160. package/dist/jsx/index.js.map +1 -1
  161. package/dist/jsx/index.mjs +2 -2
  162. package/dist/jsx/index.mjs.map +1 -1
  163. package/dist/jsx/index.native.js +1 -0
  164. package/dist/jsx/index.native.js.map +1 -1
  165. package/dist/jsx/layouts/ViewSwitcher.mjs +4 -5
  166. package/dist/jsx/layouts/ViewSwitcher.mjs.map +1 -1
  167. package/dist/jsx/layouts/ViewSwitcher.native.js +13 -7
  168. package/dist/jsx/layouts/ViewSwitcher.native.js.map +1 -1
  169. package/dist/jsx/layouts/attachments.mjs +4 -8
  170. package/dist/jsx/layouts/attachments.mjs.map +1 -1
  171. package/dist/jsx/layouts/attachments.native.js +5 -8
  172. package/dist/jsx/layouts/attachments.native.js.map +1 -1
  173. package/dist/jsx/layouts/page.mjs +2 -1
  174. package/dist/jsx/layouts/page.mjs.map +1 -1
  175. package/dist/jsx/layouts/page.native.js +4 -1
  176. package/dist/jsx/layouts/page.native.js.map +1 -1
  177. package/dist/jsx/layouts/tags.mjs +1 -0
  178. package/dist/jsx/layouts/tags.mjs.map +1 -1
  179. package/dist/jsx/layouts/tags.native.js +2 -0
  180. package/dist/jsx/layouts/tags.native.js.map +1 -1
  181. package/dist/jsx/layouts/timeline.mjs +0 -6
  182. package/dist/jsx/layouts/timeline.mjs.map +1 -1
  183. package/dist/jsx/layouts/timeline.native.js +0 -6
  184. package/dist/jsx/layouts/timeline.native.js.map +1 -1
  185. package/dist/jsx/surfaces.mjs +21 -1
  186. package/dist/jsx/surfaces.mjs.map +1 -1
  187. package/dist/jsx/surfaces.native.js +22 -0
  188. package/dist/jsx/surfaces.native.js.map +1 -1
  189. package/dist/jsx/views/Calendar.mjs +3 -1
  190. package/dist/jsx/views/Calendar.mjs.map +1 -1
  191. package/dist/jsx/views/Calendar.native.js +8 -2
  192. package/dist/jsx/views/Calendar.native.js.map +1 -1
  193. package/dist/jsx/views/Kanban.mjs +23 -14
  194. package/dist/jsx/views/Kanban.mjs.map +1 -1
  195. package/dist/jsx/views/Kanban.native.js +1 -0
  196. package/dist/jsx/views/Kanban.native.js.map +1 -1
  197. package/dist/jsx/views/ReportBuilder.mjs +13 -3
  198. package/dist/jsx/views/ReportBuilder.mjs.map +1 -1
  199. package/dist/jsx/views/ReportBuilder.native.js +12 -2
  200. package/dist/jsx/views/ReportBuilder.native.js.map +1 -1
  201. package/package.json +9 -9
  202. package/src/Accordion/Accordion.stories.tsx +7 -2
  203. package/src/Avatar/Avatar.spec.tsx +31 -0
  204. package/src/Avatar/index.tsx +19 -1
  205. package/src/Badge/Badge.stories.tsx +3 -1
  206. package/src/Badge/index.tsx +4 -1
  207. package/src/Carousel/Carousel.spec.tsx +271 -0
  208. package/src/Carousel/Carousel.stories.tsx +35 -0
  209. package/src/Carousel/index.tsx +45 -20
  210. package/src/DialogContent.stories.tsx +2 -2
  211. package/src/DotIndicator/dotIndicator.spec.tsx +43 -1
  212. package/src/DotIndicator/index.tsx +169 -35
  213. package/src/Pagination.api.spec.tsx +345 -0
  214. package/src/Pagination.tsx +138 -176
  215. package/src/Panel.stories.tsx +4 -4
  216. package/src/ProgressSteps/ProgressSteps.spec.tsx +40 -1
  217. package/src/ProgressSteps/ProgressSteps.stories.tsx +3 -1
  218. package/src/ProgressSteps/index.tsx +6 -1
  219. package/src/charts/PieChart.tsx +4 -1
  220. package/src/feedback/ConfirmDialog.tsx +2 -2
  221. package/src/index.ts +1 -0
  222. package/src/layouts/PageHeader.stories.tsx +21 -3
  223. package/src/layouts/ScreenToolbar.stories.tsx +12 -5
  224. package/src/layouts/ViewSwitcher.tsx +25 -6
  225. package/src/layouts/attachments.tsx +4 -1
  226. package/src/layouts/layouts.spec.tsx +77 -1
  227. package/src/layouts/page.tsx +5 -1
  228. package/src/layouts/pageTitleScale.spec.tsx +70 -0
  229. package/src/layouts/tags.tsx +2 -0
  230. package/src/layouts/timeline-radius.spec.tsx +21 -0
  231. package/src/layouts/timeline.tsx +7 -18
  232. package/src/shared/t.spec.ts +47 -0
  233. package/src/surfaces.tsx +33 -0
  234. package/src/views/Calendar.stories.tsx +4 -2
  235. package/src/views/Calendar.tsx +6 -1
  236. package/src/views/Gantt.stories.tsx +2 -2
  237. package/src/views/Kanban.native.tsx +8 -2
  238. package/src/views/Kanban.stories.tsx +3 -1
  239. package/src/views/Kanban.tsx +42 -13
  240. package/src/views/ReportBuilder.tsx +15 -2
  241. package/types/Avatar/index.d.ts.map +1 -1
  242. package/types/Badge/index.d.ts.map +1 -1
  243. package/types/Carousel/index.d.ts.map +1 -1
  244. package/types/DotIndicator/index.d.ts +44 -3
  245. package/types/DotIndicator/index.d.ts.map +1 -1
  246. package/types/Pagination.d.ts +6 -1
  247. package/types/Pagination.d.ts.map +1 -1
  248. package/types/ProgressSteps/index.d.ts.map +1 -1
  249. package/types/charts/PieChart.d.ts.map +1 -1
  250. package/types/index.d.ts +1 -1
  251. package/types/index.d.ts.map +1 -1
  252. package/types/layouts/ViewSwitcher.d.ts.map +1 -1
  253. package/types/layouts/attachments.d.ts.map +1 -1
  254. package/types/layouts/page.d.ts.map +1 -1
  255. package/types/layouts/tags.d.ts.map +1 -1
  256. package/types/layouts/timeline.d.ts.map +1 -1
  257. package/types/surfaces.d.ts +17 -0
  258. package/types/surfaces.d.ts.map +1 -1
  259. package/types/views/Calendar.d.ts.map +1 -1
  260. package/types/views/Kanban.d.ts +13 -1
  261. package/types/views/Kanban.d.ts.map +1 -1
  262. package/types/views/ReportBuilder.d.ts.map +1 -1
@@ -79,7 +79,6 @@ const ArrowButton = styled(View, {
79
79
 
80
80
  const ArrowVisual = styled(View, {
81
81
  name: "CarouselArrowVisual",
82
- borderRadius: 1000,
83
82
  backgroundColor: "$background",
84
83
  alignItems: "center",
85
84
  justifyContent: "center",
@@ -112,8 +111,19 @@ export function Carousel({
112
111
  // follows the size knob (spec Carousel arrows: sizeToken, SP-FIXED).
113
112
  // Explicit props eject.
114
113
  const gap = gapProp ?? knobProps.gap.gap;
114
+ // Scroll-offset math needs the gap in px — the slide pitch is
115
+ // slideWidth + gap (LC-62). Space tokens resolve the way DotIndicator's do.
116
+ const gapPx =
117
+ typeof gap === "number"
118
+ ? gap
119
+ : ((getTokens().space as Record<string, { val?: number } | undefined>)?.[gap]?.val ?? 0);
115
120
  const arrowSize = (getTokens().size as any)?.[knobProps.sizeToken]?.val ?? 40;
116
121
  const arrowGlyphSize = Math.round(arrowSize / 2);
122
+ // Arrows are R-BINARY (spec Carousel anatomy): square in the radius `none`
123
+ // world, a full circle at every other value. The fixed 1000 arc was
124
+ // knob-dead — an undeclared knob-immune shape the old geometric sweep
125
+ // silently read as an identity circle (D-09 / LC-60).
126
+ const arrowRadius = knobProps.pointy ? 0 : 1000;
117
127
  // Press-floor target around the painted circle; the edge inset shifts so
118
128
  // the circle still sits 8px from the container edge (clamped ≥0 so the
119
129
  // container's overflow:hidden never clips the enlarged target).
@@ -122,17 +132,22 @@ export function Carousel({
122
132
  const [activeIndex, setActiveIndex] = useState(initialSlide);
123
133
  const trackRef = useRef<TamaguiScrollView | null>(null);
124
134
  const slideCount = Children.count(children);
125
- const slideWidthRef = useRef<number>(0);
135
+ // LC-62 SLIDE-VIEWPORT-WIDTH: slides size from the MEASURED scroll viewport,
136
+ // so the measurement is state (slides re-render on measure/resize). On web a
137
+ // slide's `width="100%"` resolves against the track's CONTENT size, not the
138
+ // clipping viewport — N fixed-width slides inflate to N×W each and slides
139
+ // 2..N land beyond max scroll while the dots animate at nothing.
140
+ const [slideWidth, setSlideWidth] = useState(0);
126
141
  const autoPlayRef = useRef<ReturnType<typeof setInterval> | null>(null);
127
142
 
128
143
  const scrollToSlide = useCallback(
129
144
  (index: number) => {
130
145
  if (!trackRef.current) return;
131
146
  if (index < 0 || index >= slideCount) return;
132
- const offset = index * slideWidthRef.current;
147
+ const offset = index * (slideWidth + gapPx);
133
148
  trackRef.current.scrollTo({ x: offset, animated: true });
134
149
  },
135
- [slideCount],
150
+ [slideCount, slideWidth, gapPx],
136
151
  );
137
152
 
138
153
  const goToSlide = useCallback(
@@ -154,19 +169,18 @@ export function Carousel({
154
169
  const goNext = useCallback(() => goToSlide(activeIndex + 1), [activeIndex, goToSlide]);
155
170
  const goPrev = useCallback(() => goToSlide(activeIndex - 1), [activeIndex, goToSlide]);
156
171
 
157
- // Handle scroll to update active index
172
+ // Handle scroll to update active index (pitch = slideWidth + gap)
158
173
  const handleScroll = useCallback(
159
174
  (event: { nativeEvent: { contentOffset: { x: number } } }) => {
160
175
  const scrollX = event.nativeEvent.contentOffset.x;
161
- const width = slideWidthRef.current;
162
- if (!width) return;
163
- const newIndex = Math.round(scrollX / width);
176
+ if (!slideWidth) return;
177
+ const newIndex = Math.round(scrollX / (slideWidth + gapPx));
164
178
  if (newIndex !== activeIndex && newIndex >= 0 && newIndex < slideCount) {
165
179
  setActiveIndex(newIndex);
166
180
  onSlideChange?.(newIndex);
167
181
  }
168
182
  },
169
- [activeIndex, slideCount, onSlideChange],
183
+ [activeIndex, slideCount, onSlideChange, slideWidth, gapPx],
170
184
  );
171
185
 
172
186
  // Auto-play
@@ -183,12 +197,17 @@ export function Carousel({
183
197
  }
184
198
  }, [autoPlay, goNext]);
185
199
 
186
- // Initial scroll
200
+ // Anchor the active slide whenever geometry changes: the first measure
201
+ // (which also lands `initialSlide` — activeIndex starts there), a resize
202
+ // re-measure, or a gap change. Index offsets are only valid at the pitch
203
+ // they were computed for, so re-derive and snap without animating.
204
+ const activeIndexRef = useRef(activeIndex);
205
+ activeIndexRef.current = activeIndex;
187
206
  useEffect(() => {
188
- if (initialSlide > 0) {
189
- scrollToSlide(initialSlide);
190
- }
191
- }, [initialSlide, scrollToSlide]);
207
+ if (!slideWidth || slideCount === 0 || !trackRef.current) return;
208
+ const index = Math.max(0, Math.min(activeIndexRef.current, slideCount - 1));
209
+ trackRef.current.scrollTo({ x: index * (slideWidth + gapPx), animated: false });
210
+ }, [slideWidth, gapPx, slideCount]);
192
211
 
193
212
  const canGoPrev = loop || activeIndex > 0;
194
213
  const canGoNext = loop || activeIndex < slideCount - 1;
@@ -215,13 +234,19 @@ export function Carousel({
215
234
  style={{ scrollSnapType: "x mandatory" } as any}
216
235
  onScroll={handleScroll}
217
236
  scrollEventThrottle={16}
218
- onLayout={(e) => {
219
- slideWidthRef.current = e.nativeEvent.layout.width;
220
- }}
237
+ onLayout={(e) => setSlideWidth(e.nativeEvent.layout.width)}
238
+ {...({ dataSet: { carouselTrack: "true" } } as Record<string, unknown>)}
221
239
  >
222
240
  <XStack gap={gap}>
223
241
  {Children.map(children, (child, index) => (
224
- <CarouselSlide key={index} width="100%" style={{ scrollSnapAlign: "start" } as any}>
242
+ <CarouselSlide
243
+ key={index}
244
+ // Measured-viewport width (LC-62), never a percentage; slides
245
+ // stay content-sized only for the pre-measure frame.
246
+ width={slideWidth > 0 ? slideWidth : undefined}
247
+ style={{ scrollSnapAlign: "start" } as any}
248
+ {...({ "data-carousel-slide": index } as Record<string, unknown>)}
249
+ >
225
250
  {child}
226
251
  </CarouselSlide>
227
252
  ))}
@@ -253,7 +278,7 @@ export function Carousel({
253
278
  }) as unknown as () => void
254
279
  }
255
280
  >
256
- <ArrowVisual width={arrowSize} height={arrowSize}>
281
+ <ArrowVisual width={arrowSize} height={arrowSize} borderRadius={arrowRadius}>
257
282
  <PrevGlyph size={arrowGlyphSize} />
258
283
  </ArrowVisual>
259
284
  </ArrowButton>
@@ -278,7 +303,7 @@ export function Carousel({
278
303
  }) as unknown as () => void
279
304
  }
280
305
  >
281
- <ArrowVisual width={arrowSize} height={arrowSize}>
306
+ <ArrowVisual width={arrowSize} height={arrowSize} borderRadius={arrowRadius}>
282
307
  <NextGlyph size={arrowGlyphSize} />
283
308
  </ArrowVisual>
284
309
  </ArrowButton>
@@ -3,7 +3,7 @@ import { useState } from "react";
3
3
  // Catalog Button (G-15 story honesty / LC-18, UX-P06).
4
4
  import { Button } from "@multiplatform.one/forms";
5
5
  import { Dialog, Paragraph, Unspaced, YStack } from "tamagui";
6
- import { DialogContent } from "./surfaces";
6
+ import { DialogContent, DialogOverlay } from "./surfaces";
7
7
 
8
8
  function DialogHarness({
9
9
  long = false,
@@ -20,7 +20,7 @@ function DialogHarness({
20
20
  <Button>Open dialog</Button>
21
21
  </Dialog.Trigger>
22
22
  <Dialog.Portal>
23
- <Dialog.Overlay key="overlay" backgroundColor="$shadow6" />
23
+ <DialogOverlay key="overlay" />
24
24
  <DialogContent key="content" maxWidth={480} maxHeight={520}>
25
25
  <Dialog.Title size="$7">Dialog title</Dialog.Title>
26
26
  <Dialog.Description>Knob-aware dialog content surface.</Dialog.Description>
@@ -6,11 +6,35 @@
6
6
  * blocking activation.
7
7
  */
8
8
 
9
- import { fireEvent } from "@testing-library/react";
9
+ import { fireEvent, render } from "@testing-library/react";
10
10
  import { renderWithProviders } from "@multiplatform.one/test-utils";
11
+ import {
12
+ createThemesBuilder,
13
+ defaultAccentTheme,
14
+ defaultBaseTheme,
15
+ defaultBuilderOptions,
16
+ } from "@multiplatform.one/theme";
17
+ import { configWithoutAnimations } from "@tamagui/config";
18
+ import { animationsCSS } from "@tamagui/config/v5-css";
19
+ import { TamaguiProvider, createTamagui } from "tamagui";
11
20
  import { describe, expect, it, vi } from "vitest";
12
21
  import { DotIndicator } from "./index";
13
22
 
23
+ // House builder themes for the LC-05 mark assertion — created at MODULE
24
+ // scope: tamagui registers a config's theme variables globally at
25
+ // createTamagui time, and a config created after the first render (inside a
26
+ // test body) never resolves its tokens (forms Progress.spec pattern).
27
+ const houseThemes = createThemesBuilder(
28
+ defaultBaseTheme,
29
+ defaultAccentTheme,
30
+ defaultBuilderOptions,
31
+ ).themes() as Record<string, Record<string, string>>;
32
+ const houseConfig = createTamagui({
33
+ ...configWithoutAnimations,
34
+ animations: animationsCSS,
35
+ themes: houseThemes as any,
36
+ });
37
+
14
38
  describe("DotIndicator", () => {
15
39
  it("renders one labeled dot per item with aria-current on the active dot", () => {
16
40
  const { container } = renderWithProviders(
@@ -72,4 +96,22 @@ describe("DotIndicator", () => {
72
96
  fireEvent.click(dots[1]);
73
97
  expect(onChange).not.toHaveBeenCalled();
74
98
  });
99
+
100
+ // LC-05 ONE-EMPHASIS / D-01: the active dot is the selection mark, so it
101
+ // fills with the accent channel (indicator.selected = $accentBackground);
102
+ // idle dots stay neutral chrome. Stock test themes carry no
103
+ // $accentBackground, so mount the house builder themes for real resolution.
104
+ it("paints the active dot with the accent mark and idle dots neutral", () => {
105
+ const { container } = render(
106
+ <TamaguiProvider config={houseConfig} defaultTheme="light" disableInjectCSS>
107
+ <DotIndicator total={3} activeIndex={1} />
108
+ </TamaguiProvider>,
109
+ );
110
+ const active = container.querySelector('[data-dot-state="active"]') as HTMLElement;
111
+ const idle = container.querySelector('[data-dot-state="idle"]') as HTMLElement;
112
+ expect(active).toBeTruthy();
113
+ expect(idle).toBeTruthy();
114
+ expect(active.className).toContain("_bg-accentBackg");
115
+ expect(idle.className).not.toContain("_bg-accentBackg");
116
+ });
75
117
  });
@@ -1,4 +1,5 @@
1
- import { MIN_PRESS_TARGET, useResolvedKnobs } from "@multiplatform.one/theme";
1
+ import { MIN_PRESS_TARGET, radiusClassProps, useResolvedKnobs } from "@multiplatform.one/theme";
2
+ import { CheckIcon } from "@phosphor-icons/react";
2
3
  import type React from "react";
3
4
  import { useTranslation } from "react-i18next";
4
5
  import type { GetProps } from "tamagui";
@@ -8,8 +9,33 @@ import { withInterp } from "../shared/t";
8
9
 
9
10
  // ── Types ─────────────────────────────────────────────────────
10
11
 
12
+ /**
13
+ * Which IDIOM the strip paints — never a size of its own (LC-15 / Axiom 5).
14
+ *
15
+ * - `dots` / `pills` — the badge-capable idiom: the mark takes the scale's
16
+ * `dot` metric and may carry a completion check.
17
+ * - `bars` — the progress-TICK idiom: the mark takes the scale's `bar` metric
18
+ * and never carries a completion badge (a tick is not a badge).
19
+ *
20
+ * **The mark's size resolves through exactly one channel**, in this order:
21
+ * 1. the host's pin — `dotSize` / `activeDotSize`;
22
+ * 2. otherwise the size knob's scale (`dotGeometryMap`), read at the metric
23
+ * the idiom selects.
24
+ *
25
+ * That single rule is why `<Pagination variant="bars">` is chunkier than a
26
+ * bare `<DotIndicator variant="bars">`: the pager PINS a larger step, because
27
+ * its strip sits on a rail between 44px controls. Same idiom, one channel,
28
+ * two sizes — `bars` does not name a second geometry.
29
+ */
11
30
  export type DotVariant = "dots" | "bars" | "pills";
12
31
 
32
+ /**
33
+ * What the dots stand for. The two idioms differ only in a11y vocabulary:
34
+ * a carousel marks its current slide (`aria-current="true"`, "Go to item N"),
35
+ * a wizard marks its current step (`aria-current="step"`, "Go to step N").
36
+ */
37
+ export type DotIdiom = "item" | "step";
38
+
13
39
  export interface DotIndicatorProps {
14
40
  /** Total number of dots */
15
41
  total: number;
@@ -23,6 +49,24 @@ export interface DotIndicatorProps {
23
49
  gap?: number | string;
24
50
  /** Disable interaction */
25
51
  disabled?: boolean;
52
+ /** Indices already completed — painted in the completion ramp with a check */
53
+ completedSteps?: Set<number>;
54
+ /** A11y vocabulary: carousel position (default) or wizard progress */
55
+ idiom?: DotIdiom;
56
+ /** Seat the strip on a filled capsule rail (the wizard/pager chrome) */
57
+ rail?: boolean;
58
+ /** Painted dot size; defaults to the size knob's geometry */
59
+ dotSize?: number | string;
60
+ /** Painted width of the active dot; defaults to the size knob's geometry */
61
+ activeDotSize?: number | string;
62
+ /** LC-25: named transition tweening the active dot's geometry and color */
63
+ transition?: string;
64
+ /**
65
+ * Name of the per-dot state attribute (`active` / `idle`). Hosts that
66
+ * publish their own DOM contract pass theirs — Pagination keeps
67
+ * `data-pagination-dot`, which probes and specs already assert against.
68
+ */
69
+ dotStateAttribute?: string;
26
70
  }
27
71
 
28
72
  // ── Knob mappings ─────────────────────────────────────────────
@@ -37,12 +81,34 @@ const dotGapMap: Record<string, string> = {
37
81
 
38
82
  // R-PILL items scale their geometry with the size knob (spec:526-528);
39
83
  // medium keeps the previous 8/6/24 geometry.
84
+ //
85
+ // This table is the ONLY place an unpinned mark's pixels are defined: the
86
+ // idiom picks `dot` or `bar`, the size knob picks the row. A host that needs
87
+ // another size pins one (`dotSize`/`activeDotSize`) rather than declaring a
88
+ // second geometry under a variant name.
40
89
  const dotGeometryMap: Record<string, { dot: number; bar: number; active: number }> = {
41
90
  $3: { dot: 6, bar: 5, active: 18 },
42
91
  $4: { dot: 8, bar: 6, active: 24 },
43
92
  $5: { dot: 10, bar: 8, active: 30 },
44
93
  };
45
94
 
95
+ /**
96
+ * Smallest dot that can legibly hold the completion checkmark. The tick idiom
97
+ * never carries one at ANY size — that is what makes it a tick rather than a
98
+ * badge — so the size floor only guards the badge-capable idioms.
99
+ */
100
+ const CHECK_MIN_DOT = 8;
101
+
102
+ /** Resolve a size token (`$0.75`) or raw px to the number the press-target math needs. */
103
+ function sizePx(value: number | string | undefined, fallback: number): number {
104
+ if (typeof value === "number") return value;
105
+ if (typeof value === "string") {
106
+ const token = (getTokens().size as Record<string, { val?: number } | undefined>)?.[value]?.val;
107
+ if (typeof token === "number") return token;
108
+ }
109
+ return fallback;
110
+ }
111
+
46
112
  // ── Styled components ─────────────────────────────────────────
47
113
 
48
114
  const DotsContainer = styled(XStack, {
@@ -50,6 +116,22 @@ const DotsContainer = styled(XStack, {
50
116
  alignItems: "center",
51
117
  justifyContent: "center",
52
118
  gap: "$2",
119
+ variants: {
120
+ // A rail seats the strip on a filled capsule instead of the page. It is a
121
+ // variant of the strip rather than host chrome because the rail and the
122
+ // idle dot color are ONE decision: idle dots have to step up the ramp to
123
+ // stay visible on the rail's fill, and split across two files that pairing
124
+ // drifts (LC-16).
125
+ rail: {
126
+ true: {
127
+ backgroundColor: componentColors.interactive.background,
128
+ // R-PILL (D-09 / LC-60): the rail is a capsule at every radius value,
129
+ // like the dots it holds.
130
+ ...radiusClassProps("R-PILL", "DotIndicator rail"),
131
+ borderRadius: "$10",
132
+ },
133
+ },
134
+ } as const,
53
135
  });
54
136
 
55
137
  // DG-LAY-04 / D-02 press-floor: the painted dot stays tiny (6–10px) but the
@@ -73,6 +155,13 @@ const DotTarget = styled(View, {
73
155
 
74
156
  const Dot = styled(View, {
75
157
  name: "Dot",
158
+ // R-PILL (D-09 / LC-60): an ornamental dot is a circle at every radius
159
+ // value — the knob moves its size, never its shape — so it declares the
160
+ // class the `radius:none` sweep licenses it under.
161
+ ...radiusClassProps("R-PILL", "DotIndicator dot"),
162
+ // Centers the completion checkmark; inert for the empty dots.
163
+ alignItems: "center",
164
+ justifyContent: "center",
76
165
  borderRadius: 1000,
77
166
  backgroundColor: componentColors.indicator.inactive,
78
167
  hoverStyle: {
@@ -86,31 +175,11 @@ const Dot = styled(View, {
86
175
  backgroundColor: componentColors.indicator.selected,
87
176
  },
88
177
  },
89
- variant: {
90
- dots: {
91
- width: 8,
92
- height: 8,
93
- },
94
- bars: {
95
- width: 6,
96
- height: 6,
97
- },
98
- pills: {
99
- width: 8,
100
- height: 8,
101
- },
102
- },
103
- activeVariant: {
104
- dots: {
105
- width: 24,
106
- },
107
- bars: {
108
- width: 24,
109
- },
110
- pills: {
111
- width: 24,
112
- },
113
- },
178
+ // No `variant` geometry here: the mark's width/height are always passed
179
+ // explicitly from the one size channel below, so a per-variant pixel table
180
+ // on the styled component could only ever be a second, silently-losing
181
+ // definition of the same thing (it already disagreed with the scale at
182
+ // size knob `$3`, where a tick is 5px, not 6).
114
183
  disabled: {
115
184
  true: {
116
185
  opacity: 0.5,
@@ -128,29 +197,73 @@ export function DotIndicator({
128
197
  variant = "dots",
129
198
  gap: gapProp,
130
199
  disabled = false,
200
+ completedSteps,
201
+ idiom = "item",
202
+ rail = false,
203
+ dotSize,
204
+ activeDotSize,
205
+ transition,
206
+ dotStateAttribute = "data-dot-state",
131
207
  }: DotIndicatorProps) {
132
208
  const { t } = useTranslation();
133
209
  const { knobProps } = useResolvedKnobs();
134
210
  // Consumer gap ejects; otherwise the space knob drives the inter-item gap.
135
211
  const gap = gapProp ?? dotGapMap[knobProps.space] ?? "$2";
136
212
  const geometry = dotGeometryMap[knobProps.sizeToken] ?? dotGeometryMap.$4;
137
- const itemSize = variant === "bars" ? geometry.bar : geometry.dot;
213
+ // The tick idiom (`bars`) is ONE flag: it picks the scale's thinner metric
214
+ // AND withholds the completion badge, so the name cannot come to mean one
215
+ // thing for size and another for badging.
216
+ const isTick = variant === "bars";
217
+ // A host may pin the painted geometry (Pagination's token sizes); otherwise
218
+ // the size knob drives it. Pin first, scale second — one channel.
219
+ const itemSize = sizePx(dotSize, isTick ? geometry.bar : geometry.dot);
220
+ const activeSize = sizePx(activeDotSize, geometry.active);
138
221
 
139
222
  // Horizontal slop is capped at just under half the resolved gap so two
140
223
  // enlarged targets keep ≥1px of dead space between them.
141
224
  const gapPx = typeof gap === "number" ? gap : ((getTokens().space as any)?.[gap]?.val ?? 7);
142
225
  const slopX = Math.max(0, Math.floor((gapPx - 1) / 2));
143
- const outsetY = Math.ceil((MIN_PRESS_TARGET - itemSize) / 2);
226
+ const outsetY = Math.max(0, Math.ceil((MIN_PRESS_TARGET - itemSize) / 2));
144
227
 
145
228
  // No onChange means the indicator is read-only (informational), which is
146
229
  // different from disabled (interactive but blocked)
147
230
  const interactive = !disabled && !!onChange;
231
+ const isStep = idiom === "step";
232
+ const label = isStep ? t("Go to step {{number}}") : t("Go to item {{number}}");
233
+ const currentValue = isStep ? "step" : "true";
234
+ // LC-25: discrete jumps tween the indicator; the explicit CSS shorthand is
235
+ // the belt-and-suspenders arm for the web driver, which only picks up
236
+ // Tamagui's named transition on some props.
237
+ const motionStyle = transition
238
+ ? {
239
+ transition:
240
+ "width var(--mp-transition, 100ms) ease, height var(--mp-transition, 100ms) ease, background-color var(--mp-transition, 100ms) ease",
241
+ }
242
+ : undefined;
148
243
 
149
244
  return (
150
- <DotsContainer gap={gap}>
245
+ <DotsContainer
246
+ gap={gap}
247
+ rail={rail}
248
+ {...(rail
249
+ ? { paddingHorizontal: knobProps.panelPadding.padding, height: knobProps.sizeToken }
250
+ : undefined)}
251
+ >
151
252
  {Array.from({ length: total }).map((_, index) => {
152
253
  const isActive = index === activeIndex;
153
- const dotWidth = isActive ? geometry.active : itemSize;
254
+ // Completion is what a step ALREADY did; the current step outranks it.
255
+ const isCompleted = !isActive && !!completedSteps?.has(index);
256
+ const dotWidth = isActive ? activeSize : itemSize;
257
+ // Only the NON-selected fills are computed here. The selected mark
258
+ // stays on the `active` variant so no later prop can paint over the
259
+ // accent — the D-01 failure mode this component was fixed for.
260
+ const restingFill = isCompleted
261
+ ? componentColors.indicator.completed
262
+ : rail
263
+ ? // Idle chrome steps up the ramp on a rail so the dot still
264
+ // separates from the capsule fill it sits on.
265
+ componentColors.indicator.track
266
+ : componentColors.indicator.inactive;
154
267
  return (
155
268
  <DotTarget
156
269
  key={index}
@@ -160,8 +273,10 @@ export function DotIndicator({
160
273
  marginVertical={-outsetY}
161
274
  cursor={interactive ? "pointer" : disabled ? "not-allowed" : "default"}
162
275
  onPress={interactive ? () => onChange?.(index) : undefined}
163
- aria-label={withInterp(t("Go to item {{number}}"), { number: index + 1 })}
164
- {...(isActive ? ({ "aria-current": "true" } as Record<string, unknown>) : undefined)}
276
+ aria-label={withInterp(label, { number: index + 1 })}
277
+ {...(isActive
278
+ ? ({ "aria-current": currentValue } as Record<string, unknown>)
279
+ : undefined)}
165
280
  {...(interactive
166
281
  ? {
167
282
  role: "button" as const,
@@ -178,13 +293,32 @@ export function DotIndicator({
178
293
  : undefined)}
179
294
  >
180
295
  <Dot
181
- variant={variant}
182
296
  active={isActive}
183
- activeVariant={isActive ? variant : undefined}
297
+ {...(isActive
298
+ ? undefined
299
+ : {
300
+ backgroundColor: restingFill,
301
+ // A completed dot and a rail dot both hold their fill on
302
+ // hover: the neutral hover step is PALER than either, so
303
+ // the page-level hover would read as a dim.
304
+ ...(isCompleted || rail
305
+ ? { hoverStyle: { backgroundColor: restingFill } }
306
+ : undefined),
307
+ })}
184
308
  width={dotWidth}
185
309
  height={itemSize}
186
310
  disabled={disabled}
187
- />
311
+ transition={transition}
312
+ style={motionStyle}
313
+ {...({ [dotStateAttribute]: isActive ? "active" : "idle" } as Record<
314
+ string,
315
+ unknown
316
+ >)}
317
+ >
318
+ {isCompleted && !isTick && itemSize >= CHECK_MIN_DOT && (
319
+ <CheckIcon size={8} color="white" weight="bold" />
320
+ )}
321
+ </Dot>
188
322
  </DotTarget>
189
323
  );
190
324
  })}