@multiplatform.one/forms 7.67.0 → 7.69.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 (271) hide show
  1. package/dist/cjs/AdaptivePopup/index.cjs +45 -5
  2. package/dist/cjs/AdaptivePopup/index.native.js +47 -5
  3. package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
  4. package/dist/cjs/Button/IconButtonGroup.cjs +95 -0
  5. package/dist/cjs/Button/IconButtonGroup.native.js +101 -0
  6. package/dist/cjs/Button/IconButtonGroup.native.js.map +1 -0
  7. package/dist/cjs/Button/glass.cjs +34 -0
  8. package/dist/cjs/Button/glass.native.js +55 -0
  9. package/dist/cjs/Button/glass.native.js.map +1 -0
  10. package/dist/cjs/Button/iconButtonGroupContext.cjs +33 -0
  11. package/dist/cjs/Button/iconButtonGroupContext.native.js +36 -0
  12. package/dist/cjs/Button/iconButtonGroupContext.native.js.map +1 -0
  13. package/dist/cjs/Button/index.cjs +174 -30
  14. package/dist/cjs/Button/index.native.js +174 -29
  15. package/dist/cjs/Button/index.native.js.map +1 -1
  16. package/dist/cjs/Button/mediaPlate.cjs +31 -0
  17. package/dist/cjs/Button/mediaPlate.native.js +34 -0
  18. package/dist/cjs/Button/mediaPlate.native.js.map +1 -0
  19. package/dist/cjs/ContextualSaveBar/index.cjs +6 -8
  20. package/dist/cjs/ContextualSaveBar/index.native.js +6 -8
  21. package/dist/cjs/ContextualSaveBar/index.native.js.map +1 -1
  22. package/dist/cjs/DescriptionList/index.cjs +0 -1
  23. package/dist/cjs/DescriptionList/index.native.js +0 -1
  24. package/dist/cjs/DescriptionList/index.native.js.map +1 -1
  25. package/dist/cjs/FloatingPanel/index.native.js +21 -16
  26. package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
  27. package/dist/cjs/fields/Barcode/BarcodeScanner.cjs +7 -8
  28. package/dist/cjs/fields/Barcode/BarcodeScanner.native.js +7 -8
  29. package/dist/cjs/fields/Barcode/BarcodeScanner.native.js.map +1 -1
  30. package/dist/cjs/fields/FileUpload/index.cjs +86 -60
  31. package/dist/cjs/fields/FileUpload/index.native.js +99 -73
  32. package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
  33. package/dist/cjs/fields/Geolocation/index.cjs +7 -8
  34. package/dist/cjs/fields/Geolocation/index.native.js +7 -8
  35. package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
  36. package/dist/cjs/fields/ImageField/index.cjs +4 -6
  37. package/dist/cjs/fields/ImageField/index.native.js +4 -6
  38. package/dist/cjs/fields/ImageField/index.native.js.map +1 -1
  39. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.cjs +20 -28
  40. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js +20 -28
  41. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  42. package/dist/cjs/index.cjs +11 -1
  43. package/dist/cjs/index.native.js +10 -0
  44. package/dist/cjs/index.native.js.map +1 -1
  45. package/dist/cjs/shared/colorRamps.cjs +0 -8
  46. package/dist/cjs/shared/colorRamps.native.js +0 -8
  47. package/dist/cjs/shared/colorRamps.native.js.map +1 -1
  48. package/dist/cjs/standardActions.cjs +117 -0
  49. package/dist/cjs/standardActions.native.js +120 -0
  50. package/dist/cjs/standardActions.native.js.map +1 -0
  51. package/dist/esm/AdaptivePopup/index.mjs +45 -5
  52. package/dist/esm/AdaptivePopup/index.mjs.map +1 -1
  53. package/dist/esm/AdaptivePopup/index.native.js +47 -5
  54. package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
  55. package/dist/esm/Button/IconButtonGroup.mjs +69 -0
  56. package/dist/esm/Button/IconButtonGroup.mjs.map +1 -0
  57. package/dist/esm/Button/IconButtonGroup.native.js +72 -0
  58. package/dist/esm/Button/IconButtonGroup.native.js.map +1 -0
  59. package/dist/esm/Button/glass.mjs +8 -0
  60. package/dist/esm/Button/glass.mjs.map +1 -0
  61. package/dist/esm/Button/glass.native.js +26 -0
  62. package/dist/esm/Button/glass.native.js.map +1 -0
  63. package/dist/esm/Button/iconButtonGroupContext.mjs +7 -0
  64. package/dist/esm/Button/iconButtonGroupContext.mjs.map +1 -0
  65. package/dist/esm/Button/iconButtonGroupContext.native.js +7 -0
  66. package/dist/esm/Button/iconButtonGroupContext.native.js.map +1 -0
  67. package/dist/esm/Button/index.mjs +166 -30
  68. package/dist/esm/Button/index.mjs.map +1 -1
  69. package/dist/esm/Button/index.native.js +167 -30
  70. package/dist/esm/Button/index.native.js.map +1 -1
  71. package/dist/esm/Button/mediaPlate.mjs +6 -0
  72. package/dist/esm/Button/mediaPlate.mjs.map +1 -0
  73. package/dist/esm/Button/mediaPlate.native.js +6 -0
  74. package/dist/esm/Button/mediaPlate.native.js.map +1 -0
  75. package/dist/esm/ContextualSaveBar/index.mjs +7 -9
  76. package/dist/esm/ContextualSaveBar/index.mjs.map +1 -1
  77. package/dist/esm/ContextualSaveBar/index.native.js +7 -9
  78. package/dist/esm/ContextualSaveBar/index.native.js.map +1 -1
  79. package/dist/esm/DescriptionList/index.mjs +0 -1
  80. package/dist/esm/DescriptionList/index.mjs.map +1 -1
  81. package/dist/esm/DescriptionList/index.native.js +0 -1
  82. package/dist/esm/DescriptionList/index.native.js.map +1 -1
  83. package/dist/esm/FloatingPanel/index.native.js +22 -17
  84. package/dist/esm/FloatingPanel/index.native.js.map +1 -1
  85. package/dist/esm/fields/Barcode/BarcodeScanner.mjs +8 -9
  86. package/dist/esm/fields/Barcode/BarcodeScanner.mjs.map +1 -1
  87. package/dist/esm/fields/Barcode/BarcodeScanner.native.js +8 -9
  88. package/dist/esm/fields/Barcode/BarcodeScanner.native.js.map +1 -1
  89. package/dist/esm/fields/FileUpload/index.mjs +87 -61
  90. package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
  91. package/dist/esm/fields/FileUpload/index.native.js +100 -74
  92. package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
  93. package/dist/esm/fields/Geolocation/index.mjs +8 -9
  94. package/dist/esm/fields/Geolocation/index.mjs.map +1 -1
  95. package/dist/esm/fields/Geolocation/index.native.js +8 -9
  96. package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
  97. package/dist/esm/fields/ImageField/index.mjs +6 -8
  98. package/dist/esm/fields/ImageField/index.mjs.map +1 -1
  99. package/dist/esm/fields/ImageField/index.native.js +6 -8
  100. package/dist/esm/fields/ImageField/index.native.js.map +1 -1
  101. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs +20 -28
  102. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
  103. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js +20 -28
  104. package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  105. package/dist/esm/index.mjs +3 -2
  106. package/dist/esm/index.mjs.map +1 -1
  107. package/dist/esm/index.native.js +3 -2
  108. package/dist/esm/index.native.js.map +1 -1
  109. package/dist/esm/shared/colorRamps.mjs +0 -8
  110. package/dist/esm/shared/colorRamps.mjs.map +1 -1
  111. package/dist/esm/shared/colorRamps.native.js +0 -8
  112. package/dist/esm/shared/colorRamps.native.js.map +1 -1
  113. package/dist/esm/standardActions.mjs +91 -0
  114. package/dist/esm/standardActions.mjs.map +1 -0
  115. package/dist/esm/standardActions.native.js +91 -0
  116. package/dist/esm/standardActions.native.js.map +1 -0
  117. package/dist/jsx/AdaptivePopup/index.mjs +45 -5
  118. package/dist/jsx/AdaptivePopup/index.mjs.map +1 -1
  119. package/dist/jsx/AdaptivePopup/index.native.js +47 -5
  120. package/dist/jsx/AdaptivePopup/index.native.js.map +1 -1
  121. package/dist/jsx/Button/IconButtonGroup.mjs +69 -0
  122. package/dist/jsx/Button/IconButtonGroup.mjs.map +1 -0
  123. package/dist/jsx/Button/IconButtonGroup.native.js +101 -0
  124. package/dist/jsx/Button/IconButtonGroup.native.js.map +1 -0
  125. package/dist/jsx/Button/glass.mjs +8 -0
  126. package/dist/jsx/Button/glass.mjs.map +1 -0
  127. package/dist/jsx/Button/glass.native.js +55 -0
  128. package/dist/jsx/Button/glass.native.js.map +1 -0
  129. package/dist/jsx/Button/iconButtonGroupContext.mjs +7 -0
  130. package/dist/jsx/Button/iconButtonGroupContext.mjs.map +1 -0
  131. package/dist/jsx/Button/iconButtonGroupContext.native.js +36 -0
  132. package/dist/jsx/Button/iconButtonGroupContext.native.js.map +1 -0
  133. package/dist/jsx/Button/index.mjs +166 -30
  134. package/dist/jsx/Button/index.mjs.map +1 -1
  135. package/dist/jsx/Button/index.native.js +174 -29
  136. package/dist/jsx/Button/index.native.js.map +1 -1
  137. package/dist/jsx/Button/mediaPlate.mjs +6 -0
  138. package/dist/jsx/Button/mediaPlate.mjs.map +1 -0
  139. package/dist/jsx/Button/mediaPlate.native.js +34 -0
  140. package/dist/jsx/Button/mediaPlate.native.js.map +1 -0
  141. package/dist/jsx/ContextualSaveBar/index.mjs +7 -9
  142. package/dist/jsx/ContextualSaveBar/index.mjs.map +1 -1
  143. package/dist/jsx/ContextualSaveBar/index.native.js +6 -8
  144. package/dist/jsx/ContextualSaveBar/index.native.js.map +1 -1
  145. package/dist/jsx/DescriptionList/index.mjs +0 -1
  146. package/dist/jsx/DescriptionList/index.mjs.map +1 -1
  147. package/dist/jsx/DescriptionList/index.native.js +0 -1
  148. package/dist/jsx/DescriptionList/index.native.js.map +1 -1
  149. package/dist/jsx/FloatingPanel/index.native.js +21 -16
  150. package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
  151. package/dist/jsx/fields/Barcode/BarcodeScanner.mjs +8 -9
  152. package/dist/jsx/fields/Barcode/BarcodeScanner.mjs.map +1 -1
  153. package/dist/jsx/fields/Barcode/BarcodeScanner.native.js +7 -8
  154. package/dist/jsx/fields/Barcode/BarcodeScanner.native.js.map +1 -1
  155. package/dist/jsx/fields/FileUpload/index.mjs +87 -61
  156. package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
  157. package/dist/jsx/fields/FileUpload/index.native.js +99 -73
  158. package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
  159. package/dist/jsx/fields/Geolocation/index.mjs +8 -9
  160. package/dist/jsx/fields/Geolocation/index.mjs.map +1 -1
  161. package/dist/jsx/fields/Geolocation/index.native.js +7 -8
  162. package/dist/jsx/fields/Geolocation/index.native.js.map +1 -1
  163. package/dist/jsx/fields/ImageField/index.mjs +6 -8
  164. package/dist/jsx/fields/ImageField/index.mjs.map +1 -1
  165. package/dist/jsx/fields/ImageField/index.native.js +4 -6
  166. package/dist/jsx/fields/ImageField/index.native.js.map +1 -1
  167. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs +20 -28
  168. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
  169. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js +20 -28
  170. package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  171. package/dist/jsx/index.js +3 -2
  172. package/dist/jsx/index.js.map +1 -1
  173. package/dist/jsx/index.mjs +3 -2
  174. package/dist/jsx/index.mjs.map +1 -1
  175. package/dist/jsx/index.native.js +10 -0
  176. package/dist/jsx/index.native.js.map +1 -1
  177. package/dist/jsx/shared/colorRamps.mjs +0 -8
  178. package/dist/jsx/shared/colorRamps.mjs.map +1 -1
  179. package/dist/jsx/shared/colorRamps.native.js +0 -8
  180. package/dist/jsx/shared/colorRamps.native.js.map +1 -1
  181. package/dist/jsx/standardActions.mjs +91 -0
  182. package/dist/jsx/standardActions.mjs.map +1 -0
  183. package/dist/jsx/standardActions.native.js +120 -0
  184. package/dist/jsx/standardActions.native.js.map +1 -0
  185. package/package.json +12 -8
  186. package/src/AdaptivePopup/AdaptivePopup.spec.tsx +44 -0
  187. package/src/AdaptivePopup/AdaptivePopup.stories.tsx +12 -32
  188. package/src/AdaptivePopup/index.tsx +65 -13
  189. package/src/Button/Button.disabledGlyph.spec.tsx +111 -0
  190. package/src/Button/Button.iconButton.spec.tsx +341 -0
  191. package/src/Button/Button.stories.tsx +179 -2
  192. package/src/Button/IconButtonGroup.spec.tsx +126 -0
  193. package/src/Button/IconButtonGroup.tsx +87 -0
  194. package/src/Button/glass.native.ts +51 -0
  195. package/src/Button/glass.ts +33 -0
  196. package/src/Button/iconButtonGroupContext.ts +14 -0
  197. package/src/Button/index.tsx +314 -42
  198. package/src/Button/mediaPlate.ts +9 -0
  199. package/src/ContextualSaveBar/ContextualSaveBar.spec.tsx +3 -1
  200. package/src/ContextualSaveBar/ContextualSaveBar.stories.tsx +2 -2
  201. package/src/ContextualSaveBar/index.tsx +7 -10
  202. package/src/ControlGroup/ControlGroup.stories.tsx +3 -2
  203. package/src/DescriptionList/index.tsx +0 -1
  204. package/src/ErrorSummary.stories.tsx +2 -1
  205. package/src/FieldLayout.stories.tsx +3 -2
  206. package/src/FloatingPanel/FloatingPanel.pointMenu.native.spec.tsx +69 -0
  207. package/src/FloatingPanel/FloatingPanel.stories.tsx +6 -10
  208. package/src/FloatingPanel/index.native.tsx +25 -4
  209. package/src/Form/Form.stories.tsx +6 -5
  210. package/src/errorSummaryStories.spec.tsx +4 -4
  211. package/src/fields/Barcode/BarcodeScanner.native.tsx +10 -9
  212. package/src/fields/Barcode/BarcodeScanner.tsx +10 -9
  213. package/src/fields/Checkbox/Checkbox.stories.tsx +3 -3
  214. package/src/fields/Checkbox/CheckboxGroup.stories.tsx +2 -2
  215. package/src/fields/ColorPicker/ColorPicker.stories.tsx +2 -2
  216. package/src/fields/Combobox/Combobox.stories.tsx +2 -2
  217. package/src/fields/DatePicker/DatePicker.stories.tsx +2 -1
  218. package/src/fields/DatePicker/DateRangePicker.stories.tsx +2 -2
  219. package/src/fields/DatePicker/DatetimePicker.stories.tsx +2 -1
  220. package/src/fields/DatePicker/MonthPicker.stories.tsx +2 -2
  221. package/src/fields/DatePicker/MultiDatePicker.stories.tsx +2 -2
  222. package/src/fields/FileUpload/FileUpload.spec.tsx +14 -5
  223. package/src/fields/FileUpload/FileUpload.stories.tsx +2 -2
  224. package/src/fields/FileUpload/index.tsx +103 -77
  225. package/src/fields/Geolocation/index.tsx +12 -7
  226. package/src/fields/ImageField/ImageField.spec.tsx +5 -5
  227. package/src/fields/ImageField/ImageField.stories.tsx +2 -2
  228. package/src/fields/ImageField/index.tsx +6 -9
  229. package/src/fields/Input/Input.stories.tsx +2 -2
  230. package/src/fields/MentionInput/MentionInput.stories.tsx +2 -1
  231. package/src/fields/Progress/Progress.stories.tsx +2 -1
  232. package/src/fields/RadioGroup/RadioGroup.stories.tsx +2 -1
  233. package/src/fields/Signature/Signature.stories.tsx +2 -2
  234. package/src/fields/Stepper/Stepper.stories.tsx +2 -2
  235. package/src/fields/TextArea/TextArea.stories.tsx +2 -2
  236. package/src/fields/ToggleGroup/ToggleGroup.stories.tsx +2 -2
  237. package/src/fields/nativeDateTime/OsDateTimeSheet.presentation.spec.tsx +6 -5
  238. package/src/fields/nativeDateTime/OsDateTimeSheet.tsx +24 -12
  239. package/src/index.ts +18 -2
  240. package/src/shared/colorRamps.ts +1 -11
  241. package/src/standardActions.ts +96 -0
  242. package/types/AdaptivePopup/index.d.ts +13 -1
  243. package/types/AdaptivePopup/index.d.ts.map +1 -1
  244. package/types/Button/IconButtonGroup.d.ts +22 -0
  245. package/types/Button/IconButtonGroup.d.ts.map +1 -0
  246. package/types/Button/glass.d.ts +26 -0
  247. package/types/Button/glass.d.ts.map +1 -0
  248. package/types/Button/glass.native.d.ts +17 -0
  249. package/types/Button/glass.native.d.ts.map +1 -0
  250. package/types/Button/iconButtonGroupContext.d.ts +9 -0
  251. package/types/Button/iconButtonGroupContext.d.ts.map +1 -0
  252. package/types/Button/index.d.ts +55 -2
  253. package/types/Button/index.d.ts.map +1 -1
  254. package/types/Button/mediaPlate.d.ts +10 -0
  255. package/types/Button/mediaPlate.d.ts.map +1 -0
  256. package/types/ContextualSaveBar/index.d.ts.map +1 -1
  257. package/types/DescriptionList/index.d.ts.map +1 -1
  258. package/types/FloatingPanel/index.native.d.ts +2 -1
  259. package/types/FloatingPanel/index.native.d.ts.map +1 -1
  260. package/types/fields/Barcode/BarcodeScanner.d.ts.map +1 -1
  261. package/types/fields/Barcode/BarcodeScanner.native.d.ts.map +1 -1
  262. package/types/fields/FileUpload/index.d.ts.map +1 -1
  263. package/types/fields/Geolocation/index.d.ts.map +1 -1
  264. package/types/fields/ImageField/index.d.ts.map +1 -1
  265. package/types/fields/nativeDateTime/OsDateTimeSheet.d.ts.map +1 -1
  266. package/types/index.d.ts +4 -2
  267. package/types/index.d.ts.map +1 -1
  268. package/types/shared/colorRamps.d.ts +1 -11
  269. package/types/shared/colorRamps.d.ts.map +1 -1
  270. package/types/standardActions.d.ts +24 -0
  271. package/types/standardActions.d.ts.map +1 -0
