@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
@@ -6,9 +6,24 @@ import {
6
6
  FOCUS_RING_MIN_CONTRAST,
7
7
  ensureFocusVisibleRing,
8
8
  Paragraph,
9
+ Surface,
9
10
  } from "@multiplatform.one/theme";
10
11
  import { Button as TamaguiButton, XStack, YStack, useTheme } from "tamagui";
11
- import { Button, type ButtonProps } from "./index";
12
+ import {
13
+ ArchiveIcon,
14
+ ArrowCircleUpIcon,
15
+ ArrowsClockwiseIcon,
16
+ CaretLeftIcon,
17
+ CopyIcon,
18
+ DotsThreeIcon,
19
+ DownloadSimpleIcon,
20
+ EnvelopeSimpleIcon,
21
+ HeartIcon,
22
+ PlusIcon,
23
+ TrashIcon,
24
+ XIcon,
25
+ } from "@phosphor-icons/react";
26
+ import { Button, IconButton, IconButtonGroup, type ButtonProps } from "./index";
12
27
  import { useState } from "react";
13
28
 
14
29
  const pressed = (name: string) => () => action(name)();
@@ -411,10 +426,172 @@ export const DisabledReasons: Story = {
411
426
  Publish
412
427
  </Button>
413
428
  <Button disabled disabledReason="No saved changes" disabledReasonPlacement={placement}>
414
- Save
429
+ Save draft
415
430
  </Button>
416
431
  </YStack>
417
432
  ))}
418
433
  </YStack>
419
434
  ),
420
435
  };
