@multiplatform.one/forms 7.68.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 (333) hide show
  1. package/dist/cjs/AdaptivePopup/index.native.js +2 -2
  2. package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
  3. package/dist/cjs/Button/IconButtonGroup.cjs +95 -0
  4. package/dist/cjs/Button/IconButtonGroup.native.js +101 -0
  5. package/dist/cjs/Button/IconButtonGroup.native.js.map +1 -0
  6. package/dist/cjs/Button/glass.cjs +4 -0
  7. package/dist/cjs/Button/glass.native.js +8 -0
  8. package/dist/cjs/Button/glass.native.js.map +1 -1
  9. package/dist/cjs/Button/iconButtonGroupContext.cjs +33 -0
  10. package/dist/cjs/Button/iconButtonGroupContext.native.js +36 -0
  11. package/dist/cjs/Button/iconButtonGroupContext.native.js.map +1 -0
  12. package/dist/cjs/Button/index.cjs +35 -20
  13. package/dist/cjs/Button/index.native.js +38 -22
  14. package/dist/cjs/Button/index.native.js.map +1 -1
  15. package/dist/cjs/Button/mediaPlate.cjs +31 -0
  16. package/dist/cjs/Button/mediaPlate.native.js +34 -0
  17. package/dist/cjs/Button/mediaPlate.native.js.map +1 -0
  18. package/dist/cjs/Chip/index.native.js.map +1 -1
  19. package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js +1 -1
  20. package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
  21. package/dist/cjs/ContextualSaveBar/clientKnownIdentity.native.js.map +1 -1
  22. package/dist/cjs/ContextualSaveBar/commitImmediateField.native.js.map +1 -1
  23. package/dist/cjs/ContextualSaveBar/index.native.js +3 -3
  24. package/dist/cjs/ContextualSaveBar/index.native.js.map +1 -1
  25. package/dist/cjs/ContextualSaveBar/useUnsavedChangesGuard.native.js.map +1 -1
  26. package/dist/cjs/ControlGroup/index.native.js +1 -1
  27. package/dist/cjs/ControlGroup/index.native.js.map +1 -1
  28. package/dist/cjs/DescriptionList/index.native.js +2 -2
  29. package/dist/cjs/DescriptionList/index.native.js.map +1 -1
  30. package/dist/cjs/ErrorSummary.native.js.map +1 -1
  31. package/dist/cjs/FieldDisplay/editMode.native.js.map +1 -1
  32. package/dist/cjs/FieldDisplay/formatters.native.js.map +1 -1
  33. package/dist/cjs/FieldDisplay/index.native.js +1 -1
  34. package/dist/cjs/FieldDisplay/index.native.js.map +1 -1
  35. package/dist/cjs/FieldGroup.native.js.map +1 -1
  36. package/dist/cjs/FloatingPanel/RegularPanel.native.js.map +1 -1
  37. package/dist/cjs/FloatingPanel/compactFace.native.js.map +1 -1
  38. package/dist/cjs/FloatingPanel/containingBlock.native.js.map +1 -1
  39. package/dist/cjs/FloatingPanel/index.native.js +21 -16
  40. package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
  41. package/dist/cjs/FloatingPanel/nestedAction.native.js.map +1 -1
  42. package/dist/cjs/FloatingPanel/pointAnchor.native.js.map +1 -1
  43. package/dist/cjs/FloatingPanel/regularPlacement.native.js.map +1 -1
  44. package/dist/cjs/FloatingPanel/trailingAction.native.js +1 -1
  45. package/dist/cjs/FloatingPanel/trailingAction.native.js.map +1 -1
  46. package/dist/cjs/FloatingPanel/useFloatingPanel.native.js.map +1 -1
  47. package/dist/cjs/Form/index.native.js +1 -1
  48. package/dist/cjs/Form/index.native.js.map +1 -1
  49. package/dist/cjs/Form/submissionController.native.js.map +1 -1
  50. package/dist/cjs/Form/validateOn.native.js.map +1 -1
  51. package/dist/cjs/FormGrid.native.js.map +1 -1
  52. package/dist/cjs/FormSection.native.js.map +1 -1
  53. package/dist/cjs/InputParts/PasswordReveal.native.js.map +1 -1
  54. package/dist/cjs/InputParts/index.native.js.map +1 -1
  55. package/dist/cjs/Label/index.native.js.map +1 -1
  56. package/dist/cjs/MediaPicker/base64.native.js.map +1 -1
  57. package/dist/cjs/MediaPicker/payload.native.js.map +1 -1
  58. package/dist/cjs/MediaPicker/types.native.js.map +1 -1
  59. package/dist/cjs/MediaPicker/useMediaPicker.native.js.map +1 -1
  60. package/dist/cjs/Skeleton/index.native.js.map +1 -1
  61. package/dist/cjs/Spinner/SpinnerGlyph.native.js.map +1 -1
  62. package/dist/cjs/Spinner/spinnerSize.native.js.map +1 -1
  63. package/dist/cjs/Table/index.native.js.map +1 -1
  64. package/dist/cjs/Wheel/haptic.native.js.map +1 -1
  65. package/dist/cjs/Wheel/index.native.js.map +1 -1
  66. package/dist/cjs/Wheel/wheelShared.native.js.map +1 -1
  67. package/dist/cjs/actionLabel.native.js.map +1 -1
  68. package/dist/cjs/fieldDefaults.native.js.map +1 -1
  69. package/dist/cjs/fieldLayout.native.js +1 -1
  70. package/dist/cjs/fieldLayout.native.js.map +1 -1
  71. package/dist/cjs/fields/Barcode/BarcodePreview.native.js.map +1 -1
  72. package/dist/cjs/fields/Barcode/BarcodeScanner.native.js +1 -1
  73. package/dist/cjs/fields/Barcode/code128.native.js.map +1 -1
  74. package/dist/cjs/fields/Barcode/index.native.js +2 -2
  75. package/dist/cjs/fields/Barcode/index.native.js.map +1 -1
  76. package/dist/cjs/fields/Barcode/qr.native.js.map +1 -1
  77. package/dist/cjs/fields/Barcode/scannerTypes.native.js.map +1 -1
  78. package/dist/cjs/fields/Checkbox/Checkbox.native.js +1 -1
  79. package/dist/cjs/fields/Checkbox/Checkbox.native.js.map +1 -1
  80. package/dist/cjs/fields/Checkbox/CheckboxBox.native.js.map +1 -1
  81. package/dist/cjs/fields/Checkbox/CheckboxCard.native.js.map +1 -1
  82. package/dist/cjs/fields/Checkbox/CheckboxFieldFrame.native.js.map +1 -1
  83. package/dist/cjs/fields/Checkbox/CheckboxGroup.native.js +1 -1
  84. package/dist/cjs/fields/Checkbox/CheckboxGroup.native.js.map +1 -1
  85. package/dist/cjs/fields/Checkbox/checkboxGroupContext.native.js.map +1 -1
  86. package/dist/cjs/fields/Checkbox/index.native.js.map +1 -1
  87. package/dist/cjs/fields/ColorPicker/SystemColorField.native.js.map +1 -1
  88. package/dist/cjs/fields/ColorPicker/index.native.js +2 -2
  89. package/dist/cjs/fields/ColorPicker/index.native.js.map +1 -1
  90. package/dist/cjs/fields/ColorPicker/parts.native.js +2 -2
  91. package/dist/cjs/fields/ColorPicker/parts.native.js.map +1 -1
  92. package/dist/cjs/fields/ColorPicker/systemColorWell.gnome.native.js.map +1 -1
  93. package/dist/cjs/fields/Combobox/authorOptions.native.js.map +1 -1
  94. package/dist/cjs/fields/Combobox/index.native.js +5 -5
  95. package/dist/cjs/fields/Combobox/index.native.js.map +1 -1
  96. package/dist/cjs/fields/DatePicker/Calendar.native.js.map +1 -1
  97. package/dist/cjs/fields/DatePicker/DateRangePicker.native.js +2 -2
  98. package/dist/cjs/fields/DatePicker/DateRangePicker.native.js.map +1 -1
  99. package/dist/cjs/fields/DatePicker/DatetimePicker.catalog.native.js +3 -3
  100. package/dist/cjs/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
  101. package/dist/cjs/fields/DatePicker/DatetimePicker.native.js +2 -2
  102. package/dist/cjs/fields/DatePicker/MonthPicker.native.js +2 -2
  103. package/dist/cjs/fields/DatePicker/MonthPicker.native.js.map +1 -1
  104. package/dist/cjs/fields/DatePicker/MultiDatePicker.native.js +2 -2
  105. package/dist/cjs/fields/DatePicker/MultiDatePicker.native.js.map +1 -1
  106. package/dist/cjs/fields/DatePicker/catalog.native.js +2 -2
  107. package/dist/cjs/fields/DatePicker/catalog.native.js.map +1 -1
  108. package/dist/cjs/fields/DatePicker/fieldValueSync.native.js.map +1 -1
  109. package/dist/cjs/fields/DatePicker/index.native.js +2 -2
  110. package/dist/cjs/fields/DatePicker/parts.native.js +2 -2
  111. package/dist/cjs/fields/DatePicker/parts.native.js.map +1 -1
  112. package/dist/cjs/fields/DatePicker/utils.native.js.map +1 -1
  113. package/dist/cjs/fields/Duration/durationMath.native.js.map +1 -1
  114. package/dist/cjs/fields/Duration/index.native.js +3 -3
  115. package/dist/cjs/fields/Duration/index.native.js.map +1 -1
  116. package/dist/cjs/fields/FileUpload/filePickerTypes.native.js.map +1 -1
  117. package/dist/cjs/fields/FileUpload/index.cjs +4 -1
  118. package/dist/cjs/fields/FileUpload/index.native.js +6 -3
  119. package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
  120. package/dist/cjs/fields/FileUpload/nativePickerLoader.native.js.map +1 -1
  121. package/dist/cjs/fields/Geolocation/expoMapsLoader.native.js.map +1 -1
  122. package/dist/cjs/fields/Geolocation/geoValue.native.js.map +1 -1
  123. package/dist/cjs/fields/Geolocation/geocoder.native.js.map +1 -1
  124. package/dist/cjs/fields/Geolocation/geocoderContext.native.js.map +1 -1
  125. package/dist/cjs/fields/Geolocation/index.native.js +5 -5
  126. package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
  127. package/dist/cjs/fields/Geolocation/mapMath.native.js.map +1 -1
  128. package/dist/cjs/fields/Geolocation/mapTypes.native.js.map +1 -1
  129. package/dist/cjs/fields/Geolocation/nativeSearchFocus.native.js.map +1 -1
  130. package/dist/cjs/fields/Geolocation/photonGeocoder.native.js.map +1 -1
  131. package/dist/cjs/fields/Icon/drawnSet.native.js.map +1 -1
  132. package/dist/cjs/fields/Icon/index.native.js +3 -3
  133. package/dist/cjs/fields/Icon/index.native.js.map +1 -1
  134. package/dist/cjs/fields/ImageField/index.native.js +2 -2
  135. package/dist/cjs/fields/ImageField/index.native.js.map +1 -1
  136. package/dist/cjs/fields/Input/index.native.js +3 -3
  137. package/dist/cjs/fields/Input/index.native.js.map +1 -1
  138. package/dist/cjs/fields/MentionInput/index.native.js +3 -3
  139. package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
  140. package/dist/cjs/fields/OTPInput/index.native.js +3 -3
  141. package/dist/cjs/fields/OTPInput/index.native.js.map +1 -1
  142. package/dist/cjs/fields/OTPInput/otpCellCss.native.js.map +1 -1
  143. package/dist/cjs/fields/PhoneInput/dialCodes.native.js.map +1 -1
  144. package/dist/cjs/fields/PhoneInput/index.native.js +3 -3
  145. package/dist/cjs/fields/PhoneInput/index.native.js.map +1 -1
  146. package/dist/cjs/fields/PhoneInput/phone.native.js.map +1 -1
  147. package/dist/cjs/fields/PhoneInput/regionNames.native.js.map +1 -1
  148. package/dist/cjs/fields/Progress/index.native.js +1 -1
  149. package/dist/cjs/fields/Progress/index.native.js.map +1 -1
  150. package/dist/cjs/fields/RadioGroup/index.native.js +1 -1
  151. package/dist/cjs/fields/RadioGroup/index.native.js.map +1 -1
  152. package/dist/cjs/fields/Rating/index.native.js +1 -1
  153. package/dist/cjs/fields/Rating/index.native.js.map +1 -1
  154. package/dist/cjs/fields/Rating/ratingMath.native.js.map +1 -1
  155. package/dist/cjs/fields/Rating/types.native.js.map +1 -1
  156. package/dist/cjs/fields/SearchInput/index.native.js +2 -2
  157. package/dist/cjs/fields/SearchInput/index.native.js.map +1 -1
  158. package/dist/cjs/fields/Select/InlineChoices.native.js.map +1 -1
  159. package/dist/cjs/fields/Select/SheetPickerRow.native.js.map +1 -1
  160. package/dist/cjs/fields/Select/index.native.js +4 -4
  161. package/dist/cjs/fields/Select/index.native.js.map +1 -1
  162. package/dist/cjs/fields/Select/shouldInlineChoices.native.js.map +1 -1
  163. package/dist/cjs/fields/Signature/index.native.js +2 -2
  164. package/dist/cjs/fields/Signature/index.native.js.map +1 -1
  165. package/dist/cjs/fields/Slider/index.native.js +1 -1
  166. package/dist/cjs/fields/Slider/index.native.js.map +1 -1
  167. package/dist/cjs/fields/Slider/jumpMotion.native.js.map +1 -1
  168. package/dist/cjs/fields/Stepper/index.native.js +2 -2
  169. package/dist/cjs/fields/Stepper/index.native.js.map +1 -1
  170. package/dist/cjs/fields/Stepper/resolveStepperPlacement.native.js.map +1 -1
  171. package/dist/cjs/fields/Switch/footprint.native.js.map +1 -1
  172. package/dist/cjs/fields/Switch/index.native.js +2 -2
  173. package/dist/cjs/fields/Switch/index.native.js.map +1 -1
  174. package/dist/cjs/fields/TextArea/index.native.js +2 -2
  175. package/dist/cjs/fields/TextArea/index.native.js.map +1 -1
  176. package/dist/cjs/fields/TimePicker/catalog.native.js +3 -3
  177. package/dist/cjs/fields/TimePicker/catalog.native.js.map +1 -1
  178. package/dist/cjs/fields/TimePicker/index.native.js +2 -2
  179. package/dist/cjs/fields/TimePicker/parts.native.js +1 -1
  180. package/dist/cjs/fields/TimePicker/parts.native.js.map +1 -1
  181. package/dist/cjs/fields/TimeRangeScrubber/geometry.native.js.map +1 -1
  182. package/dist/cjs/fields/TimeRangeScrubber/index.native.js +2 -2
  183. package/dist/cjs/fields/TimeRangeScrubber/index.native.js.map +1 -1
  184. package/dist/cjs/fields/ToggleGroup/index.native.js +1 -1
  185. package/dist/cjs/fields/ToggleGroup/index.native.js.map +1 -1
  186. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js +1 -1
  187. package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
  188. package/dist/cjs/fields/nativeDateTime/dateTimePickerLoader.native.js.map +1 -1
  189. package/dist/cjs/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
  190. package/dist/cjs/fields/nativeDateTime/sheetKeyboard.native.js.map +1 -1
  191. package/dist/cjs/fields/nativeSelect.native.js.map +1 -1
  192. package/dist/cjs/fields/selectedOrder.native.js.map +1 -1
  193. package/dist/cjs/formSpacing.native.js.map +1 -1
  194. package/dist/cjs/hooks/useFocusManagement.native.js.map +1 -1
  195. package/dist/cjs/index.cjs +3 -0
  196. package/dist/cjs/index.native.js +48 -45
  197. package/dist/cjs/index.native.js.map +1 -1
  198. package/dist/cjs/keyboard/KeyboardDoneBar.native.js +1 -1
  199. package/dist/cjs/keyboard/chain.native.js.map +1 -1
  200. package/dist/cjs/keyboard/index.native.js.map +1 -1
  201. package/dist/cjs/keyboard/types.native.js.map +1 -1
  202. package/dist/cjs/requiredMarking.native.js.map +1 -1
  203. package/dist/cjs/shared/TimeColumn.native.js +1 -1
  204. package/dist/cjs/shared/TimeColumn.native.js.map +1 -1
  205. package/dist/cjs/shared/capabilityPreflight.native.js.map +1 -1
  206. package/dist/cjs/shared/choiceRowPress.native.js.map +1 -1
  207. package/dist/cjs/shared/colorRamps.native.js.map +1 -1
  208. package/dist/cjs/shared/devWarn.native.js.map +1 -1
  209. package/dist/cjs/shared/floatingList.native.js +1 -1
  210. package/dist/cjs/shared/floatingList.native.js.map +1 -1
  211. package/dist/cjs/shared/focusManagement.native.js.map +1 -1
  212. package/dist/cjs/shared/groupContext.native.js.map +1 -1
  213. package/dist/cjs/shared/i18n.native.js.map +1 -1
  214. package/dist/cjs/shared/instantMotion.native.js.map +1 -1
  215. package/dist/cjs/shared/nativeA11y.native.js.map +1 -1
  216. package/dist/cjs/shared/nativeAutoFocus.native.js.map +1 -1
  217. package/dist/cjs/shared/overlaySizeClass.native.js.map +1 -1
  218. package/dist/cjs/shared/overlayStack.native.js.map +1 -1
  219. package/dist/cjs/shared/panelPortal.native.js.map +1 -1
  220. package/dist/cjs/shared/pickerTriggerContract.native.js.map +1 -1
  221. package/dist/cjs/shared/pressSlopProps.native.js.map +1 -1
  222. package/dist/cjs/shared/restoreRetries.native.js.map +1 -1
  223. package/dist/cjs/shared/sheetNavigation.native.js.map +1 -1
  224. package/dist/cjs/shared/systemControlModules.gnome.native.js.map +1 -1
  225. package/dist/cjs/shared/systemControls.native.js.map +1 -1
  226. package/dist/cjs/shared/t.native.js.map +1 -1
  227. package/dist/cjs/shared/tableCellContext.native.js.map +1 -1
  228. package/dist/cjs/shared/touchWebPicker.native.js.map +1 -1
  229. package/dist/cjs/shared/useDragScroll.native.js.map +1 -1
  230. package/dist/cjs/shared/useKeyboardInset.native.js.map +1 -1
  231. package/dist/cjs/shared/useSheetEscapeDismiss.native.js.map +1 -1
  232. package/dist/cjs/shared/useWheelIncrement.native.js.map +1 -1
  233. package/dist/cjs/shared/utils.native.js +1 -1
  234. package/dist/cjs/shared/utils.native.js.map +1 -1
  235. package/dist/cjs/shared/zIndex.native.js.map +1 -1
  236. package/dist/cjs/standardActions.native.js.map +1 -1
  237. package/dist/cjs/themedPrimitives.native.js.map +1 -1
  238. package/dist/cjs/types.native.js.map +1 -1
  239. package/dist/cjs/utils/LinkCell.native.js.map +1 -1
  240. package/dist/esm/Button/IconButtonGroup.mjs +69 -0
  241. package/dist/esm/Button/IconButtonGroup.mjs.map +1 -0
  242. package/dist/esm/Button/IconButtonGroup.native.js +72 -0
  243. package/dist/esm/Button/IconButtonGroup.native.js.map +1 -0
  244. package/dist/esm/Button/glass.mjs +4 -1
  245. package/dist/esm/Button/glass.mjs.map +1 -1
  246. package/dist/esm/Button/glass.native.js +8 -1
  247. package/dist/esm/Button/glass.native.js.map +1 -1
  248. package/dist/esm/Button/iconButtonGroupContext.mjs +7 -0
  249. package/dist/esm/Button/iconButtonGroupContext.mjs.map +1 -0
  250. package/dist/esm/Button/iconButtonGroupContext.native.js +7 -0
  251. package/dist/esm/Button/iconButtonGroupContext.native.js.map +1 -0
  252. package/dist/esm/Button/index.mjs +32 -21
  253. package/dist/esm/Button/index.mjs.map +1 -1
  254. package/dist/esm/Button/index.native.js +33 -21
  255. package/dist/esm/Button/index.native.js.map +1 -1
  256. package/dist/esm/Button/mediaPlate.mjs +6 -0
  257. package/dist/esm/Button/mediaPlate.mjs.map +1 -0
  258. package/dist/esm/Button/mediaPlate.native.js +6 -0
  259. package/dist/esm/Button/mediaPlate.native.js.map +1 -0
  260. package/dist/esm/FloatingPanel/index.native.js +22 -17
  261. package/dist/esm/FloatingPanel/index.native.js.map +1 -1
  262. package/dist/esm/fields/FileUpload/index.mjs +5 -2
  263. package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
  264. package/dist/esm/fields/FileUpload/index.native.js +5 -2
  265. package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
  266. package/dist/esm/index.mjs +2 -2
  267. package/dist/esm/index.mjs.map +1 -1
  268. package/dist/esm/index.native.js +2 -2
  269. package/dist/esm/index.native.js.map +1 -1
  270. package/dist/jsx/Button/IconButtonGroup.mjs +69 -0
  271. package/dist/jsx/Button/IconButtonGroup.mjs.map +1 -0
  272. package/dist/jsx/Button/IconButtonGroup.native.js +101 -0
  273. package/dist/jsx/Button/IconButtonGroup.native.js.map +1 -0
  274. package/dist/jsx/Button/glass.mjs +4 -1
  275. package/dist/jsx/Button/glass.mjs.map +1 -1
  276. package/dist/jsx/Button/glass.native.js +8 -0
  277. package/dist/jsx/Button/glass.native.js.map +1 -1
  278. package/dist/jsx/Button/iconButtonGroupContext.mjs +7 -0
  279. package/dist/jsx/Button/iconButtonGroupContext.mjs.map +1 -0
  280. package/dist/jsx/Button/iconButtonGroupContext.native.js +36 -0
  281. package/dist/jsx/Button/iconButtonGroupContext.native.js.map +1 -0
  282. package/dist/jsx/Button/index.mjs +32 -21
  283. package/dist/jsx/Button/index.mjs.map +1 -1
  284. package/dist/jsx/Button/index.native.js +36 -20
  285. package/dist/jsx/Button/index.native.js.map +1 -1
  286. package/dist/jsx/Button/mediaPlate.mjs +6 -0
  287. package/dist/jsx/Button/mediaPlate.mjs.map +1 -0
  288. package/dist/jsx/Button/mediaPlate.native.js +34 -0
  289. package/dist/jsx/Button/mediaPlate.native.js.map +1 -0
  290. package/dist/jsx/FloatingPanel/index.native.js +21 -16
  291. package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
  292. package/dist/jsx/fields/FileUpload/index.mjs +5 -2
  293. package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
  294. package/dist/jsx/fields/FileUpload/index.native.js +4 -1
  295. package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
  296. package/dist/jsx/index.js +2 -2
  297. package/dist/jsx/index.js.map +1 -1
  298. package/dist/jsx/index.mjs +2 -2
  299. package/dist/jsx/index.mjs.map +1 -1
  300. package/dist/jsx/index.native.js +3 -0
  301. package/dist/jsx/index.native.js.map +1 -1
  302. package/package.json +8 -8
  303. package/src/Button/Button.iconButton.spec.tsx +76 -14
  304. package/src/Button/Button.stories.tsx +142 -21
  305. package/src/Button/IconButtonGroup.spec.tsx +126 -0
  306. package/src/Button/IconButtonGroup.tsx +87 -0
  307. package/src/Button/glass.native.ts +15 -2
  308. package/src/Button/glass.ts +16 -1
  309. package/src/Button/iconButtonGroupContext.ts +14 -0
  310. package/src/Button/index.tsx +82 -42
  311. package/src/Button/mediaPlate.ts +9 -0
  312. package/src/FloatingPanel/FloatingPanel.pointMenu.native.spec.tsx +69 -0
  313. package/src/FloatingPanel/index.native.tsx +25 -4
  314. package/src/fields/FileUpload/FileUpload.spec.tsx +11 -7
  315. package/src/fields/FileUpload/index.tsx +4 -2
  316. package/src/index.ts +9 -1
  317. package/types/Button/IconButtonGroup.d.ts +22 -0
  318. package/types/Button/IconButtonGroup.d.ts.map +1 -0
  319. package/types/Button/glass.d.ts +12 -1
  320. package/types/Button/glass.d.ts.map +1 -1
  321. package/types/Button/glass.native.d.ts +8 -2
  322. package/types/Button/glass.native.d.ts.map +1 -1
  323. package/types/Button/iconButtonGroupContext.d.ts +9 -0
  324. package/types/Button/iconButtonGroupContext.d.ts.map +1 -0
  325. package/types/Button/index.d.ts +18 -9
  326. package/types/Button/index.d.ts.map +1 -1
  327. package/types/Button/mediaPlate.d.ts +10 -0
  328. package/types/Button/mediaPlate.d.ts.map +1 -0
  329. package/types/FloatingPanel/index.native.d.ts +2 -1
  330. package/types/FloatingPanel/index.native.d.ts.map +1 -1
  331. package/types/fields/FileUpload/index.d.ts.map +1 -1
  332. package/types/index.d.ts +2 -2
  333. package/types/index.d.ts.map +1 -1