@@ -27,6 +27,8 @@ import {
27
27
  YStack,
28
28
  } from "tamagui";
29
29
  import { X } from "@tamagui/lucide-icons-2";
30
+ import { IconButton } from "../Button";
31
+ import { standardActions } from "../standardActions";
30
32
  import { formCommonColors } from "../shared/colorRamps";
31
33
  import { useReturnFocusOnClose, useSheetModalFocus } from "../shared/focusManagement";
32
34
  import { t } from "../shared/t";
@@ -71,6 +73,18 @@ export interface AdaptivePopupProps {
71
73
  showCloseButton?: boolean;
72
74
  /** Custom footer content (e.g. action buttons) */
73
75
  footer?: ReactNode;
76
+ /**
77
+ * LC-260: the popup's one confirm. The header becomes the iOS 26 sheet
78
+ * bar: cancel (X) leading, the title centred, the accent check trailing,
79
+ * and the corner close goes.
80
+ */
81
+ onConfirm?: () => void;
82
+ confirmKind?: "confirm" | "done" | "save" | "submit";
83
+ /** The confirm's accessible name when its kind is too thin ("Save profile"). */
84
+ confirmLabel?: string;
85
+ confirmDisabled?: boolean;
86
+ confirmDisabledReason?: string;
87
+ confirmLoading?: boolean;
74
88
  /** Whether to disable the trigger */
75
89
  disabled?: boolean;
76
90
  }