436
+
437
+ /**
438
+ * LC-176 ICON-BUTTON, by context. In a bar the glyph takes the bar tint (on
439
+ * Liquid Glass on iOS 26); in content with no intent it is a bold glyph in the
440
+ * text colour; an intent keeps its ink; a selected toggle keeps LC-131; inline
441
+ * beside a value it is the text's size; over media it rides the one plate.
442
+ */
443
+ export const IconButtons: Story = {
444
+ render: () => {
445
+ const [on, setOn] = useState(false);
446
+ return (
447
+ <YStack gap="$4" padding="$4" backgroundColor="$background">
448
+ <Surface bar>
449
+ <XStack alignItems="center" data-testid="icon-button-bar">
450
+ <IconButton icon={CaretLeftIcon} aria-label="Back" onPress={pressed("back")} />
451
+ <Paragraph flex={1} fontWeight="600">
452
+ Inbox
453
+ </Paragraph>
454
+ <IconButtonGroup data-testid="icon-button-toolbar">
455
+ <IconButton icon={ArchiveIcon} aria-label="Archive" onPress={pressed("archive")} />
456
+ <IconButton icon={TrashIcon} aria-label="Delete" onPress={pressed("delete")} />
457
+ <IconButton
458
+ icon={EnvelopeSimpleIcon}
459
+ aria-label="Mark unread"
460
+ selected={on}
461
+ onPress={() => setOn((v) => !v)}
462
+ />
463
+ <IconButton icon={DotsThreeIcon} aria-label="More" onPress={pressed("more")} />
464
+ </IconButtonGroup>
465
+ </XStack>
466
+ </Surface>
467
+ <Surface bar>
468
+ <XStack alignItems="center" data-testid="icon-button-bar-disabled">
469
+ <Paragraph flex={1}>Downloads</Paragraph>
470
+ <IconButtonGroup>
471
+ <IconButton
472
+ icon={DownloadSimpleIcon}
473
+ aria-label="Download"
474
+ onPress={pressed("download")}
475
+ />
476
+ <IconButton
477
+ icon={DownloadSimpleIcon}
478
+ aria-label="Download (nothing to download)"
479
+ disabled
480
+ />
481
+ </IconButtonGroup>
482
+ </XStack>
483
+ </Surface>
484
+ <XStack alignItems="center" data-testid="icon-button-content">
485
+ <Paragraph flex={1}>A row in content</Paragraph>
486
+ <IconButtonGroup>
487
+ <IconButton
488
+ icon={DownloadSimpleIcon}
489
+ aria-label="Download"
490
+ onPress={pressed("download")}
491
+ />
492
+ <IconButton icon={TrashIcon} error aria-label="Delete" onPress={pressed("delete")} />
493
+ </IconButtonGroup>
494
+ </XStack>
495
+ <XStack alignItems="center" data-testid="icon-button-inline">
496
+ <Paragraph>shc node join</Paragraph>
497
+ <IconButton inline icon={CopyIcon} aria-label="Copy" onPress={pressed("copy")} />
498
+ </XStack>
499
+ <YStack
500
+ position="relative"
501
+ width="$20"
502
+ height="$12"
503
+ backgroundColor="$blue9"
504
+ borderRadius="$4"
505
+ data-testid="icon-button-media"
506
+ >
507
+ <YStack position="absolute" top="$2" left="$2">
508
+ <IconButton
509
+ plate="media"
510
+ icon={HeartIcon}
511
+ aria-label="Favourite"
512
+ onPress={pressed("favourite")}
513
+ />
514
+ </YStack>
515
+ <YStack position="absolute" top="$2" right="$2">
516
+ <IconButtonGroup plate="media">
517
+ <IconButton
518
+ plate="media"
519
+ icon={ArrowsClockwiseIcon}
520
+ aria-label="Replace photo"
521
+ onPress={pressed("replace")}
522
+ />
523
+ <IconButton
524
+ plate="media"
525
+ icon={XIcon}
526
+ aria-label="Remove photo"
527
+ onPress={pressed("remove")}
528
+ />
529
+ </IconButtonGroup>
530
+ </YStack>
531
+ </YStack>
532
+ </YStack>
533
+ );
534
+ },
535
+ };
536
+
537
+ // The SHC console's call sites as written (features/console place.tsx and
538
+ // CopyButton.tsx), spread so the design lint's `circular` finding stays on
539
+ // SHC's own code rather than this reproduction.
540
+ const shcRoundGlyph = { circular: true, chromeless: true, accentInk: true } as const;
541
+
542
+ /** Clay's SHC Nodes screen (IMG_1925 on FZ-8020): the add and the copy. */
543
+ export const ShcNodesScreen: Story = {
544
+ render: () => (
545
+ <YStack gap="$4" padding="$4" backgroundColor="$background" data-testid="shc-nodes">
546
+ <XStack alignItems="center" gap="$2">
547
+ <Paragraph flex={1} size="$9" fontWeight="700">
548
+ Nodes
549
+ </Paragraph>
550
+ <Button size="$3" {...shcRoundGlyph} icon={PlusIcon} aria-label="Add node" />
551
+ </XStack>
552
+ <XStack alignItems="center" gap="$2">
553
+ <Paragraph fontFamily="$mono">shc node join</Paragraph>
554
+ <Button size="$2" {...shcRoundGlyph} icon={CopyIcon} aria-label="Copy" />
555
+ </XStack>
556
+ <XStack alignItems="center">
557
+ <Paragraph fontFamily="$mono">shc node join</Paragraph>
558
+ <IconButton inline icon={CopyIcon} aria-label="Copy (inline)" />
559
+ </XStack>
560
+ </YStack>
561
+ ),
562
+ };
563
+
564
+ // SHC features/console manage/app/UpdatesPane.tsx as written.
565
+ const shcUpdatePlate = { circular: true, outlined: true } as const;
566
+
567
+ /**
568
+ * Clay's SHC Updates rows (IMG_1926 on FZ-8020): the plate SHC asks for, its
569
+ * glyph now at the circle ratio, above the bare tinted glyph LC-176 wants.
570
+ */
571
+ export const ShcUpdateRows: Story = {
572
+ render: () => (
573
+ <YStack gap="$2" padding="$4" backgroundColor="$background" data-testid="shc-update-rows">
574
+ {(["$3", "$4"] as const).map((size) => (
575
+ <XStack key={size} alignItems="center" gap="$3" minHeight="$5">
576
+ <YStack flex={1}>
577
+ <Paragraph fontWeight="600">pokemon</Paragraph>
578
+ <Paragraph color="$color11">1.2.0+34</Paragraph>
579
+ </YStack>
580
+ <Button
581
+ size={size}
582
+ {...shcUpdatePlate}
583
+ icon={ArrowCircleUpIcon}
584
+ aria-label={`Update pokemon (plate ${size})`}
585
+ />
586
+ </XStack>
587
+ ))}
588
+ <XStack alignItems="center" gap="$3" minHeight="$5">
589
+ <YStack flex={1}>
590
+ <Paragraph fontWeight="600">pokemon</Paragraph>
591
+ <Paragraph color="$color11">1.2.0+34</Paragraph>
592
+ </YStack>
593
+ <IconButton accentInk icon={ArrowCircleUpIcon} aria-label="Update pokemon" />
594
+ </XStack>
595
+ </YStack>
596
+ ),
597
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ *
4
+ * LC-176 (FZ-8020): icon buttons side by side share one ground, never a row
5
+ * of separate circles. Clay: "they'd actually group together, like you know
6
+ * how water bubbles all kind of group together."
7
+ */
8
+
9
+ import { cleanup, render, screen } from "@testing-library/react";
10
+ import { createDefaultThemeConfig, Surface } from "@multiplatform.one/theme";
11
+ import type { ReactElement } from "react";
12
+ import { TamaguiProvider } from "tamagui";
13
+ import { afterEach, describe, expect, it, vi } from "vitest";
14
+ import { IconButton, IconButtonGroup } from "./index";
15
+
16
+ const glass = vi.hoisted(() => ({ available: false, containerLoads: 0 }));
17
+ vi.mock("./glass", () => ({
18
+ loadGlassView: () =>
19
+ glass.available ? (props: object) => <div data-glass="" {...props} /> : null,
20
+ loadGlassContainer: () => {
21
+ glass.containerLoads += 1;
22
+ return glass.available
23
+ ? ({ children, spacing }: { children?: unknown; spacing?: number }) => (
24
+ <div data-glass-container={spacing}>{children as never}</div>
25
+ )
26
+ : null;
27
+ },
28
+ }));
29
+
30
+ afterEach(() => {
31
+ cleanup();
32
+ glass.available = false;
33
+ glass.containerLoads = 0;
34
+ });
35
+
36
+ const houseConfig = createDefaultThemeConfig();
37
+
38
+ function Glyph({ size, weight }: { size?: number; weight?: string }) {
39
+ return <svg data-glyph="" data-weight={weight} width={size} height={size} />;
40
+ }
41
+
42
+ function houseRender(ui: ReactElement) {
43
+ return render(
44
+ <TamaguiProvider config={houseConfig.tamagui} defaultTheme="light" disableInjectCSS>
45
+ {ui}
46
+ </TamaguiProvider>,
47
+ );
48
+ }
49
+
50
+ describe("LC-176: IconButtonGroup", () => {
51
+ it("over media, the group is one capsule and its buttons draw their glyphs alone", () => {
52
+ houseRender(
53
+ <>
54
+ <IconButtonGroup plate="media">
55
+ <IconButton plate="media" icon={Glyph} aria-label="Replace" />
56
+ <IconButton plate="media" icon={Glyph} aria-label="Remove" />
57
+ </IconButtonGroup>
58
+ <IconButton plate="media" icon={Glyph} aria-label="Lone" />
59
+ </>,
60
+ );
61
+ const capsules = document.querySelectorAll('[data-mp-icon-button-group="media"]');
62
+ expect(capsules).toHaveLength(1);
63
+ for (const label of ["Replace", "Remove"]) {
64
+ const button = screen.getByLabelText(label);
65
+ expect(capsules[0]!.contains(button)).toBe(true);
66
+ expect(button.getAttribute("data-mp-icon-button")).not.toBe("media");
67
+ expect(button.querySelector("[data-glyph]")!.getAttribute("data-weight")).toBe("bold");
68
+ }
69
+ // A lone button keeps its own circle.
70
+ expect(screen.getByLabelText("Lone").getAttribute("data-mp-icon-button")).toBe("media");
71
+ });
72
+
73
+ it("in a bar with Liquid Glass, the group is one glass capsule and its buttons draw their glyphs alone (FZ-8158)", () => {
74
+ glass.available = true;
75
+ houseRender(
76
+ <Surface bar>
77
+ <IconButtonGroup>
78
+ <IconButton icon={Glyph} aria-label="Archive" />
79
+ <IconButton icon={Glyph} aria-label="Delete" />
80
+ </IconButtonGroup>
81
+ <IconButton icon={Glyph} aria-label="Back" />
82
+ </Surface>,
83
+ );
84
+ const group = document.querySelector('[data-mp-icon-button-group="glass"]')!;
85
+ expect(group).not.toBeNull();
86
+ const capsules = document.querySelectorAll("[data-glass]");
87
+ expect(capsules).toHaveLength(2);
88
+ const capsule = capsules[0]!;
89
+ expect(capsule.parentElement!.contains(group)).toBe(true);
90
+ expect(document.querySelector("[data-glass-container]")).toBeNull();
91
+ for (const label of ["Archive", "Delete"]) {
92
+ const button = screen.getByLabelText(label);
93
+ expect(group.contains(button)).toBe(true);
94
+ expect(button.getAttribute("data-mp-icon-button")).toBe("glyph");
95
+ expect(button.querySelector("[data-glass]")).toBeNull();
96
+ }
97
+ // A lone button keeps its own circle.
98
+ expect(screen.getByLabelText("Back").getAttribute("data-mp-icon-button")).toBe("glass");
99
+ });
100
+
101
+ it("without the glass module the bar group is a bare row", () => {
102
+ houseRender(
103
+ <Surface bar>
104
+ <IconButtonGroup>
105
+ <IconButton icon={Glyph} aria-label="Search" />
106
+ <IconButton icon={Glyph} aria-label="More" />
107
+ </IconButtonGroup>
108
+ </Surface>,
109
+ );
110
+ expect(document.querySelector("[data-glass-container]")).toBeNull();
111
+ expect(document.querySelector('[data-mp-icon-button-group="bar"]')).not.toBeNull();
112
+ expect(screen.getByLabelText("Search").getAttribute("data-mp-icon-button")).toBe("glyph");
113
+ });
114
+
115
+ it("outside a bar it never loads the glass", () => {
116
+ glass.available = true;
117
+ houseRender(
118
+ <IconButtonGroup>
119
+ <IconButton icon={Glyph} aria-label="Copy" />
120
+ </IconButtonGroup>,
121
+ );
122
+ expect(glass.containerLoads).toBe(0);
123
+ expect(document.querySelector("[data-glass]")).toBeNull();
124
+ expect(document.querySelector('[data-mp-icon-button-group="row"]')).not.toBeNull();
125
+ });
126
+ });
@@ -0,0 +1,87 @@
1
+ import { radiusClassProps, useBarSurface } from "@multiplatform.one/theme";
2
+ import { useState, type ReactNode } from "react";
3
+ import { isWeb, Theme, View, XStack, type XStackProps } from "tamagui";
4
+ import { loadGlassView } from "./glass";
5
+ import { MEDIA_PLATE_WEB_STYLE } from "./mediaPlate";
6
+ import { IconButtonGroupContext } from "./iconButtonGroupContext";
7
+
8
+ /**
9
+ * LC-176: how far apart two Liquid Glass circles may sit and still merge. Half
10
+ * the 44pt target, so neighbours on the bar's gap always join.
11
+ */
12
+ export const ICON_BUTTON_GLASS_MERGE_PX = 22;
13
+
14
+ export interface IconButtonGroupProps extends XStackProps {
15
+ /** Over imagery or video: the group shares one material capsule. */
16
+ plate?: "media";
17
+ children?: ReactNode;
18
+ }
19
+
20
+ /**
21
+ * LC-176: icon buttons that sit side by side. Grounds merge, they never stand
22
+ * as a row of separate circles: on iOS 26 the bar's group is one Liquid Glass
23
+ * capsule, as the system draws a bar's item group (merging each circle through
24
+ * the glass container left a waist between them, FZ-8158); over media the
25
+ * group shares one material capsule; a bare row stays bare. A lone button
26
+ * keeps its own circle.
27
+ */
28
+ export function IconButtonGroup({ plate, children, ...rest }: IconButtonGroupProps) {
29
+ const bar = useBarSurface();
30
+ const [capsuleHeight, setCapsuleHeight] = useState(0);
31
+ if (plate === "media") {
32
+ return (
33
+ <IconButtonGroupContext.Provider value="media">
34
+ <Theme name="dark">
35
+ <XStack
36
+ alignItems="center"
37
+ alignSelf="flex-start"
38
+ borderRadius={100000}
39
+ backgroundColor="$shadow4"
40
+ overflow="hidden"
41
+ data-mp-icon-button-group="media"
42
+ {...radiusClassProps("R-IDENTITY", "icon button plate")}
43
+ {...(isWeb ? { style: MEDIA_PLATE_WEB_STYLE } : undefined)}
44
+ {...rest}
45
+ >
46
+ {children}
47
+ </XStack>
48
+ </Theme>
49
+ </IconButtonGroupContext.Provider>
50
+ );
51
+ }
52
+ const GlassView = bar ? loadGlassView() : null;
53
+ const row = (
54
+ <XStack
55
+ alignItems="center"
56
+ data-mp-icon-button-group={GlassView ? "glass" : bar ? "bar" : "row"}
57
+ {...rest}
58
+ >
59
+ {children}
60
+ </XStack>
61
+ );
62
+ if (!GlassView) return row;
63
+ return (
64
+ <IconButtonGroupContext.Provider value="glass">
65
+ <View
66
+ position="relative"
67
+ flexShrink={1}
68
+ minWidth={0}
69
+ onLayout={(event) => setCapsuleHeight(event.nativeEvent.layout.height)}
70
+ >
71
+ <GlassView
72
+ glassEffectStyle="regular"
73
+ pointerEvents="none"
74
+ style={{
75
+ position: "absolute",
76
+ top: 0,
77
+ right: 0,
78
+ bottom: 0,
79
+ left: 0,
80
+ borderRadius: capsuleHeight / 2,
81
+ }}
82
+ />
83
+ {row}
84
+ </View>
85
+ </IconButtonGroupContext.Provider>
86
+ );
87
+ }
@@ -0,0 +1,51 @@
1
+ import type { GlassContainerComponent, GlassViewComponent } from "./glass";
2
+
3
+ export type { GlassContainerComponent, GlassViewComponent } from "./glass";
4
+
5
+ let cachedGlassView: GlassViewComponent | null | undefined;
6
+ let cachedGlassContainer: GlassContainerComponent | null | undefined;
7
+
8
+ /**
9
+ * LC-176: expo-glass-effect's `GlassView`, when this binary links its native
10
+ * module and the OS draws Liquid Glass (iOS 26+); Android never links it. The module is probed through
11
+ * `requireOptionalNativeModule` BEFORE the guarded `require`, so an OTA on a
12
+ * binary built without it, or an older iOS, falls back to the bare tinted
13
+ * glyph instead of throwing mid-render (the `nativePickerLoader.ts` contract).
14
+ */
15
+ export function loadGlassView(): GlassViewComponent | null {
16
+ if (cachedGlassView !== undefined) return cachedGlassView;
17
+ cachedGlassView = null;
18
+ try {
19
+ const core = require("expo-modules-core") as {
20
+ requireOptionalNativeModule?: (moduleName: string) => unknown;
21
+ };
22
+ if (core.requireOptionalNativeModule?.("ExpoGlassEffect") == null) return cachedGlassView;
23
+ const glass = require("expo-glass-effect") as {
24
+ GlassView?: GlassViewComponent;
25
+ GlassContainer?: GlassContainerComponent;
26
+ isLiquidGlassAvailable?: () => boolean;
27
+ isGlassEffectAPIAvailable?: () => boolean;
28
+ };
29
+ if (
30
+ glass.GlassView &&
31
+ glass.isLiquidGlassAvailable?.() &&
32
+ glass.isGlassEffectAPIAvailable?.()
33
+ ) {
34
+ cachedGlassView = glass.GlassView;
35
+ cachedGlassContainer = glass.GlassContainer ?? null;
36
+ }
37
+ } catch {
38
+ cachedGlassView = null;
39
+ }
40
+ return cachedGlassView;
41
+ }
42
+
43
+ /**
44
+ * LC-176: expo-glass-effect's `GlassContainer`, present exactly when
45
+ * `loadGlassView` found Liquid Glass. A binary whose module predates the
46
+ * container keeps separate circles rather than throwing.
47
+ */
48
+ export function loadGlassContainer(): GlassContainerComponent | null {
49
+ if (cachedGlassView === undefined) loadGlassView();
50
+ return cachedGlassContainer ?? null;
51
+ }
@@ -0,0 +1,33 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+
3
+ /** The slice of expo-glass-effect's `GlassView` an icon button draws. */
4
+ export type GlassViewComponent = ComponentType<{
5
+ style?: unknown;
6
+ glassEffectStyle?: "regular" | "clear";
7
+ isInteractive?: boolean;
8
+ pointerEvents?: "none" | "auto" | "box-none" | "box-only";
9
+ }>;
10
+
11
+ /** The slice of expo-glass-effect's `GlassContainer` an icon button group draws. */
12
+ export type GlassContainerComponent = ComponentType<{
13
+ spacing?: number;
14
+ style?: unknown;
15
+ children?: ReactNode;
16
+ }>;
17
+
18
+ /**
19
+ * LC-176: the iOS 26 Liquid Glass material an icon button floats on in a bar.
20
+ * Only iOS has it (`glass.native.ts`; the native module is Apple-only, so Android probes null); web, Android and GNOME draw the bare
21
+ * tinted glyph.
22
+ */
23
+ export function loadGlassView(): GlassViewComponent | null {
24
+ return null;
25
+ }
26
+
27
+ /**
28
+ * LC-176: the container that merges neighbouring Liquid Glass circles into one
29
+ * shape, as iOS 26 groups bar buttons. Only iOS has it.
30
+ */
31
+ export function loadGlassContainer(): GlassContainerComponent | null {
32
+ return null;
33
+ }
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ /**
4
+ * LC-176: the ground an `IconButtonGroup` paints under its buttons. `media`
5
+ * is one shared material capsule and `glass` one shared Liquid Glass capsule,
6
+ * so a button inside draws its bare glyph instead of its own circle.
7
+ */
8
+ export type IconButtonGroupGround = "media" | "glass" | null;
9
+
10
+ export const IconButtonGroupContext = createContext<IconButtonGroupGround>(null);
11
+
12
+ export function useIconButtonGroupGround(): IconButtonGroupGround {
13
+ return useContext(IconButtonGroupContext);
14
+ }