@@ -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
+ }
@@ -1,8 +1,9 @@
1
- import type { GlassViewComponent } from "./glass";
1
+ import type { GlassContainerComponent, GlassViewComponent } from "./glass";
2
2
 
3
- export type { GlassViewComponent } from "./glass";
3
+ export type { GlassContainerComponent, GlassViewComponent } from "./glass";
4
4
 
5
5
  let cachedGlassView: GlassViewComponent | null | undefined;
6
+ let cachedGlassContainer: GlassContainerComponent | null | undefined;
6
7
 
7
8
  /**
8
9
  * LC-176: expo-glass-effect's `GlassView`, when this binary links its native
@@ -21,6 +22,7 @@ export function loadGlassView(): GlassViewComponent | null {
21
22
  if (core.requireOptionalNativeModule?.("ExpoGlassEffect") == null) return cachedGlassView;
22
23
  const glass = require("expo-glass-effect") as {
23
24
  GlassView?: GlassViewComponent;
25
+ GlassContainer?: GlassContainerComponent;
24
26
  isLiquidGlassAvailable?: () => boolean;
25
27
  isGlassEffectAPIAvailable?: () => boolean;
26
28
  };
@@ -30,9 +32,20 @@ export function loadGlassView(): GlassViewComponent | null {
30
32
  glass.isGlassEffectAPIAvailable?.()
31
33
  ) {
32
34
  cachedGlassView = glass.GlassView;
35
+ cachedGlassContainer = glass.GlassContainer ?? null;
33
36
  }
34
37
  } catch {
35
38
  cachedGlassView = null;
36
39
  }
37
40
  return cachedGlassView;
38
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
+ }
@@ -1,4 +1,4 @@
1
- import type { ComponentType } from "react";
1
+ import type { ComponentType, ReactNode } from "react";
2
2
 
3
3
  /** The slice of expo-glass-effect's `GlassView` an icon button draws. */