@@ -210,6 +224,12 @@ export function AdaptivePopup({
210
224
  showCloseButton = true,
211
225
  footer,
212
226
  disabled,
227
+ onConfirm,
228
+ confirmKind = "done",
229
+ confirmLabel,
230
+ confirmDisabled,
231
+ confirmDisabledReason,
232
+ confirmLoading,
213
233
  }: AdaptivePopupProps) {
214
234
  const { knobProps } = useResolvedKnobs({ painted: false });
215
235
  const { knobProps: nestedKnobs } = useResolvedKnobs();
@@ -282,22 +302,54 @@ export function AdaptivePopup({
282
302
  : { onPress: openFromTrigger }),
283
303
  };
284
304
 
285
- const headerContent = (title || showCloseButton) && (
286
- <Unspaced>
287
- <XStack
288
- justifyContent="space-between"
289
- alignItems="center"
290
- paddingRight={showCloseButton ? closePx : undefined}
291
- minHeight={showCloseButton ? closePx : undefined}
292
- >
305
+ const confirms = onConfirm != null;
306
+ const cornerClose = showCloseButton && !confirms;
307
+ const confirmBar = confirms && (
308
+ <XStack alignItems="center" data-mp-overlay-header="confirm" {...knobProps.gapSm}>
309
+ <IconButton
310
+ icon={standardActions.cancel.icon}
311
+ aria-label={t("Cancel")}
312
+ data-testid="adaptive-popup-cancel"
313
+ onPress={() => onOpenChange(false)}
314
+ />
315
+ <XStack flex={1} minWidth={0} justifyContent="center">
293
316
  {title ? (
294
- <YStack flex={1}>
295
- <Dialog.Title {...knobProps.heading}>{title}</Dialog.Title>
296
- </YStack>
317
+ <Dialog.Title {...knobProps.heading} textAlign="center">
318
+ {title}
319
+ </Dialog.Title>
297
320
  ) : null}
298
321
  </XStack>
299
- </Unspaced>
322
+ <IconButton
323
+ icon={standardActions[confirmKind].icon}
324
+ plate="primary"
325
+ aria-label={confirmLabel ?? t(standardActions[confirmKind].label)}
326
+ data-testid="adaptive-popup-confirm"
327
+ loading={confirmLoading}
328
+ disabled={confirmDisabled}
329
+ disabledReason={confirmDisabledReason}
330
+ disabledReasonPlacement="inline"
331
+ onPress={onConfirm}
332
+ />
333
+ </XStack>
300
334
  );
335
+ const headerContent = confirms
336
+ ? confirmBar
337
+ : (title || showCloseButton) && (
338
+ <Unspaced>
339
+ <XStack
340
+ justifyContent="space-between"
341
+ alignItems="center"
342
+ paddingRight={cornerClose ? closePx : undefined}
343
+ minHeight={cornerClose ? closePx : undefined}
344
+ >
345
+ {title ? (
346
+ <YStack flex={1}>
347
+ <Dialog.Title {...knobProps.heading}>{title}</Dialog.Title>
348
+ </YStack>
349
+ ) : null}
350
+ </XStack>
351
+ </Unspaced>
352
+ );
301
353
 
302
354
  const footerContent = footer && <YStack paddingTop="$2">{footer}</YStack>;
303
355
 
@@ -320,7 +372,7 @@ export function AdaptivePopup({
320
372
  )}
321
373
  <YStack flex={size === "fullscreen" ? 1 : undefined}>{children}</YStack>
322
374
  {footerContent}
323
- {showCloseButton ? <PopupCloseButton onPress={() => onOpenChange(false)} /> : null}
375
+ {cornerClose ? <PopupCloseButton onPress={() => onOpenChange(false)} /> : null}
324
376
  </YStack>
325
377
  );
326
378
 
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ *
4
+ * LC-40 DISABLED-VISIBLE for a frameless control (FZ-7889 round 4). A
5
+ * disabled chromeless Button is its enabled twin dimmed: the same transparent
6
+ * frame and ink, at the recipe's glyph opacity. The keepLabel wash once
7
+ * filled it, so a pager's first-page chevron read as more prominent than the
8
+ * enabled next chevron beside it.
9
+ *
10
+ * jsdom cascades no Tamagui CSS, so this reads the atomic classes each frame
11
+ * asks for and the opacity rule behind its `_o-` class.
12
+ */
13
+
14
+ import { cleanup, render, screen } from "@testing-library/react";
15
+ import { createDefaultThemeConfig, Preset } from "@multiplatform.one/theme";
16
+ import type { ReactElement } from "react";
17
+ import { TamaguiProvider, YStack } from "tamagui";
18
+ import { afterEach, describe, expect, it } from "vitest";
19
+ import { Button } from "./index";
20
+
21
+ afterEach(cleanup);
22
+
23
+ const houseConfig = createDefaultThemeConfig();
24
+
25
+ const SCHEMES = ["light", "dark"] as const;
26
+
27
+ function houseRender(ui: ReactElement, scheme: string, disabledStyle = "keepLabel") {
28
+ return render(
29
+ <TamaguiProvider config={houseConfig.tamagui} defaultTheme={scheme} disableInjectCSS>
30
+ <Preset overrides={{ disabledStyle: disabledStyle as "keepLabel" | "dimWhole" }}>
31
+ <YStack backgroundColor="$background">{ui}</YStack>
32
+ </Preset>
33
+ </TamaguiProvider>,
34
+ );
35
+ }
36
+
37
+ function injectedOpacity(className: string): number {
38
+ for (const sheet of Array.from(document.styleSheets)) {
39
+ for (const rule of Array.from(sheet.cssRules)) {
40
+ if (!(rule instanceof CSSStyleRule)) continue;
41
+ if (!rule.selectorText.endsWith(`.${className}`)) continue;
42
+ const value = rule.style.getPropertyValue("opacity");
43
+ if (value) return Number(value);
44
+ }
45
+ }
46
+ throw new Error(`no injected opacity rule for .${className}`);
47
+ }
48
+
49
+ function opacityOf(el: Element): number {
50
+ const cls = Array.from(el.classList).find((c) => c.startsWith("_o-"));
51
+ return cls ? injectedOpacity(cls) : 1;
52
+ }
53
+
54
+ /** Resting paint classes for one property prefix (`_bg-`, `_col-`, `_bc-`). */
55
+ function rest(el: Element, prefix: string): string[] {
56
+ return Array.from(el.classList)
57
+ .filter((c) => c.startsWith(prefix) && !/0(hover|active|focus|press)/.test(c))
58
+ .sort();
59
+ }
60
+
61
+ function pair(scheme: string, disabledStyle?: string) {
62
+ houseRender(
63
+ <>
64
+ <Button chromeless aria-label="Next">
65
+ Next
66
+ </Button>
67
+ <Button chromeless aria-label="Previous" disabled>
68
+ Previous
69
+ </Button>
70
+ </>,
71
+ scheme,
72
+ disabledStyle,
73
+ );
74
+ return {
75
+ enabled: screen.getByLabelText("Next"),
76
+ disabled: screen.getByLabelText("Previous"),
77
+ enabledLabel: screen.getByText("Next"),
78
+ disabledLabel: screen.getByText("Previous"),
79
+ };
80
+ }
81
+
82
+ describe("LC-40: a disabled chromeless Button is its enabled twin, dimmed", () => {
83
+ it.each(SCHEMES)("%s keepLabel: no wash, same ink, glyph opacity", (scheme) => {
84
+ const { enabled, disabled, enabledLabel, disabledLabel } = pair(scheme);
85
+ expect(rest(disabled, "_bg-")).toEqual(rest(enabled, "_bg-"));
86
+ expect(rest(disabled, "_bc-")).toEqual(rest(enabled, "_bc-"));
87
+ expect(rest(disabledLabel, "_col-")).toEqual(rest(enabledLabel, "_col-"));
88
+ expect(opacityOf(enabled)).toBe(1);
89
+ expect(opacityOf(disabled)).toBeCloseTo(0.35);
90
+ });
91
+
92
+ it.each(SCHEMES)("%s dimWhole: no wash, the assembly dim alone", (scheme) => {
93
+ const { enabled, disabled } = pair(scheme, "dimWhole");
94
+ expect(rest(disabled, "_bg-")).toEqual(rest(enabled, "_bg-"));
95
+ expect(opacityOf(disabled)).toBeCloseTo(0.5);
96
+ });
97
+
98
+ it("a framed Button keeps the keepLabel wash", () => {
99
+ houseRender(
100
+ <>
101
+ <Button>Enabled</Button>
102
+ <Button disabled>Disabled</Button>
103
+ </>,
104
+ "light",
105
+ );
106
+ const enabled = screen.getByText("Enabled").closest("[role=button]")!;
107
+ const disabled = screen.getByText("Disabled").closest("[role=button]")!;
108
+ expect(rest(disabled, "_bg-")).not.toEqual(rest(enabled, "_bg-"));
109
+ expect(opacityOf(disabled)).toBe(1);
110
+ });
111
+ });
@@ -0,0 +1,341 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ *
4
+ * LC-176 ICON-BUTTON (FZ-8020). Clay: square or round buttons holding a
5
+ * single icon felt wrong, "it's not supposed to have background or border
6
+ * around it". An icon-only Button paints no plate: a square box on the 44pt
7
+ * slop, and a plate only over media. Round 3 (Clay's compare pick, "USE the
8
+ * native look where possible"): the context picks the treatment. In a bar it
9
+ * is the bar tint, on Liquid Glass where iOS draws it; in content with no
10
+ * intent it is a bold glyph in the text colour.
11
+ *
12
+ * jsdom cascades no Tamagui CSS, so this reads the atomic classes each frame
13
+ * asks for.
14
+ */
15
+
16
+ import { cleanup, render, screen } from "@testing-library/react";
17
+ import { createDefaultThemeConfig, Surface } from "@multiplatform.one/theme";
18
+ import type { ReactElement } from "react";
19
+ import { TamaguiProvider, YStack } from "tamagui";
20
+ import { afterEach, describe, expect, it, vi } from "vitest";
21
+ import {
22
+ Button,
23
+ ICON_BUTTON_CIRCLE_GLYPH_RATIO,
24
+ ICON_BUTTON_GLYPH_PX,
25
+ ICON_BUTTON_NEUTRAL_GLYPH_PX,
26
+ IconButton,
27
+ iconButtonGlyphPx,
28
+ labelLinePx,
29
+ } from "./index";
30
+
31
+ const glass = vi.hoisted(() => ({ available: false }));
32
+ vi.mock("./glass", () => ({
33
+ loadGlassView: () =>
34
+ glass.available ? (props: object) => <div data-glass="" {...props} /> : null,
35
+ loadGlassContainer: () => null,
36
+ }));
37
+
38
+ afterEach(() => {
39
+ cleanup();
40
+ glass.available = false;
41
+ });
42
+
43
+ const houseConfig = createDefaultThemeConfig();
44
+
45
+ const SCHEMES = ["light", "dark"] as const;
46
+
47
+ function Glyph({ size, color, weight }: { size?: number; color?: string; weight?: string }) {
48
+ return <svg data-glyph="" data-weight={weight} width={size} height={size} color={color} />;
49
+ }
50
+
51
+ function glyphOf(label: string): SVGElement {
52
+ return screen.getByLabelText(label).querySelector("[data-glyph]") as SVGElement;
53
+ }
54
+
55
+ function houseRender(ui: ReactElement, scheme: string) {
56
+ return render(
57
+ <TamaguiProvider config={houseConfig.tamagui} defaultTheme={scheme} disableInjectCSS>
58
+ <YStack backgroundColor="$background">{ui}</YStack>
59
+ </TamaguiProvider>,
60
+ );
61
+ }
62
+
63
+ /** Resting paint classes for one property prefix (`_bg-`, `_col-`, `_bc-`). */
64
+ function rest(el: Element, prefix: string): string[] {
65
+ return Array.from(el.classList)
66
+ .filter((c) => c.startsWith(prefix) && !/0(hover|active|focus|press)/.test(c))
67
+ .sort();
68
+ }
69
+
70
+ describe("LC-176: an icon-only Button is a bare glyph", () => {
71
+ it.each(SCHEMES)("%s: in a bar, no fill and no border at rest, the bar tint as ink", (scheme) => {
72
+ houseRender(
73
+ <>
74
+ <Surface bar>
75
+ <Button icon={Glyph} aria-label="More" />
76
+ <Button chromeless accentInk icon={Glyph} aria-label="Bar" />
77
+ </Surface>
78
+ <Button icon={Glyph}>Labelled</Button>
79
+ </>,
80
+ scheme,
81
+ );
82
+ const glyph = screen.getByLabelText("More");
83
+ const bar = screen.getByLabelText("Bar");
84
+ const labelled = screen.getByText("Labelled").closest("[role=button]")!;
85
+ expect(glyph.getAttribute("data-mp-icon-button")).toBe("glyph");
86
+ expect(rest(glyph, "_bg-")).toEqual(rest(bar, "_bg-"));
87
+ expect(rest(glyph, "_bc-")).toEqual(rest(bar, "_bc-"));
88
+ expect(rest(glyph, "_col-")).toEqual(rest(bar, "_col-"));
89
+ expect(rest(glyph, "_bg-")).not.toEqual(rest(labelled, "_bg-"));
90
+ expect(labelled.hasAttribute("data-mp-icon-button")).toBe(false);
91
+ });
92
+
93
+ it("web hover and press light a soft plate", () => {
94
+ houseRender(<Button icon={Glyph} aria-label="More" />, "light");
95
+ const classes = Array.from(screen.getByLabelText("More").classList);
96
+ expect(classes.some((c) => /^_bg-0hover-/.test(c))).toBe(true);
97
+ expect(classes.some((c) => /^_bg-0active-/.test(c))).toBe(true);
98
+ });
99
+
100
+ it("sits in a square box with the press slop", () => {
101
+ houseRender(<Button icon={Glyph} aria-label="More" />, "light");
102
+ const glyph = screen.getByLabelText("More");
103
+ const height = glyph.getAttribute("data-mp-button-height");
104
+ expect(Number(height)).toBeGreaterThan(0);
105
+ expect(Array.from(glyph.classList).some((c) => /^_w-/.test(c))).toBe(true);
106
+ expect(Array.from(glyph.classList).some((c) => /^_pr-0px|^_pr-0\b/.test(c))).toBe(true);
107
+ });
108
+
109
+ it("an explicit frame is honoured, so the design lint can name it", () => {
110
+ houseRender(
111
+ <>
112
+ <Button outlined icon={Glyph} aria-label="Outlined" />
113
+ <Button circular icon={Glyph} aria-label="Circular" />
114
+ <Button chromeless={false} icon={Glyph} aria-label="Filled" />
115
+ </>,
116
+ "light",
117
+ );
118
+ for (const label of ["Outlined", "Circular", "Filled"])
119
+ expect(screen.getByLabelText(label).hasAttribute("data-mp-icon-button")).toBe(false);
120
+ });
121
+
122
+ it.each(SCHEMES)(
123
+ "%s: circular chromeless paints no plate, so it is an icon button too",
124
+ (scheme) => {
125
+ // The SHC console's add (+) and copy buttons, written before LC-176.
126
+ houseRender(
127
+ <>
128
+ <Button size="$3" circular chromeless accentInk icon={Glyph} aria-label="Add" />
129
+ <Button size="$3" chromeless accentInk icon={Glyph} aria-label="Bare" />
130
+ </>,
131
+ scheme,
132
+ );
133
+ const add = screen.getByLabelText("Add");
134
+ expect(add.getAttribute("data-mp-icon-button")).toBe("glyph");
135
+ expect(rest(add, "_bg-")).toEqual(rest(screen.getByLabelText("Bare"), "_bg-"));
136
+ expect(rest(add, "_col-")).toEqual(rest(screen.getByLabelText("Bare"), "_col-"));
137
+ expect(glyphOf("Add").getAttribute("data-weight")).toBe("bold");
138
+ expect(Number(glyphOf("Add").getAttribute("width"))).toBe(
139
+ Number(glyphOf("Bare").getAttribute("width")),
140
+ );
141
+ // A pointer toolbar keeps its token box (LC-54), so the floor is the
142
+ // glyph the box holds, never the recipe's small label icon.
143
+ expect(Number(glyphOf("Add").getAttribute("width"))).toBeGreaterThan(16);
144
+ },
145
+ );
146
+
147
+ it.each(["$3", "$4"] as const)(
148
+ "%s: a circle the caller asks for carries its glyph at the Settings ratio, bold",
149
+ (size) => {
150
+ // Clay, IMG_1926 on FZ-8020: the SHC Updates rows drew a ~12pt glyph in
151
+ // a 44pt `circular outlined` plate.
152
+ houseRender(
153
+ <Button size={size} circular outlined icon={Glyph} aria-label="Update" />,
154
+ "dark",
155
+ );
156
+ const update = screen.getByLabelText("Update");
157
+ expect(update.hasAttribute("data-mp-icon-button")).toBe(false);
158
+ const circle = Number(update.getAttribute("data-mp-button-height"));
159
+ const glyph = Number(glyphOf("Update").getAttribute("width"));
160
+ expect(glyph).toBe(Math.round(circle * ICON_BUTTON_CIRCLE_GLYPH_RATIO));
161
+ expect(glyph / circle).toBeGreaterThanOrEqual(0.55);
162
+ expect(glyph / circle).toBeLessThanOrEqual(0.6);
163
+ expect(glyphOf("Update").getAttribute("data-weight")).toBe("bold");
164
+ },
165
+ );
166
+
167
+ it("a selected icon toggle keeps the LC-131 selected fill", () => {
168
+ houseRender(<Button icon={Glyph} aria-label="Shuffle" selected />, "light");
169
+ expect(screen.getByLabelText("Shuffle").getAttribute("data-mp-selected")).toBe("true");
170
+ });
171
+
172
+ it.each(SCHEMES)("%s: plate=media is the one plated icon button", (scheme) => {
173
+ houseRender(
174
+ <>
175
+ <Button plate="media" icon={Glyph} aria-label="Remove" />
176
+ <Button icon={Glyph} aria-label="More" />
177
+ </>,
178
+ scheme,
179
+ );
180
+ const media = screen.getByLabelText("Remove");
181
+ expect(media.getAttribute("data-mp-icon-button")).toBe("media");
182
+ expect(rest(media, "_bg-")).not.toEqual(rest(screen.getByLabelText("More"), "_bg-"));
183
+ expect(rest(media, "_bg-").join(" ")).not.toMatch(/transparent/);
184
+ });
185
+
186
+ it("a toggle over media shows its state in the glyph, not a selected fill", () => {
187
+ houseRender(<Button plate="media" icon={Glyph} aria-label="Like" aria-pressed />, "light");
188
+ expect(screen.getByLabelText("Like").hasAttribute("data-mp-selected")).toBe(false);
189
+ });
190
+
191
+ it.each(SCHEMES)(
192
+ "%s: in content with no intent, a bold glyph in the text colour, 24pt or more",
193
+ (scheme) => {
194
+ houseRender(
195
+ <>
196
+ <Button icon={Glyph} aria-label="Neutral" />
197
+ <Button error icon={Glyph} aria-label="Delete" />
198
+ <Surface bar>
199
+ <Button icon={Glyph} aria-label="Bar" />
200
+ </Surface>
201
+ </>,
202
+ scheme,
203
+ );
204
+ const neutral = screen.getByLabelText("Neutral");
205
+ expect(neutral.getAttribute("data-mp-icon-button")).toBe("neutral");
206
+ expect(rest(neutral, "_bg-").join(" ")).not.toMatch(/background|color\d/);
207
+ expect(rest(neutral, "_col-")).not.toEqual(rest(screen.getByLabelText("Bar"), "_col-"));
208
+ expect(glyphOf("Neutral").getAttribute("data-weight")).toBe("bold");
209
+ expect(ICON_BUTTON_NEUTRAL_GLYPH_PX).toBeGreaterThanOrEqual(24);
210
+ expect(Number(glyphOf("Neutral").getAttribute("width"))).toBeGreaterThanOrEqual(
211
+ ICON_BUTTON_NEUTRAL_GLYPH_PX,
212
+ );
213
+ // An intent keeps its own on-page ink; the stroke stays bold.
214
+ expect(screen.getByLabelText("Delete").getAttribute("data-mp-icon-button")).toBe("glyph");
215
+ expect(glyphOf("Delete").getAttribute("data-weight")).toBe("bold");
216
+ },
217
+ );
218
+
219
+ it("on iOS 26 with the glass module, a bar's icon button floats on Liquid Glass", () => {
220
+ glass.available = true;
221
+ houseRender(
222
+ <>
223
+ <Surface bar>
224
+ <Button icon={Glyph} aria-label="Search" />
225
+ <IconButton inline icon={Glyph} aria-label="InlineInBar" />
226
+ </Surface>
227
+ <Button icon={Glyph} aria-label="Content" />
228
+ </>,
229
+ "light",
230
+ );
231
+ const search = screen.getByLabelText("Search");
232
+ expect(search.getAttribute("data-mp-icon-button")).toBe("glass");
233
+ expect(search.querySelector("[data-glass]")).not.toBeNull();
234
+ expect(glyphOf("Search").getAttribute("data-weight")).toBe("bold");
235
+ // Glass is the bar button; content and inline glyphs never float on it.
236
+ expect(screen.getByLabelText("Content").querySelector("[data-glass]")).toBeNull();
237
+ expect(screen.getByLabelText("InlineInBar").querySelector("[data-glass]")).toBeNull();
238
+ });
239
+
240
+ it("without the glass module (web, Android, older iOS, an older binary) the bar glyph stays bare", () => {
241
+ houseRender(
242
+ <Surface bar>
243
+ <Button icon={Glyph} aria-label="Search" />
244
+ </Surface>,
245
+ "light",
246
+ );
247
+ const search = screen.getByLabelText("Search");
248
+ expect(search.getAttribute("data-mp-icon-button")).toBe("glyph");
249
+ expect(search.querySelector("[data-glass]")).toBeNull();
250
+ });
251
+
252
+ it("IconButton is the same glyph", () => {
253
+ houseRender(
254
+ <Surface bar>
255
+ <IconButton icon={Glyph} aria-label="Named" />
256
+ <Button icon={Glyph} aria-label="Plain" />
257
+ </Surface>,
258
+ "light",
259
+ );
260
+ const named = screen.getByLabelText("Named");
261
+ const plain = screen.getByLabelText("Plain");
262
+ expect(named.getAttribute("data-mp-icon-button")).toBe("glyph");
263
+ expect(rest(named, "_bg-")).toEqual(rest(plain, "_bg-"));
264
+ });
265
+
266
+ it("a bar glyph is as tall in ink as the row's text line, Phosphor bold, in its box", () => {
267
+ houseRender(
268
+ <Surface bar>
269
+ <Button size="$2" icon={Glyph} aria-label="Small" />
270
+ <Button icon={Glyph} aria-label="Default" />
271
+ <IconButton glyphWeight="regular" icon={Glyph} aria-label="Regular" />
272
+ <Button size="$2" icon={Glyph}>
273
+ Label
274
+ </Button>
275
+ </Surface>,
276
+ "light",
277
+ );
278
+ expect(ICON_BUTTON_GLYPH_PX).toBeGreaterThanOrEqual(24);
279
+ const line = labelLinePx("$body");
280
+ expect(line).toBeDefined();
281
+ const glyphPx = Number(glyphOf("Default").getAttribute("width"));
282
+ expect(glyphPx).toBeGreaterThanOrEqual(iconButtonGlyphPx(line));
283
+ // Phosphor inks ~208 of 256 units: the glyph's ink meets the text line.
284
+ expect(Math.abs((iconButtonGlyphPx(line) * 208) / 256 - line!)).toBeLessThanOrEqual(1);
285
+ for (const label of ["Small", "Default"]) {
286
+ const glyph = glyphOf(label);
287
+ expect(glyph.getAttribute("data-weight")).toBe("bold");
288
+ const box = Number(screen.getByLabelText(label).getAttribute("data-mp-button-height"));
289
+ expect(box).toBeGreaterThanOrEqual(Number(glyph.getAttribute("width")));
290
+ }
291
+ expect(glyphOf("Regular").getAttribute("data-weight")).toBe("regular");
292
+ const labelled = screen.getByText("Label").closest("[role=button]")!;
293
+ const labelGlyph = labelled.querySelector("[data-glyph]")!;
294
+ expect(Number(labelGlyph.getAttribute("width"))).toBeLessThan(ICON_BUTTON_GLYPH_PX);
295
+ expect(labelGlyph.hasAttribute("data-weight")).toBe(false);
296
+ });
297
+
298
+ it("a plate never shrinks its glyph: media and confirm take the bar size", () => {
299
+ houseRender(
300
+ <>
301
+ <Surface bar>
302
+ <Button icon={Glyph} aria-label="Bar" />
303
+ </Surface>
304
+ <Button plate="media" icon={Glyph} aria-label="Remove photo" />
305
+ <Button plate="primary" icon={Glyph} aria-label="Done" />
306
+ </>,
307
+ "light",
308
+ );
309
+ const bar = Number(glyphOf("Bar").getAttribute("width"));
310
+ for (const label of ["Remove photo", "Done"]) {
311
+ expect(Number(glyphOf(label).getAttribute("width"))).toBe(bar);
312
+ expect(glyphOf(label).getAttribute("data-weight")).toBe("bold");
313
+ }
314
+ });
315
+
316
+ it("an inline glyph is the text's size, 8pt after the value, bold and tinted in content", () => {
317
+ houseRender(
318
+ <>
319
+ <IconButton inline icon={Glyph} aria-label="Copy" />
320
+ <IconButton icon={Glyph} aria-label="Bar" />
321
+ <IconButton accentInk icon={Glyph} aria-label="Tinted" />
322
+ </>,
323
+ "light",
324
+ );
325
+ const copy = screen.getByLabelText("Copy");
326
+ expect(copy.getAttribute("data-mp-icon-button")).toBe("inline");
327
+ // Clay on the SHC Nodes screen (FZ-8020): the copy beside "shc node join"
328
+ // read as untinted text. It takes the identity ink, not the text colour.
329
+ expect(rest(copy, "_col-")).toEqual(rest(screen.getByLabelText("Tinted"), "_col-"));
330
+ expect(rest(copy, "_col-")).not.toEqual(rest(screen.getByLabelText("Bar"), "_col-"));
331
+ const size = Number(glyphOf("Copy").getAttribute("width"));
332
+ expect(size).toBeGreaterThan(12);
333
+ expect(size).toBeLessThan(ICON_BUTTON_GLYPH_PX);
334
+ expect(Number(copy.getAttribute("data-mp-button-height"))).toBe(size);
335
+ expect(copy.hasAttribute("data-mp-press-slop")).toBe(true);
336
+ expect(Array.from(copy.classList).some((c) => /^_(mis|ms|ml|marginInlineStart)-/.test(c))).toBe(
337
+ true,
338
+ );
339
+ expect(glyphOf("Copy").getAttribute("data-weight")).toBe("bold");
340
+ });
341
+ });