4
4
  export type GlassViewComponent = ComponentType<{
@@ -8,6 +8,13 @@ export type GlassViewComponent = ComponentType<{
8
8
  pointerEvents?: "none" | "auto" | "box-none" | "box-only";
9
9
  }>;
10
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
+
11
18
  /**
12
19
  * LC-176: the iOS 26 Liquid Glass material an icon button floats on in a bar.
13
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
@@ -16,3 +23,11 @@ export type GlassViewComponent = ComponentType<{
16
23
  export function loadGlassView(): GlassViewComponent | null {
17
24
  return null;
18
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
+ }
@@ -73,6 +73,8 @@ import { formButtonColors, formCommonColors } from "../shared/colorRamps";
73
73
  import { warnBareDisabled, warnPositiveTabIndex, warnSizeRecipeEscape } from "../shared/devWarn";
74
74
  import { useControlGrouped } from "../shared/groupContext";
75
75
  import { loadGlassView } from "./glass";
76
+ import { MEDIA_PLATE_WEB_STYLE } from "./mediaPlate";
77
+ import { useIconButtonGroupGround } from "./iconButtonGroupContext";
76
78
 
77
79
  /**
78
80
  * Coordinated size recipe (Polaris slim/medium/large, Primer s/m/l, Linear
@@ -409,17 +411,36 @@ export interface ButtonProps extends Omit<
409
411
  export const ICON_BUTTON_PRESS_OPACITY = 0.35;
410
412
 
411
413
  /**
412
- * LC-176: a bar or row icon button's glyph floor (HIG 22-24pt in a 44pt
413
- * target). A bare glyph carries its own weight, so it never shrinks to the
414
- * label-icon size of a small control.
414
+ * LC-176: an icon button's glyph floor. The glyph itself is sized to its row
415
+ * (`iconButtonGlyphPx`), so it is never smaller than a 44pt target's glyph.
415
416
  */
416
- export const ICON_BUTTON_GLYPH_PX = 22;
417
+ export const ICON_BUTTON_GLYPH_PX = 24;
418
+
419
+ /** LC-176: the content glyph's floor; the same bold, row-sized glyph. */
420
+ export const ICON_BUTTON_NEUTRAL_GLYPH_PX = 24;
417
421
 
418
422
  /**
419
- * LC-176: a neutral icon button (content, no intent: no tint to carry it) is
420
- * a bold monochrome glyph at 24pt or more, the Instagram / Threads weight.
423
+ * Phosphor draws a glyph's ink across about 208 of its 256 units, so a glyph
424
+ * whose ink is as tall as a text line is that line over this ratio.
421
425
  */
422
- export const ICON_BUTTON_NEUTRAL_GLYPH_PX = 24;
426
+ const PHOSPHOR_INK_RATIO = 208 / 256;
427
+
428
+ /**
429
+ * LC-176: an icon button's glyph is as tall in ink as the row's text line
430
+ * (YouTube's and the Phone app's bar glyphs), never a small glyph in a big
431
+ * target.
432
+ */
433
+ export function iconButtonGlyphPx(rowLinePx: number | undefined): number {
434
+ if (rowLinePx === undefined) return ICON_BUTTON_GLYPH_PX;
435
+ return Math.max(ICON_BUTTON_GLYPH_PX, Math.round(rowLinePx / PHOSPHOR_INK_RATIO));
436
+ }
437
+
438
+ /**
439
+ * LC-176: a glyph on a circle the caller asked for (`circular`, `outlined`)
440
+ * fills this share of the circle, the iOS Settings and Contacts leading-icon
441
+ * ratio, never a small glyph lost in a big plate.
442
+ */
443
+ export const ICON_BUTTON_CIRCLE_GLYPH_RATIO = 0.58;
423
444
 
424
445
  /** LC-176: an inline glyph is the text's size; Phosphor's 256 grid pads ~12%. */
425
446
  const INLINE_GLYPH_RATIO = 1.2;
@@ -465,7 +486,7 @@ export function Button({
465
486
  iconAfter,
466
487
  selected,
467
488
  nested = false,
468
- circular,
489
+ circular: circularProp,
469
490
  chromeless,
470
491
  edge,
471
492
  accentInk,
@@ -556,7 +577,12 @@ export function Button({
556
577
  // (`outlined`, `circular`, `chromeless={false}`) or a segment owns it.
557
578
  const labelled = verb != null || (children != null && children !== false && children !== "");
558
579
  const iconOnly = !labelled && Boolean(icon || iconAfter);
559
- const mediaPlate = iconOnly && plate === "media";
580
+ // LC-176: `circular chromeless` paints no plate, so an icon-only one is an
581
+ // icon button like any other (the SHC console's add and copy buttons).
582
+ const circular = circularProp && !(iconOnly && chromeless === true && plate == null);
583
+ // LC-176: inside a group's shared capsule a button draws its glyph alone.
584
+ const groupGround = useIconButtonGroupGround();
585
+ const mediaPlate = iconOnly && plate === "media" && groupGround !== "media";
560
586
  const confirmPlate = iconOnly && primaryPlate;
561
587
  const iconButton =
562
588
  iconOnly &&
@@ -575,12 +601,16 @@ export function Button({
575
601
  // LC-176: the context picks the icon button's treatment, never the app. In a
576
602
  // bar it takes the bar tint (the accent, on Liquid Glass where iOS draws
577
603
  // it); in content, with no intent, it is a bold glyph in the text colour.
578
- const neutralGlyph = iconButton && !(theme ?? intent) && !barSurface && accentInk == null;
604
+ // An action inline with a value (copy beside an address) is tinted like a
605
+ // link, so it reads as the one pressable thing on the line.
606
+ const neutralGlyph =
607
+ iconButton && !(theme ?? intent) && !barSurface && accentInk == null && inline !== true;
579
608
  if (iconButton) {
580
609
  chromeless = true;
581
- accentInk = accentInk ?? (!(theme ?? intent) && barSurface);
610
+ accentInk = accentInk ?? (!(theme ?? intent) && (barSurface || inline === true));
582
611
  }
583
- const resolvedGlyphWeight = glyphWeight ?? (neutralGlyph ? "bold" : "regular");
612
+ // LC-176: a glyph that is the whole button is bold, so it reads as one.
613
+ const resolvedGlyphWeight = glyphWeight ?? (iconOnly ? "bold" : "regular");
584
614
  const elevatedGround = useElevatedSurface();
585
615
  const darkScheme = isDarkThemeName(useThemeName());
586
616
  const resolvedSize = typeof sizeProp === "number" ? sizeProp : knobProps.sizeToken;
@@ -773,34 +803,43 @@ export function Button({
773
803
  const barGlyph =
774
804
  ((iconButton || mediaPlate || confirmPlate) && !nested) ||
775
805
  (labelled && chromeless && accentInk === true && edge === "start");
776
- const glyphFloor =
777
- neutralGlyph && !inlineGlyph ? ICON_BUTTON_NEUTRAL_GLYPH_PX : ICON_BUTTON_GLYPH_PX;
806
+ const labelLine = labelLinePx(knobProps.body.fontFamily);
807
+ const glyphFloor = iconButtonGlyphPx(labelLine);
778
808
  // LC-176: on iOS 26 a bar's icon button floats on Liquid Glass, the native
779
809
  // bar button. Elsewhere, and on a binary without the module, it stays the
780
810
  // bare tinted glyph.
811
+ const glassGroup = groupGround === "glass";
781
812
  const GlassView =
782
- iconButton && !inlineGlyph && !nested && accentInk && barSurface && !selectedFill
813
+ iconButton && !inlineGlyph && !nested && accentInk && barSurface && !selectedFill && !glassGroup
783
814
  ? loadGlassView()
784
815
  : null;
816
+ const framedCircle = iconOnly && Boolean(circular) && !nested && !mediaPlate && !confirmPlate;
817
+ const framedCirclePx =
818
+ touch && typeof sizeProp !== "number"
819
+ ? Math.max(sizeTokenPx(resolvedSize), MIN_PRESS_TARGET)
820
+ : sizeTokenPx(resolvedSize);
785
821
  const iconSize = nested
786
822
  ? Math.min(recipe.iconSize, Math.max(12, Math.round(knobProps.nestedControl.px * 0.45)))
787
- : inlineGlyph
788
- ? Math.round(recipe.fontSize * INLINE_GLYPH_RATIO)
789
- : barGlyph
790
- ? touch
791
- ? Math.max(glyphFloor, resolveButtonSizeRecipe("$6", recipeFamily, touch).iconSize)
792
- : // A pointer toolbar keeps its token box (LC-54); the glyph fills it
793
- // up to the same floor.
794
- Math.max(recipe.iconSize, Math.min(glyphFloor, recipe.height - 8))
795
- : recipe.iconSize;
823
+ : framedCircle
824
+ ? Math.round(framedCirclePx * ICON_BUTTON_CIRCLE_GLYPH_RATIO)
825
+ : inlineGlyph
826
+ ? Math.round(recipe.fontSize * INLINE_GLYPH_RATIO)
827
+ : barGlyph
828
+ ? touch
829
+ ? Math.max(glyphFloor, resolveButtonSizeRecipe("$6", recipeFamily, touch).iconSize)
830
+ : // A pointer toolbar keeps its token box (LC-54); the glyph fills it
831
+ // up to the same floor.
832
+ Math.max(recipe.iconSize, Math.min(glyphFloor, recipe.height - 8))
833
+ : recipe.iconSize;
796
834
  const glyph =
797
- barGlyph || inlineGlyph ? withGlyphWeight(icon, iconSize, resolvedGlyphWeight) : icon;
835
+ barGlyph || inlineGlyph || framedCircle
836
+ ? withGlyphWeight(icon, iconSize, resolvedGlyphWeight)
837
+ : icon;
798
838
 
799
839
  // LC-158: the frame keeps its recipe height under Dynamic Type, so the
800
840
  // label grows only as far as that height holds: its em and its line, which
801
841
  // React Native scales by the same multiplier.
802
842
  const labelBoxPx = nested ? knobProps.nestedControl.px : recipe.height;
803
- const labelLine = labelLinePx(knobProps.body.fontFamily);
804
843
  const labelMaxFontScale = Math.min(
805
844
  maxFontScaleToFit(
806
845
  labelBoxPx,
@@ -879,7 +918,7 @@ export function Button({
879
918
  </View>
880
919
  ) : null;
881
920
  const glyphAfter =
882
- iconAfter && (barGlyph || inlineGlyph)
921
+ iconAfter && (barGlyph || inlineGlyph || framedCircle)
883
922
  ? withGlyphWeight(iconAfter, iconSize, resolvedGlyphWeight)
884
923
  : iconAfter;
885
924
  const iconAfterElement = glyphAfter ? (
@@ -994,11 +1033,8 @@ export function Button({
994
1033
  : undefined;
995
1034
  // A square box, so an `edge` pull puts the glyph itself on the gutter.
996
1035
  // On glass the circle, not the glyph, sits on the margin.
997
- const glyphEdgePull = GlassView
998
- ? 0
999
- : iconButton
1000
- ? -Math.max(0, (visualPx - iconSize) / 2)
1001
- : edgePull;
1036
+ const glyphEdgePull =
1037
+ GlassView || glassGroup ? 0 : iconButton ? -Math.max(0, (visualPx - iconSize) / 2) : edgePull;
1002
1038
  const buttonElement = (
1003
1039
  <ButtonFrame
1004
1040
  // DG-ST-03: an explained disable is aria-disabled (focusable, press
@@ -1046,20 +1082,14 @@ export function Button({
1046
1082
  ? { width: visualPx, minWidth: visualPx, paddingHorizontal: 0 }
1047
1083
  : undefined)}
1048
1084
  {...(inlineGlyph ? { marginInlineStart: "$2" as const } : undefined)}
1049
- {...(GlassView ? { borderRadius: 100000, overflow: "hidden" as const } : undefined)}
1085
+ // The glass rounds itself, unclipped.
1086
+ {...(GlassView ? { borderRadius: 100000 } : undefined)}
1050
1087
  {...(confirmPlate ? { borderRadius: 100000 } : undefined)}
1051
1088
  {...(mediaPlate
1052
1089
  ? {
1053
1090
  borderRadius: 100000,
1054
1091
  backgroundColor: "$shadow4",
1055
- ...(isWeb
1056
- ? {
1057
- style: {
1058
- backdropFilter: "blur(16px) saturate(180%)",
1059
- WebkitBackdropFilter: "blur(16px) saturate(180%)",
1060
- },
1061
- }
1062
- : undefined),
1092
+ ...(isWeb ? { style: MEDIA_PLATE_WEB_STYLE } : undefined),
1063
1093
  }
1064
1094
  : undefined)}
1065
1095
  {...(edge && !inlineGlyph && chromeless && !selectedFill && !isCircular
@@ -1221,7 +1251,14 @@ export function Button({
1221
1251
  glassEffectStyle="regular"
1222
1252
  isInteractive
1223
1253
  pointerEvents="none"
1224
- style={{ position: "absolute", top: 0, right: 0, bottom: 0, left: 0 }}
1254
+ style={{
1255
+ position: "absolute",
1256
+ top: 0,
1257
+ right: 0,
1258
+ bottom: 0,
1259
+ left: 0,
1260
+ borderRadius: visualPx / 2,
1261
+ }}
1225
1262
  />
1226
1263
  ) : null}
1227
1264
  {iconElement}
@@ -1292,3 +1329,6 @@ export interface IconButtonProps extends Omit<
1292
1329
  export function IconButton(props: IconButtonProps) {
1293
1330
  return <Button {...props} />;
1294
1331
  }
1332
+
1333
+ export { IconButtonGroup, ICON_BUTTON_GLASS_MERGE_PX } from "./IconButtonGroup";
1334
+ export type { IconButtonGroupProps } from "./IconButtonGroup";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * LC-176: the media plate's material on web, where a blur behind the plate is
3
+ * the platform's material; native keeps the translucent ground alone. One
4
+ * copy, shared by a lone plate and a group's capsule.
5
+ */
6
+ export const MEDIA_PLATE_WEB_STYLE = {
7
+ backdropFilter: "blur(16px) saturate(180%)",
8
+ WebkitBackdropFilter: "blur(16px) saturate(180%)",
9
+ } as const;
@@ -124,3 +124,72 @@ describe("FloatingPanel native ContextMenu at the long-press point", () => {
124
124
  expect(sheet()).toBeNull();
125
125
  });
126
126
  });
127
+
128
+ function Glyph(_: { onPress: () => void; "aria-label": string }) {
129
+ return <Text>◐</Text>;
130
+ }
131
+
132
+ function GlyphPanel({ onPress, anchored = false }: { onPress: () => void; anchored?: boolean }) {
133
+ const [open, setOpen] = useState(false);
134
+ return (
135
+ <FloatingPanel
136
+ open={open}
137
+ onOpenChange={setOpen}
138
+ openOn="contextmenu"
139
+ triggerSizing="content"
140
+ {...(anchored
141
+ ? { compactFace: "menu" as const, anchorMode: "attach" as const, triggerEcho: false }
142
+ : null)}
143
+ trigger={<Glyph onPress={onPress} aria-label="Appearance" />}
144
+ >
145
+ <Text>Dark</Text>
146
+ </FloatingPanel>
147
+ );
148
+ }
149
+
150
+ function tap(target: Element) {
151
+ act(() => {
152
+ target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0 }));
153
+ vi.advanceTimersByTime(100);
154
+ target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, button: 0 }));
155
+ target.dispatchEvent(new MouseEvent("click", { bubbles: true, button: 0 }));
156
+ });
157
+ }
158
+
159
+ describe("FloatingPanel native ContextMenu keeps its trigger's tap (FZ-8158)", () => {
160
+ it("runs the trigger's own onPress on a tap and names the button after it", () => {
161
+ screen.width = 390;
162
+ screen.height = 844;
163
+ const onPress = vi.fn();
164
+ const result = renderWithProviders(<GlyphPanel onPress={onPress} />);
165
+ tap(result.getByRole("button", { name: "Appearance" }));
166
+ expect(onPress).toHaveBeenCalledTimes(1);
167
+ expect(sheet()).toBeNull();
168
+ });
169
+
170
+ it("opens the menu on a long-press without running the tap", () => {
171
+ screen.width = 390;
172
+ screen.height = 844;
173
+ const onPress = vi.fn();
174
+ const result = renderWithProviders(<GlyphPanel onPress={onPress} />);
175
+ longPress(result.getByRole("button", { name: "Appearance" }), 200, 300);
176
+ expect(sheet()).not.toBeNull();
177
+ expect(onPress).not.toHaveBeenCalled();
178
+ });
179
+
180
+ it("opens as the pull-down under its trigger on a phone when menu-faced, never a sheet (FZ-8158 r5)", () => {
181
+ screen.width = 390;
182
+ screen.height = 844;
183
+ const onPress = vi.fn();
184
+ const result = renderWithProviders(<GlyphPanel onPress={onPress} anchored />);
185
+ longPress(result.getByRole("button", { name: "Appearance" }), 200, 300);
186
+ act(() => {
187
+ vi.advanceTimersByTime(1);
188
+ });
189
+ expect(document.querySelector('[data-testid="floating-panel-menu"]')).not.toBeNull();
190
+ expect(document.querySelector('[data-testid="floating-panel-menu-echo"]')).toBeNull();
191
+ expect(sheet()).toBeNull();
192
+ expect(pointPanel()).toBeNull();
193
+ expect(onPress).not.toHaveBeenCalled();
194
+ });
195
+ });
@@ -54,7 +54,8 @@
54
54
  * ContextMenu (`openOn="contextmenu"`): long-press opens it. On a window wider
55
55
  * than OVERLAY_BREAKPOINT (a tablet) it opens at the long-press point, the
56
56
  * same free overlay the web draws at the pointer; a phone keeps the sheet,
57
- * as web does at ≤640.
57
+ * as web does at ≤640. With `compactFace="menu"` it opens as the pull-down at
58
+ * its trigger instead, at every width (a bar button's long-press, FZ-8158).
58
59
  *
59
60
  * Regular width (FZ-8116, LC-266): on an iPad or any window wider than
60
61
  * OVERLAY_BREAKPOINT the sheet is the wrong form. The panel opens over its
@@ -64,6 +65,7 @@
64
65
 
65
66
  import { nativeAnimationConfig, type MotionStop } from "@multiplatform.one/theme";
66
67
  import {
68
+ isValidElement,
67
69
  type ReactNode,
68
70
  useCallback,
69
71
  useContext,
@@ -208,10 +210,25 @@ export function FloatingPanel({
208
210
  [disabled, onOpenChange],
209
211
  );
210
212
  const isContextMenu = openOn === "contextmenu";
213
+ // A long-press menu never eats its trigger's tap: the web lets the primary
214
+ // click through to the trigger, so here the Pressable that owns the gesture
215
+ // runs the trigger's own onPress and carries its name.
216
+ const triggerOwn =
217
+ isContextMenu && isValidElement(trigger)
218
+ ? (trigger.props as {
219
+ onPress?: (event: GestureResponderEvent) => void;
220
+ "aria-label"?: string;
221
+ accessibilityLabel?: string;
222
+ })
223
+ : undefined;
224
+ const triggerLabel =
225
+ triggerA11y?.label ?? triggerOwn?.["aria-label"] ?? triggerOwn?.accessibilityLabel;
211
226
  const regular = overlaySizeClass(windowWidth) === "regular";
212
227
  const atPoint = isContextMenu && pressPoint !== null && regular;
213
228
  const insideOverlay = useInsideOverlay();
214
- const asMenu = !isContextMenu && resolveCompactFace(compactFace, insideOverlay) === "menu";
229
+ const asMenu = isContextMenu
230
+ ? compactFace === "menu"
231
+ : resolveCompactFace(compactFace, insideOverlay) === "menu";
215
232
  const asRegular = regular && !asMenu && !isContextMenu && sheet !== true;
216
233
  const anchored = asMenu || asRegular;
217
234
  const triggerRef = useRef<RNView | null>(null);
@@ -543,12 +560,16 @@ export function FloatingPanel({
543
560
  >
544
561
  <Pressable
545
562
  {...(isContextMenu
546
- ? { onLongPress: handleTriggerLongPress, delayLongPress: 500 }
563
+ ? {
564
+ onLongPress: handleTriggerLongPress,
565
+ delayLongPress: 500,
566
+ ...(triggerOwn?.onPress && !disabled ? { onPress: triggerOwn.onPress } : undefined),
567
+ }
547
568
  : { onPress: handleTriggerPress })}
548
569
  disabled={disabled}
549
570
  accessibilityRole="button"
550
571
  accessibilityState={{ disabled: !!disabled, expanded: !!open }}
551
- {...(triggerA11y?.label ? { accessibilityLabel: triggerA11y.label } : undefined)}
572
+ {...(triggerLabel ? { accessibilityLabel: triggerLabel } : undefined)}
552
573
  {...(triggerA11y?.value
553
574
  ? { accessibilityValue: { text: triggerA11y.value } }
554
575
  : undefined)}
@@ -67,11 +67,15 @@ describe("FileUpload", () => {
67
67
  const result = renderWithProviders(
68
68
  <FileUpload existing={{ src: "/files/sprite.png" }} onExistingRemove={remove} />,
69
69
  );
70
- // LC-260: Replace and Remove are glyphs on the media plate over the image.
70
+ // LC-260: Replace and Remove are glyphs over the image; LC-176 puts the
71
+ // pair in one shared media capsule, so the buttons draw glyphs alone.
71
72
  const actions = result.container.querySelector('[data-file-upload="preview-actions"]');
73
+ const capsule = actions?.querySelector('[data-mp-icon-button-group="media"]');
74
+ expect(capsule?.querySelector('[aria-label="Replace"]')).toBeTruthy();
75
+ expect(capsule?.querySelector('[aria-label^="Remove"]')).toBeTruthy();
72
76
  expect(
73
- actions?.querySelector('[aria-label="Replace"]')?.getAttribute("data-mp-icon-button"),
74
- ).toBe("media");
77
+ capsule?.querySelector('[aria-label="Replace"]')?.getAttribute("data-mp-icon-button"),
78
+ ).not.toBe("media");
75
79
  expect(result.container.textContent).not.toContain("Replace");
76
80
  expect(result.container.textContent).not.toContain("Choose File");
77
81
  expect(result.container.textContent).not.toContain("Drag a file");
@@ -349,7 +353,7 @@ describe("FileUpload bento-derived dropzone (MPO-57)", () => {
349
353
  expect(zone.textContent).toContain("Drag a file into this area");
350
354
  });
351
355
 
352
- it("renders picked files as in-zone rows: name, bold size line, bare ✕ remove (29 painted, hitSlop 44)", async () => {
356
+ it("renders picked files as in-zone rows: name, bold size line, bare remove glyph (29 painted, hitSlop 44)", async () => {
353
357
  const { container } = renderWithHouseThemes(
354
358
  <FileUpload label="Attachment" name="attachment" multiple />,
355
359
  );
@@ -372,13 +376,13 @@ describe("FileUpload bento-derived dropzone (MPO-57)", () => {
372
376
  expect(sizeLine).toBeTruthy();
373
377
  expect(sizeLine.className).toMatch(/_fow-700/);
374
378
 
375
- // Remove: LC-176's bare ✕, no fill or border, painted at nestedControl
376
- // (29) with hitSlop restoring the 44 floor (data-mp-press-slop 8 → 45).
377
- // The filled $color12 disc it replaced read as a plate on plain content.
379
+ // Remove: LC-176 bare ✕, no disc or fill, painted at nestedControl (29)
380
+ // with hitSlop restoring the 44 floor (data-mp-press-slop 8 → 29+16=45).
378
381
  const remove = flist.querySelector('[aria-label="Remove hello.txt"]') as HTMLElement;
379
382
  expect(remove).toBeTruthy();
380
383
  expect(remove.getAttribute("data-nested-px")).toBe("29");
381
384
  expect(remove.getAttribute("data-mp-press-slop")).toBe("8");
385
+ expect(remove.className).toMatch(/_bg-transparent/);
382
386
  expect(remove.className).not.toMatch(/_bg-color12/);
383
387
  expect(remove.querySelector("svg")).toBeTruthy();
384
388
  // The caption is dropped once files exist; the CTA stays.
@@ -5,7 +5,7 @@ import type { DeepKeys, DeepValue } from "@tanstack/form-core";
5
5
  import { type DragEvent, type ReactNode, useCallback, useEffect, useRef, useState } from "react";
6
6
  import { useMedia, useProps, type SizeTokens } from "tamagui";
7
7
  import { Image, isWeb, Text, XStack, YStack, styled } from "tamagui";
8
- import { Button, IconButton } from "../../Button";
8
+ import { Button, IconButton, IconButtonGroup } from "../../Button";
9
9
  import { standardActions } from "../../standardActions";
10
10
  import { Field, FieldLayout } from "../../fieldLayout";
11
11
  import { Skeleton } from "../../Skeleton";
@@ -1092,7 +1092,9 @@ export function FileUpload<
1092
1092
  zIndex={1}
1093
1093
  {...knobProps.gap}
1094
1094
  >
1095
- {previewActions(true, onValueChange)}
1095
+ <IconButtonGroup plate="media">
1096
+ {previewActions(true, onValueChange)}
1097
+ </IconButtonGroup>
1096
1098
  </XStack>
1097
1099
  ) : null}
1098
1100
  </YStack>
package/src/index.ts CHANGED
@@ -115,11 +115,19 @@ export type { ActionLabelParts } from "./actionLabel";
115
115
  export {
116
116
  Button,
117
117
  IconButton,
118
+ IconButtonGroup,
119
+ iconButtonGlyphPx,
120
+ ICON_BUTTON_GLASS_MERGE_PX,
118
121
  ICON_BUTTON_GLYPH_PX,
119
122
  ICON_BUTTON_NEUTRAL_GLYPH_PX,
120
123
  ICON_BUTTON_PRESS_OPACITY,
121
124
  } from "./Button";
122
- export type { ButtonProps, ButtonActionRole, IconButtonProps } from "./Button";
125
+ export type {
126
+ ButtonProps,
127
+ ButtonActionRole,
128
+ IconButtonGroupProps,
129
+ IconButtonProps,
130
+ } from "./Button";
123
131
 
124
132
  export { Input } from "./fields/Input";
125
133
  export type { InputProps } from "./fields/Input";
@@ -0,0 +1,22 @@
1
+ import { type ReactNode } from "react";
2
+ import { type XStackProps } from "tamagui";
3
+ /**
4
+ * LC-176: how far apart two Liquid Glass circles may sit and still merge. Half
5
+ * the 44pt target, so neighbours on the bar's gap always join.
6
+ */
7
+ export declare const ICON_BUTTON_GLASS_MERGE_PX = 22;
8
+ export interface IconButtonGroupProps extends XStackProps {
9
+ /** Over imagery or video: the group shares one material capsule. */
10
+ plate?: "media";
11
+ children?: ReactNode;
12
+ }
13
+ /**
14
+ * LC-176: icon buttons that sit side by side. Grounds merge, they never stand
15
+ * as a row of separate circles: on iOS 26 the bar's group is one Liquid Glass
16
+ * capsule, as the system draws a bar's item group (merging each circle through
17
+ * the glass container left a waist between them, FZ-8158); over media the
18
+ * group shares one material capsule; a bare row stays bare. A lone button
19
+ * keeps its own circle.
20
+ */
21
+ export declare function IconButtonGroup({ plate, children, ...rest }: IconButtonGroupProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=IconButtonGroup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IconButtonGroup.d.ts","sourceRoot":"","sources":["../../src/Button/IconButtonGroup.tsx"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAA8B,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAKvE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,oBAAoB,2CA2DjF"}