@multiplatform.one/components 7.81.2 → 7.82.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 (308) hide show
  1. package/dist/cjs/Accordion/index.native.js +1 -1
  2. package/dist/cjs/Accordion/index.native.js.map +1 -1
  3. package/dist/cjs/Alert/index.native.js.map +1 -1
  4. package/dist/cjs/AnimateHeight/index.native.js.map +1 -1
  5. package/dist/cjs/Avatar/getInitials.native.js.map +1 -1
  6. package/dist/cjs/Avatar/index.native.js.map +1 -1
  7. package/dist/cjs/Badge/index.native.js.map +1 -1
  8. package/dist/cjs/Button.native.js.map +1 -1
  9. package/dist/cjs/Card.native.js.map +1 -1
  10. package/dist/cjs/CardFooter.native.js.map +1 -1
  11. package/dist/cjs/CardHeader.native.js.map +1 -1
  12. package/dist/cjs/Carousel/index.native.js +3 -3
  13. package/dist/cjs/Carousel/index.native.js.map +1 -1
  14. package/dist/cjs/CheckGlyph/MenuCheckColumn.native.js.map +1 -1
  15. package/dist/cjs/CheckGlyph/index.native.js.map +1 -1
  16. package/dist/cjs/Chip/index.native.js.map +1 -1
  17. package/dist/cjs/Collapsible.native.js.map +1 -1
  18. package/dist/cjs/ContextMenu/index.native.js.map +1 -1
  19. package/dist/cjs/CopyField/CopyMenuButton.native.js +1 -1
  20. package/dist/cjs/CopyField/CopyMenuButton.native.js.map +1 -1
  21. package/dist/cjs/CopyField/index.native.js.map +1 -1
  22. package/dist/cjs/Dialog.native.js.map +1 -1
  23. package/dist/cjs/Diff/diff.native.js.map +1 -1
  24. package/dist/cjs/Diff/index.native.js.map +1 -1
  25. package/dist/cjs/DotIndicator/index.native.js.map +1 -1
  26. package/dist/cjs/DropdownMenu/index.native.js.map +1 -1
  27. package/dist/cjs/DropdownMenu/menuSettle.native.js.map +1 -1
  28. package/dist/cjs/ElapsedProgress/index.native.js.map +1 -1
  29. package/dist/cjs/ErrorBoundary/index.native.js.map +1 -1
  30. package/dist/cjs/Heading.native.js.map +1 -1
  31. package/dist/cjs/Kbd/index.native.js.map +1 -1
  32. package/dist/cjs/Kbd/keys.native.js.map +1 -1
  33. package/dist/cjs/Lightbox/MediaControl.native.js.map +1 -1
  34. package/dist/cjs/Lightbox/index.gnome.native.js.map +1 -1
  35. package/dist/cjs/Lightbox/index.native.js +1 -1
  36. package/dist/cjs/Lightbox/shared.native.js.map +1 -1
  37. package/dist/cjs/Lightbox/useLightbox.native.js.map +1 -1
  38. package/dist/cjs/MachineLiteral/index.native.js.map +1 -1
  39. package/dist/cjs/Material/index.native.js.map +1 -1
  40. package/dist/cjs/Meter/index.native.js +1 -1
  41. package/dist/cjs/Meter/index.native.js.map +1 -1
  42. package/dist/cjs/Meter/meter.native.js.map +1 -1
  43. package/dist/cjs/OverflowRow/index.native.js +1 -1
  44. package/dist/cjs/OverflowRow/index.native.js.map +1 -1
  45. package/dist/cjs/Pagination.native.js +1 -1
  46. package/dist/cjs/Pagination.native.js.map +1 -1
  47. package/dist/cjs/Popover.native.js +1 -1
  48. package/dist/cjs/Popover.native.js.map +1 -1
  49. package/dist/cjs/ProgressSteps/index.native.js.map +1 -1
  50. package/dist/cjs/Quote/index.native.js.map +1 -1
  51. package/dist/cjs/SchemeToggle/index.native.js +1 -1
  52. package/dist/cjs/SchemeToggle/index.native.js.map +1 -1
  53. package/dist/cjs/ScrollView/revealFocusedField.native.js.map +1 -1
  54. package/dist/cjs/Separator/index.native.js.map +1 -1
  55. package/dist/cjs/Sheet.native.js.map +1 -1
  56. package/dist/cjs/SheetModal/SheetHeader.native.js.map +1 -1
  57. package/dist/cjs/SheetModal/SheetNavigation.native.js.map +1 -1
  58. package/dist/cjs/SheetModal/detents.native.js.map +1 -1
  59. package/dist/cjs/SheetModal/useSheetFrameFocus.native.js.map +1 -1
  60. package/dist/cjs/Skeleton/index.native.js.map +1 -1
  61. package/dist/cjs/SkipLink/index.native.js.map +1 -1
  62. package/dist/cjs/SortableList/SortableHandle.native.js.map +1 -1
  63. package/dist/cjs/SortableList/index.native.js.map +1 -1
  64. package/dist/cjs/SortableList/useSortableRows.native.js.map +1 -1
  65. package/dist/cjs/SortableList/useUserOrder.native.js.map +1 -1
  66. package/dist/cjs/Tabs/index.native.js +1 -1
  67. package/dist/cjs/Tabs/index.native.js.map +1 -1
  68. package/dist/cjs/Text.native.js.map +1 -1
  69. package/dist/cjs/ThemePresetPicker/index.native.js +2 -2
  70. package/dist/cjs/ThemePresetPicker/index.native.js.map +1 -1
  71. package/dist/cjs/Toast/index.native.js.map +1 -1
  72. package/dist/cjs/Toast/store.native.js.map +1 -1
  73. package/dist/cjs/Tooltip/nativePress.native.js.map +1 -1
  74. package/dist/cjs/Tooltip/tooltipShared.native.js.map +1 -1
  75. package/dist/cjs/TreeSelect/index.native.js +2 -2
  76. package/dist/cjs/TreeSelect/index.native.js.map +1 -1
  77. package/dist/cjs/TreeSelect/treeSelectModel.native.js.map +1 -1
  78. package/dist/cjs/Video/PlayerChrome.native.js.map +1 -1
  79. package/dist/cjs/Video/VideoFrame.native.js.map +1 -1
  80. package/dist/cjs/Video/types.native.js.map +1 -1
  81. package/dist/cjs/Wheel/index.native.js.map +1 -1
  82. package/dist/cjs/Wizard/index.native.js +2 -2
  83. package/dist/cjs/Wizard/index.native.js.map +1 -1
  84. package/dist/cjs/actions/ActionBar.native.js.map +1 -1
  85. package/dist/cjs/actions/DangerSection.native.js.map +1 -1
  86. package/dist/cjs/actions/DestructiveAction.native.js.map +1 -1
  87. package/dist/cjs/actions/HeaderActions.native.js +2 -2
  88. package/dist/cjs/actions/HeaderActions.native.js.map +1 -1
  89. package/dist/cjs/actions/StandardAction.native.js +1 -1
  90. package/dist/cjs/actions/StandardAction.native.js.map +1 -1
  91. package/dist/cjs/actions/devWarn.native.js.map +1 -1
  92. package/dist/cjs/actions/index.native.js.map +1 -1
  93. package/dist/cjs/actions/severity.native.js.map +1 -1
  94. package/dist/cjs/appShell/AppShell.native.js +1 -1
  95. package/dist/cjs/appShell/AppShell.native.js.map +1 -1
  96. package/dist/cjs/appShell/Dock.native.js.map +1 -1
  97. package/dist/cjs/appShell/MpoTabHost.native.js.map +1 -1
  98. package/dist/cjs/appShell/ScopeMenu.native.js +1 -1
  99. package/dist/cjs/appShell/ScopeMenu.native.js.map +1 -1
  100. package/dist/cjs/appShell/SettingsSplit.native.js +1 -1
  101. package/dist/cjs/appShell/SettingsSplit.native.js.map +1 -1
  102. package/dist/cjs/appShell/Springboard.native.js +1 -1
  103. package/dist/cjs/appShell/Springboard.native.js.map +1 -1
  104. package/dist/cjs/appShell/SystemBar.native.js.map +1 -1
  105. package/dist/cjs/appShell/TabBar.native.js +1 -1
  106. package/dist/cjs/appShell/TabBar.native.js.map +1 -1
  107. package/dist/cjs/appShell/appTile.native.js +1 -1
  108. package/dist/cjs/appShell/appTile.native.js.map +1 -1
  109. package/dist/cjs/appShell/bottomChrome.native.js.map +1 -1
  110. package/dist/cjs/appShell/index.native.js.map +1 -1
  111. package/dist/cjs/charts/Axis.native.js.map +1 -1
  112. package/dist/cjs/charts/BarChart.native.js.map +1 -1
  113. package/dist/cjs/charts/ChartDataView.native.js +2 -2
  114. package/dist/cjs/charts/ChartDataView.native.js.map +1 -1
  115. package/dist/cjs/charts/ChartSurface.native.js.map +1 -1
  116. package/dist/cjs/charts/LineChart.native.js.map +1 -1
  117. package/dist/cjs/charts/PieChart.native.js.map +1 -1
  118. package/dist/cjs/charts/Sparkline.native.js.map +1 -1
  119. package/dist/cjs/charts/accessibility.native.js.map +1 -1
  120. package/dist/cjs/charts/chartHeight.native.js.map +1 -1
  121. package/dist/cjs/charts/composeChart.native.js.map +1 -1
  122. package/dist/cjs/charts/index.native.js.map +1 -1
  123. package/dist/cjs/charts/math.native.js.map +1 -1
  124. package/dist/cjs/charts/series.native.js.map +1 -1
  125. package/dist/cjs/charts/svg.native.js.map +1 -1
  126. package/dist/cjs/charts/types.native.js.map +1 -1
  127. package/dist/cjs/charts/useChartTheme.native.js.map +1 -1
  128. package/dist/cjs/code/Code/index.native.js.map +1 -1
  129. package/dist/cjs/code/CodeBlock/index.cjs +38 -31
  130. package/dist/cjs/code/CodeBlock/index.native.js +45 -38
  131. package/dist/cjs/code/CodeBlock/index.native.js.map +1 -1
  132. package/dist/cjs/code/HighlightedCode/index.native.js.map +1 -1
  133. package/dist/cjs/code/Highlighter/houseSyntaxTheme.native.js.map +1 -1
  134. package/dist/cjs/code/Highlighter/index.native.js.map +1 -1
  135. package/dist/cjs/code/Highlighter/namedGroupMatchAll.native.js.map +1 -1
  136. package/dist/cjs/code/LogView.native.js.map +1 -1
  137. package/dist/cjs/code/Pre/index.native.js.map +1 -1
  138. package/dist/cjs/code/index.native.js +5 -5
  139. package/dist/cjs/code/index.native.js.map +1 -1
  140. package/dist/cjs/componentColors.native.js.map +1 -1
  141. package/dist/cjs/feedback/ConfirmDialog.native.js.map +1 -1
  142. package/dist/cjs/feedback/NotifyHost.native.js +2 -2
  143. package/dist/cjs/feedback/NotifyHost.native.js.map +1 -1
  144. package/dist/cjs/feedback/copy.native.js.map +1 -1
  145. package/dist/cjs/feedback/index.native.js.map +1 -1
  146. package/dist/cjs/feedback/notify.native.js.map +1 -1
  147. package/dist/cjs/feedback/route.native.js.map +1 -1
  148. package/dist/cjs/feedback/store.native.js.map +1 -1
  149. package/dist/cjs/formLayouts/FormLayouts.native.js +1 -1
  150. package/dist/cjs/formLayouts/FormLayouts.native.js.map +1 -1
  151. package/dist/cjs/formLayouts/PreferencePanel.native.js +2 -2
  152. package/dist/cjs/formLayouts/PreferencePanel.native.js.map +1 -1
  153. package/dist/cjs/formLayouts/index.native.js.map +1 -1
  154. package/dist/cjs/hooks/index.native.js +1 -1
  155. package/dist/cjs/hooks/index.native.js.map +1 -1
  156. package/dist/cjs/hooks/useClipboard.native.js.map +1 -1
  157. package/dist/cjs/hooks/useDocumentTitle.native.js.map +1 -1
  158. package/dist/cjs/hooks/useHoldCue.native.js.map +1 -1
  159. package/dist/cjs/icons/CheckRegular.native.js.map +1 -1
  160. package/dist/cjs/icons/MinusRegular.native.js.map +1 -1
  161. package/dist/cjs/icons/index.native.js.map +1 -1
  162. package/dist/cjs/icons/lucide.native.js.map +1 -1
  163. package/dist/cjs/icons/themed.native.js.map +1 -1
  164. package/dist/cjs/images/Image/ImageFrame.native.js +1 -1
  165. package/dist/cjs/images/Image/ImageFrame.native.js.map +1 -1
  166. package/dist/cjs/images/Image/framing.native.js.map +1 -1
  167. package/dist/cjs/images/Image/index.native.js +1 -1
  168. package/dist/cjs/images/Image/mediaRadius.native.js.map +1 -1
  169. package/dist/cjs/images/Image/shared.native.js +1 -1
  170. package/dist/cjs/images/Image/shared.native.js.map +1 -1
  171. package/dist/cjs/images/Image/types.native.js.map +1 -1
  172. package/dist/cjs/images/index.native.js.map +1 -1
  173. package/dist/cjs/index.native.js +39 -39
  174. package/dist/cjs/index.native.js.map +1 -1
  175. package/dist/cjs/knobImmuneExports.native.js.map +1 -1
  176. package/dist/cjs/layouts/AsyncBoundary.native.js +2 -2
  177. package/dist/cjs/layouts/AsyncBoundary.native.js.map +1 -1
  178. package/dist/cjs/layouts/Debug/index.native.js.map +1 -1
  179. package/dist/cjs/layouts/EmptyState.cjs +9 -43
  180. package/dist/cjs/layouts/EmptyState.native.js +9 -45
  181. package/dist/cjs/layouts/EmptyState.native.js.map +1 -1
  182. package/dist/cjs/layouts/IdentityPlate.native.js.map +1 -1
  183. package/dist/cjs/layouts/KeyboardAvoidingWrapper/behavior.native.js.map +1 -1
  184. package/dist/cjs/layouts/PageTemplates.native.js.map +1 -1
  185. package/dist/cjs/layouts/PaneScaffold.native.js.map +1 -1
  186. package/dist/cjs/layouts/SafeAreaWrapper/insets.native.js.map +1 -1
  187. package/dist/cjs/layouts/StatusDot.native.js.map +1 -1
  188. package/dist/cjs/layouts/StatusGlyphStrip.native.js +1 -1
  189. package/dist/cjs/layouts/StatusGlyphStrip.native.js.map +1 -1
  190. package/dist/cjs/layouts/ViewSwitcher.native.js.map +1 -1
  191. package/dist/cjs/layouts/attachments.native.js +1 -1
  192. package/dist/cjs/layouts/attachments.native.js.map +1 -1
  193. package/dist/cjs/layouts/barTitleSpan.cjs +54 -0
  194. package/dist/cjs/layouts/barTitleSpan.native.js +78 -0
  195. package/dist/cjs/layouts/barTitleSpan.native.js.map +1 -0
  196. package/dist/cjs/layouts/bottomAccessory.native.js.map +1 -1
  197. package/dist/cjs/layouts/breadcrumbs.native.js.map +1 -1
  198. package/dist/cjs/layouts/index.native.js +3 -3
  199. package/dist/cjs/layouts/index.native.js.map +1 -1
  200. package/dist/cjs/layouts/page.cjs +130 -41
  201. package/dist/cjs/layouts/page.native.js +145 -47
  202. package/dist/cjs/layouts/page.native.js.map +1 -1
  203. package/dist/cjs/layouts/pageHeadingScope.native.js.map +1 -1
  204. package/dist/cjs/layouts/screenScroll.native.js.map +1 -1
  205. package/dist/cjs/layouts/shellSidebarSlot.native.js.map +1 -1
  206. package/dist/cjs/layouts/sidebar.native.js +1 -1
  207. package/dist/cjs/layouts/sidebar.native.js.map +1 -1
  208. package/dist/cjs/layouts/tags.native.js +2 -2
  209. package/dist/cjs/layouts/tags.native.js.map +1 -1
  210. package/dist/cjs/layouts/timeline.native.js +2 -2
  211. package/dist/cjs/layouts/timeline.native.js.map +1 -1
  212. package/dist/cjs/layouts/toolbar.native.js +1 -1
  213. package/dist/cjs/layouts/toolbar.native.js.map +1 -1
  214. package/dist/cjs/shared/actionScan.native.js.map +1 -1
  215. package/dist/cjs/shared/i18n.native.js.map +1 -1
  216. package/dist/cjs/shared/restoreRetries.native.js.map +1 -1
  217. package/dist/cjs/shared/t.native.js.map +1 -1
  218. package/dist/cjs/shared/textRidesText.native.js.map +1 -1
  219. package/dist/cjs/shared/useReturnFocusOnClose.native.js.map +1 -1
  220. package/dist/cjs/surfaces.native.js +1 -1
  221. package/dist/cjs/surfaces.native.js.map +1 -1
  222. package/dist/cjs/tamagui.native.js +1 -1
  223. package/dist/cjs/tamagui.native.js.map +1 -1
  224. package/dist/cjs/testSetup.native.js.map +1 -1
  225. package/dist/cjs/utils/setTinted.native.js.map +1 -1
  226. package/dist/cjs/utils/unwrapText.native.js.map +1 -1
  227. package/dist/cjs/views/Calendar.native.js +1 -1
  228. package/dist/cjs/views/Calendar.native.js.map +1 -1
  229. package/dist/cjs/views/Dashboard.native.js +2 -2
  230. package/dist/cjs/views/Dashboard.native.js.map +1 -1
  231. package/dist/cjs/views/Gantt.native.js +2 -2
  232. package/dist/cjs/views/Gantt.native.js.map +1 -1
  233. package/dist/cjs/views/GanttDateGrip.native.js.map +1 -1
  234. package/dist/cjs/views/ImageGrid.native.js +2 -2
  235. package/dist/cjs/views/ImageGrid.native.js.map +1 -1
  236. package/dist/cjs/views/Kanban.native.js +1 -1
  237. package/dist/cjs/views/List.native.js.map +1 -1
  238. package/dist/cjs/views/ListGroup.native.js.map +1 -1
  239. package/dist/cjs/views/ListRow.native.js.map +1 -1
  240. package/dist/cjs/views/Matrix.native.js +2 -2
  241. package/dist/cjs/views/Matrix.native.js.map +1 -1
  242. package/dist/cjs/views/ReportBuilder.native.js +2 -2
  243. package/dist/cjs/views/ReportBuilder.native.js.map +1 -1
  244. package/dist/cjs/views/ShortcutCard.native.js.map +1 -1
  245. package/dist/cjs/views/TreeView.native.js.map +1 -1
  246. package/dist/cjs/views/calendarMath.native.js.map +1 -1
  247. package/dist/cjs/views/ganttIntervals.native.js.map +1 -1
  248. package/dist/cjs/views/ganttMath.native.js.map +1 -1
  249. package/dist/cjs/views/index.native.js +1 -1
  250. package/dist/cjs/views/index.native.js.map +1 -1
  251. package/dist/cjs/views/kanbanDnd.native.js.map +1 -1
  252. package/dist/cjs/views/matrixMath.native.js.map +1 -1
  253. package/dist/cjs/views/sameNameScope.native.js.map +1 -1
  254. package/dist/cjs/views/treeViewDnd.native.js.map +1 -1
  255. package/dist/cjs/views/treeViewLayout.native.js.map +1 -1
  256. package/dist/cjs/views/treeViewMotion.native.js.map +1 -1
  257. package/dist/cjs/views/treeViewReveal.native.js.map +1 -1
  258. package/dist/cjs/views/usePageFlowLoadMore.native.js.map +1 -1
  259. package/dist/cjs/views/widgetChrome.native.js.map +1 -1
  260. package/dist/esm/code/CodeBlock/index.mjs +39 -32
  261. package/dist/esm/code/CodeBlock/index.mjs.map +1 -1
  262. package/dist/esm/code/CodeBlock/index.native.js +39 -32
  263. package/dist/esm/code/CodeBlock/index.native.js.map +1 -1
  264. package/dist/esm/layouts/EmptyState.mjs +9 -43
  265. package/dist/esm/layouts/EmptyState.mjs.map +1 -1
  266. package/dist/esm/layouts/EmptyState.native.js +9 -45
  267. package/dist/esm/layouts/EmptyState.native.js.map +1 -1
  268. package/dist/esm/layouts/barTitleSpan.mjs +27 -0
  269. package/dist/esm/layouts/barTitleSpan.mjs.map +1 -0
  270. package/dist/esm/layouts/barTitleSpan.native.js +48 -0
  271. package/dist/esm/layouts/barTitleSpan.native.js.map +1 -0
  272. package/dist/esm/layouts/page.mjs +132 -43
  273. package/dist/esm/layouts/page.mjs.map +1 -1
  274. package/dist/esm/layouts/page.native.js +145 -47
  275. package/dist/esm/layouts/page.native.js.map +1 -1
  276. package/dist/jsx/code/CodeBlock/index.mjs +39 -32
  277. package/dist/jsx/code/CodeBlock/index.mjs.map +1 -1
  278. package/dist/jsx/code/CodeBlock/index.native.js +38 -31
  279. package/dist/jsx/code/CodeBlock/index.native.js.map +1 -1
  280. package/dist/jsx/layouts/EmptyState.mjs +9 -43
  281. package/dist/jsx/layouts/EmptyState.mjs.map +1 -1
  282. package/dist/jsx/layouts/EmptyState.native.js +9 -45
  283. package/dist/jsx/layouts/EmptyState.native.js.map +1 -1
  284. package/dist/jsx/layouts/barTitleSpan.mjs +27 -0
  285. package/dist/jsx/layouts/barTitleSpan.mjs.map +1 -0
  286. package/dist/jsx/layouts/barTitleSpan.native.js +78 -0
  287. package/dist/jsx/layouts/barTitleSpan.native.js.map +1 -0
  288. package/dist/jsx/layouts/page.mjs +132 -43
  289. package/dist/jsx/layouts/page.mjs.map +1 -1
  290. package/dist/jsx/layouts/page.native.js +143 -45
  291. package/dist/jsx/layouts/page.native.js.map +1 -1
  292. package/package.json +9 -9
  293. package/src/code/CodeBlock/CodeBlock.copy.spec.tsx +13 -0
  294. package/src/code/CodeBlock/index.tsx +46 -30
  295. package/src/layouts/EmptyState.spec.tsx +2 -0
  296. package/src/layouts/EmptyState.tsx +6 -39
  297. package/src/layouts/PageHeader.stories.tsx +238 -38
  298. package/src/layouts/barTitleSpan.spec.ts +35 -0
  299. package/src/layouts/barTitleSpan.ts +44 -0
  300. package/src/layouts/glyphCentre.spec.tsx +218 -0
  301. package/src/layouts/navBar.spec.tsx +58 -2
  302. package/src/layouts/page.tsx +126 -34
  303. package/types/code/CodeBlock/index.d.ts.map +1 -1
  304. package/types/layouts/EmptyState.d.ts.map +1 -1
  305. package/types/layouts/barTitleSpan.d.ts +29 -0
  306. package/types/layouts/barTitleSpan.d.ts.map +1 -0
  307. package/types/layouts/page.d.ts +6 -1
  308. package/types/layouts/page.d.ts.map +1 -1
@@ -1,27 +1,30 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
2
2
  // Catalog Button (G-15 story honesty / LC-18): raw tamagui buttons ignored
3
3
  // the knobs and pinned a non-house radius (UX-P06).
4
- import { Button, IconButton, Progress } from "@multiplatform.one/forms";
4
+ import { Button, IconButton } from "@multiplatform.one/forms";
5
5
  import {
6
- CopyIcon,
7
- DotsThreeIcon,
6
+ CpuIcon,
7
+ DatabaseIcon,
8
8
  MagnifyingGlassIcon,
9
+ MemoryIcon,
9
10
  PlusIcon,
10
11
  SignOutIcon,
11
- TrashIcon,
12
12
  UserIcon,
13
13
  } from "@phosphor-icons/react";
14
14
  import { Preset } from "@multiplatform.one/theme";
15
15
  import type { ReactNode } from "react";
16
- import { XStack, YStack } from "tamagui";
16
+ import { Circle, Theme, View, XStack, YStack } from "tamagui";
17
17
  import { Avatar } from "../Avatar";
18
18
  import { Card } from "../Card";
19
+ import { themed } from "../icons/themed";
20
+ import { Chip } from "../Chip";
21
+ import { CodeBlock } from "../code/CodeBlock";
19
22
  import { DropdownMenu } from "../DropdownMenu";
20
23
  import { Popover } from "../Popover";
21
24
  import { SchemeToggle } from "../SchemeToggle";
22
25
  import { PopoverContent } from "../surfaces";
23
26
  import { Tabs } from "../Tabs";
24
- import { PageBackButton, PageHeader, Screen, ScreenToolbar } from "./page";
27
+ import { PageBackButton, PageHeader, PageSection, Screen, ScreenToolbar } from "./page";
25
28
  import { Paragraph, SizableText } from "../Text";
26
29
 
27
30
  const meta: Meta<typeof PageHeader> = {
@@ -174,23 +177,237 @@ export const LongTitleNarrow: Story = {
174
177
  ),
175
178
  };
176
179
 
177
- // FZ-8020 LC-176: the SHC console Nodes screen with every icon-button
178
- // context, drawn by the platform with no use-site paint. Its bar (back,
179
- // search, add) takes the accent, on Liquid Glass on iOS 26; content actions
180
- // (the inline copy, the card's overflow) are bold glyphs in the text colour;
181
- // the delete keeps its intent ink; the tile's favourite rides the media plate.
182
- function Meter({ label, value, detail }: { label: string; value: number; detail: string }) {
180
+ // FZ-8442: the SHC console's Nodes screen as it is now (shc
181
+ // features/console/src/manage/app/NodesPane.tsx and ManageApp.tsx's
182
+ // NodesPlace): sites as sections, each node a front panel whose whole face
183
+ // links to its detail, where the verbs live. The "+" opens one panel over
184
+ // itself holding the join command as a code block (LC-112, LC-267). The bar
185
+ // keeps every LC-176 icon-button context.
186
+ const faceplate = {
187
+ ear: 18,
188
+ screw: 9,
189
+ slot: 1.5,
190
+ led: 14,
191
+ ledGap: 2,
192
+ digitWidth: 8,
193
+ digitHeight: 14,
194
+ segment: 2,
195
+ digitGap: 2,
196
+ window: 6,
197
+ } as const;
198
+ const LED_COUNT = 20;
199
+ const bayGlyph = { cpu: themed(CpuIcon), memory: themed(MemoryIcon), disk: themed(DatabaseIcon) };
200
+
201
+ function Screw({ angle }: { angle: number }) {
202
+ const inner = faceplate.screw - 2;
203
+ return (
204
+ <Circle
205
+ size={faceplate.screw}
206
+ borderWidth={1}
207
+ borderColor="$color9"
208
+ backgroundColor="$color6"
209
+ position="relative"
210
+ >
211
+ <View
212
+ position="absolute"
213
+ top={(inner - faceplate.slot) / 2}
214
+ left={0}
215
+ width={inner}
216
+ height={faceplate.slot}
217
+ backgroundColor="$color11"
218
+ rotate={`${angle}deg`}
219
+ />
220
+ </Circle>
221
+ );
222
+ }
223
+
224
+ function RackEar({ side, angles }: { side: "left" | "right"; angles: [number, number] }) {
225
+ return (
226
+ <YStack
227
+ width={faceplate.ear}
228
+ alignSelf="stretch"
229
+ alignItems="center"
230
+ justifyContent="space-between"
231
+ paddingVertical="$3"
232
+ backgroundColor="$color4"
233
+ borderColor="$color6"
234
+ borderLeftWidth={side === "right" ? 1 : 0}
235
+ borderRightWidth={side === "left" ? 1 : 0}
236
+ aria-hidden
237
+ >
238
+ <Screw angle={angles[0]} />
239
+ <Screw angle={angles[1]} />
240
+ </YStack>
241
+ );
242
+ }
243
+
244
+ const segmentsOf: Record<string, string> = {
245
+ "0": "abcdef",
246
+ "1": "bc",
247
+ "2": "abdeg",
248
+ "3": "abcdg",
249
+ "4": "bcfg",
250
+ "5": "acdfg",
251
+ "6": "acdefg",
252
+ "7": "abc",
253
+ "8": "abcdefg",
254
+ "9": "abcdfg",
255
+ };
256
+
257
+ function segmentBox(name: string, width: number) {
258
+ const { digitHeight: h, segment: t } = faceplate;
259
+ const middle = (h - t) / 2;
260
+ const stroke = (h + t) / 2;
261
+ const boxes: Record<string, { top: number; left: number; width: number; height: number }> = {
262
+ a: { top: 0, left: 0, width, height: t },
263
+ b: { top: 0, left: width - t, width: t, height: stroke },
264
+ c: { top: middle, left: width - t, width: t, height: stroke },
265
+ d: { top: h - t, left: 0, width, height: t },
266
+ e: { top: middle, left: 0, width: t, height: stroke },
267
+ f: { top: 0, left: 0, width: t, height: stroke },
268
+ g: { top: middle, left: 0, width, height: t },
269
+ };
270
+ return boxes[name]!;
271
+ }
272
+
273
+ function AddressDisplay({ address }: { address: string }) {
274
+ return (
275
+ <Theme name="dark">
276
+ <XStack
277
+ role="img"
278
+ aria-label={address}
279
+ alignItems="center"
280
+ gap={faceplate.digitGap}
281
+ backgroundColor="$color1"
282
+ borderColor="$color6"
283
+ borderWidth={1}
284
+ borderRadius="$2"
285
+ padding={faceplate.window}
286
+ >
287
+ {[...address].map((char, index) => {
288
+ if (char === "." || char === ":")
289
+ return (
290
+ <YStack
291
+ key={index}
292
+ width={faceplate.segment}
293
+ height={faceplate.digitHeight}
294
+ justifyContent={char === "." ? "flex-end" : "space-evenly"}
295
+ >
296
+ <View
297
+ width={faceplate.segment}
298
+ height={faceplate.segment}
299
+ backgroundColor="$blue10"
300
+ />
301
+ {char === ":" ? (
302
+ <View
303
+ width={faceplate.segment}
304
+ height={faceplate.segment}
305
+ backgroundColor="$blue10"
306
+ />
307
+ ) : null}
308
+ </YStack>
309
+ );
310
+ const width = char === "1" ? faceplate.segment : faceplate.digitWidth;
311
+ return (
312
+ <View key={index} width={width} height={faceplate.digitHeight} position="relative">
313
+ {[...(segmentsOf[char] ?? "")].map((name) => (
314
+ <View
315
+ key={name}
316
+ position="absolute"
317
+ {...segmentBox(name, width)}
318
+ backgroundColor="$blue10"
319
+ />
320
+ ))}
321
+ </View>
322
+ );
323
+ })}
324
+ </XStack>
325
+ </Theme>
326
+ );
327
+ }
328
+
329
+ const ledInk = { lit: "$color11", warning: "$orange9", critical: "$red9" } as const;
330
+ const lampAt = (index: number) =>
331
+ index >= LED_COUNT * 0.9 ? "critical" : index >= LED_COUNT * 0.7 ? "warning" : "lit";
332
+
333
+ function LedBar({
334
+ bay,
335
+ label,
336
+ percent,
337
+ amount,
338
+ }: {
339
+ bay: keyof typeof bayGlyph;
340
+ label: string;
341
+ percent: number;
342
+ amount: string;
343
+ }) {
344
+ const lit = Math.round((percent / 100) * LED_COUNT);
345
+ const Glyph = bayGlyph[bay];
183
346
  return (
184
- <YStack gap="$2">
185
- <XStack justifyContent="space-between">
186
- <SizableText color="$color11">{label}</SizableText>
187
- <SizableText>{detail}</SizableText>
347
+ <YStack gap="$1.5" role="meter" aria-label={label} aria-valuenow={percent}>
348
+ <XStack alignItems="center" gap="$2">
349
+ <Glyph size={16} weight="bold" color="$color11" />
350
+ <SizableText flex={1} size="$2" fontVariant={["tabular-nums"]} numberOfLines={1}>
351
+ {amount}
352
+ </SizableText>
353
+ <SizableText size="$2" fontWeight="700" fontVariant={["tabular-nums"]}>
354
+ {`${percent}%`}
355
+ </SizableText>
356
+ </XStack>
357
+ <XStack gap={faceplate.ledGap} aria-hidden>
358
+ {Array.from({ length: LED_COUNT }, (_, index) => (
359
+ <View
360
+ key={index}
361
+ flex={1}
362
+ height={faceplate.led}
363
+ borderRadius={1}
364
+ backgroundColor={ledInk[lampAt(index)]}
365
+ opacity={index < lit ? 1 : 0.18}
366
+ />
367
+ ))}
188
368
  </XStack>
189
- <Progress value={value} />
190
369
  </YStack>
191
370
  );
192
371
  }
193
372
 
373
+ function NodeFaceplate() {
374
+ return (
375
+ <Card padding="$0" overflow="hidden" flexDirection="row" alignItems="stretch">
376
+ <RackEar side="left" angles={[38, 121]} />
377
+ <YStack flex={1} gap="$3" padding="$3">
378
+ <XStack alignItems="center" justifyContent="space-between" gap="$2.5">
379
+ <XStack gap="$1.5" alignItems="center" aria-hidden>
380
+ <Circle size="$0.75" backgroundColor="$green9" />
381
+ <Circle size="$0.5" backgroundColor="$green9" />
382
+ </XStack>
383
+ <AddressDisplay address="10.64.96.1:4003" />
384
+ </XStack>
385
+ <XStack alignItems="center" gap="$2.5">
386
+ <SizableText size="$4" fontWeight="700" numberOfLines={1}>
387
+ incus01
388
+ </SizableText>
389
+ <SizableText size="$1" fontWeight="600">
390
+ incus
391
+ </SizableText>
392
+ </XStack>
393
+ <LedBar bay="cpu" label="CPU" percent={12} amount="2.83/24 cores" />
394
+ <LedBar bay="memory" label="Memory" percent={41} amount="25.3/61.9 GiB" />
395
+ <LedBar bay="disk" label="Disk" percent={74} amount="352/476 GiB" />
396
+ <XStack gap="$1.5" flexWrap="wrap">
397
+ <Chip size="$2">AMD Ryzen 9 7900</Chip>
398
+ <Chip size="$2">x86_64</Chip>
399
+ </XStack>
400
+ <Avatar.Group max={7} size={36} circular={false}>
401
+ {["pokemon", "keycloak", "nextcloud", "fizzy", "gitlab", "openobserve"].map((app) => (
402
+ <Avatar key={app} name={app} />
403
+ ))}
404
+ </Avatar.Group>
405
+ </YStack>
406
+ <RackEar side="right" angles={[92, 7]} />
407
+ </Card>
408
+ );
409
+ }
410
+
194
411
  // LC-112 (FZ-8665 Round 45): each icon trigger's panel covers its button with
195
412
  // no X, and a press outside closes it. The join panel's title sits on the
196
413
  // panel's padding.
@@ -209,7 +426,7 @@ function AddNode() {
209
426
  </Popover.Header>
210
427
  <YStack paddingHorizontal="$4" gap="$3">
211
428
  <Paragraph color="$color11">Run this on the new machine.</Paragraph>
212
- <SizableText fontFamily="$mono">shc node join</SizableText>
429
+ <CodeBlock language="text">shc node join</CodeBlock>
213
430
  </YStack>
214
431
  </YStack>
215
432
  </PopoverContent>
@@ -258,26 +475,9 @@ function NodesScreen() {
258
475
  }
259
476
  >
260
477
  <PageHeader title="Nodes" divider={false} actions={<AddNode />} />
261
- <XStack alignItems="center">
262
- <SizableText fontFamily="$mono" color="$color11">
263
- shc node join
264
- </SizableText>
265
- <IconButton inline icon={CopyIcon} aria-label="Copy" />
266
- </XStack>
267
- <Card gap="$4">
268
- <XStack justifyContent="space-between" alignItems="center">
269
- <SizableText size="$6" fontWeight="600">
270
- incus01
271
- </SizableText>
272
- <XStack alignItems="center">
273
- <IconButton icon={DotsThreeIcon} aria-label="More for incus01" />
274
- <IconButton error icon={TrashIcon} aria-label="Remove incus01" />
275
- </XStack>
276
- </XStack>
277
- <SizableText color="$color11">10.64.96.1:4003</SizableText>
278
- <Meter label="CPU" value={12} detail="2.83/24 cores 12%" />
279
- <Meter label="Mem" value={41} detail="25.3/61.9 GiB 41%" />
280
- </Card>
478
+ <PageSection title="Unplaced" description="1 node" headingLevel={3}>
479
+ <NodeFaceplate />
480
+ </PageSection>
281
481
  </Screen>
282
482
  );
283
483
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * LC-157 (FZ-8442): the bar's inline title is centred on the bar while it
3
+ * fits clear of both side groups, and slides into the span between them when
4
+ * the wider group would touch it (Clay's "Inbox" beside four trailing glyphs).
5
+ */
6
+ import { describe, expect, it } from "vitest";
7
+ import { BAR_TITLE_GAP, barTitleSpan, sideExtents } from "./barTitleSpan";
8
+
9
+ describe("bar title span (LC-157, FZ-8442)", () => {
10
+ it("centres on the bar when the title fits clear of both groups", () => {
11
+ const span = barTitleSpan({ row: 358, leftEnd: 44, rightStart: 314, title: 60 });
12
+ expect(span.left).toBe(span.right);
13
+ expect(span.left).toBe(44 + BAR_TITLE_GAP);
14
+ });
15
+
16
+ it("slides between the groups when centring would hide it", () => {
17
+ const span = barTitleSpan({ row: 358, leftEnd: 44, rightStart: 130, title: 45 });
18
+ expect(span).toEqual({ left: 44 + BAR_TITLE_GAP, right: 358 - 130 + BAR_TITLE_GAP });
19
+ expect(358 - span.left - span.right).toBeGreaterThanOrEqual(45);
20
+ });
21
+
22
+ it("reads the groups by side, so a right-to-left bar mirrors", () => {
23
+ const ltr = sideExtents(358, { x: 16, width: 44 }, { x: 250, width: 92 });
24
+ const rtl = sideExtents(358, { x: 298, width: 44 }, { x: 16, width: 92 });
25
+ expect(ltr).toEqual({ leftEnd: 60, rightStart: 250 });
26
+ expect(rtl).toEqual({ leftEnd: 108, rightStart: 298 });
27
+ });
28
+
29
+ it("an empty group takes no room", () => {
30
+ expect(sideExtents(358, { x: 16, width: 44 }, { x: 342, width: 0 })).toEqual({
31
+ leftEnd: 60,
32
+ rightStart: 0,
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,44 @@
1
+ /** The space kept between the bar's inline title and either side group. */
2
+ export const BAR_TITLE_GAP = 8;
3
+
4
+ export interface BarTitleBox {
5
+ /** The bar row's width. */
6
+ row: number;
7
+ /** Where the group on the left ends, from the row's left edge; 0 when empty. */
8
+ leftEnd: number;
9
+ /** Where the group on the right starts, from the row's left edge; 0 when empty. */
10
+ rightStart: number;
11
+ /** The title's natural one-line width. */
12
+ title: number;
13
+ }
14
+
15
+ /**
16
+ * Where the bar's inline title sits. Like UIKit it is centred on the bar
17
+ * while it fits clear of both side groups; when the wider group would touch
18
+ * it, it centres in the span left between the groups instead of vanishing.
19
+ */
20
+ export function barTitleSpan(box: BarTitleBox): { left: number; right: number } {
21
+ const left = box.leftEnd + BAR_TITLE_GAP;
22
+ const right = (box.rightStart ? box.row - box.rightStart : 0) + BAR_TITLE_GAP;
23
+ const centred = Math.max(left, right);
24
+ return box.row - 2 * centred >= box.title ? { left: centred, right: centred } : { left, right };
25
+ }
26
+
27
+ type Extent = { x: number; width: number };
28
+
29
+ /** The bar's two side groups as physical left and right extents (RTL-safe). */
30
+ export function sideExtents(
31
+ row: number,
32
+ a: Extent,
33
+ b: Extent,
34
+ ): Pick<BarTitleBox, "leftEnd" | "rightStart"> {
35
+ const groups = [a, b].filter((group) => group.width > 0);
36
+ let leftEnd = 0;
37
+ let rightStart = 0;
38
+ for (const group of groups) {
39
+ if (group.x + group.width / 2 < row / 2)
40
+ leftEnd = Math.max(leftEnd, Math.ceil(group.x + group.width));
41
+ else rightStart = rightStart ? Math.min(rightStart, Math.floor(group.x)) : Math.floor(group.x);
42
+ }
43
+ return { leftEnd, rightStart };
44
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * LC-176 (FZ-8442): a glyph on a glass circle sits at the circle's centre.
3
+ * Clay on the Kitchen Sink SHC Nodes story: "the icons inside of these are
4
+ * aligned wrong". The back caret was pulled toward the bar's edge and the "+"
5
+ * pushed past its circle's centre.
6
+ *
7
+ * jsdom has no layout, so the ink centre is computed: the offset each box
8
+ * between the glyph and its circle adds (a margin moves a centred child by
9
+ * half of it, a translate by all of it), plus where the glyph's ink sits in
10
+ * its 256-unit Phosphor grid, read from the rendered path.
11
+ */
12
+ import { IconButton } from "@multiplatform.one/forms";
13
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
14
+ import { Surface } from "@multiplatform.one/theme";
15
+ import { PlusIcon, XIcon } from "@phosphor-icons/react";
16
+ import { cleanup, screen } from "@testing-library/react";
17
+ import { afterEach, describe, expect, it, vi } from "vitest";
18
+ import { PageBackButton, ScreenToolbar } from "./page";
19
+
20
+ const IPHONE =
21
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1";
22
+ const GRID = 256;
23
+ const TOLERANCE_PT = 0.5;
24
+
25
+ type Box = { minX: number; maxX: number; minY: number; maxY: number };
26
+
27
+ function arcExtremes(
28
+ box: Box,
29
+ from: [number, number],
30
+ r: number,
31
+ large: number,
32
+ sweep: number,
33
+ to: [number, number],
34
+ ) {
35
+ const [x1, y1] = from;
36
+ const [x2, y2] = to;
37
+ const mx = (x1 - x2) / 2;
38
+ const my = (y1 - y2) / 2;
39
+ const d2 = mx * mx + my * my;
40
+ const radius = Math.max(r, Math.sqrt(d2));
41
+ const k = (large === sweep ? -1 : 1) * Math.sqrt(Math.max(0, radius ** 2 / d2 - 1));
42
+ const cx = k * my + (x1 + x2) / 2;
43
+ const cy = -k * mx + (y1 + y2) / 2;
44
+ const start = Math.atan2(y1 - cy, x1 - cx);
45
+ let delta = Math.atan2(y2 - cy, x2 - cx) - start;
46
+ if (sweep && delta < 0) delta += 2 * Math.PI;
47
+ if (!sweep && delta > 0) delta -= 2 * Math.PI;
48
+ for (const angle of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
49
+ let t = angle - start;
50
+ while (t < 0) t += 2 * Math.PI;
51
+ while (t >= 2 * Math.PI) t -= 2 * Math.PI;
52
+ const within = delta >= 0 ? t <= delta : t >= 2 * Math.PI + delta || t === 0;
53
+ if (within) include(box, cx + radius * Math.cos(angle), cy + radius * Math.sin(angle));
54
+ }
55
+ }
56
+
57
+ function include(box: Box, x: number, y: number) {
58
+ box.minX = Math.min(box.minX, x);
59
+ box.maxX = Math.max(box.maxX, x);
60
+ box.minY = Math.min(box.minY, y);
61
+ box.maxY = Math.max(box.maxY, y);
62
+ }
63
+
64
+ /** The ink box of an SVG path made of the commands Phosphor uses. */
65
+ function inkBox(d: string): Box {
66
+ const box: Box = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
67
+ const tokens = d.match(/[a-zA-Z]|-?(?:\d*\.\d+|\d+)(?:e-?\d+)?/g) ?? [];
68
+ let i = 0;
69
+ let command = "";
70
+ let x = 0;
71
+ let y = 0;
72
+ let startX = 0;
73
+ let startY = 0;
74
+ const num = () => Number(tokens[i++]);
75
+ while (i < tokens.length) {
76
+ if (/[a-zA-Z]/.test(tokens[i]!)) command = tokens[i++]!;
77
+ const rel = command === command.toLowerCase();
78
+ const ox = rel ? x : 0;
79
+ const oy = rel ? y : 0;
80
+ switch (command.toUpperCase()) {
81
+ case "M":
82
+ x = ox + num();
83
+ y = oy + num();
84
+ startX = x;
85
+ startY = y;
86
+ include(box, x, y);
87
+ command = rel ? "l" : "L";
88
+ break;
89
+ case "L":
90
+ x = ox + num();
91
+ y = oy + num();
92
+ include(box, x, y);
93
+ break;
94
+ case "H":
95
+ x = ox + num();
96
+ include(box, x, y);
97
+ break;
98
+ case "V":
99
+ y = oy + num();
100
+ include(box, x, y);
101
+ break;
102
+ case "C": {
103
+ for (let n = 0; n < 2; n++) include(box, ox + num(), oy + num());
104
+ x = ox + num();
105
+ y = oy + num();
106
+ include(box, x, y);
107
+ break;
108
+ }
109
+ case "A": {
110
+ const r = num();
111
+ num();
112
+ num();
113
+ const large = num();
114
+ const sweep = num();
115
+ const to: [number, number] = [ox + num(), oy + num()];
116
+ arcExtremes(box, [x, y], r, large, sweep, to);
117
+ [x, y] = to;
118
+ include(box, x, y);
119
+ break;
120
+ }
121
+ case "Z":
122
+ x = startX;
123
+ y = startY;
124
+ break;
125
+ default:
126
+ throw new Error(`inkBox: unhandled path command ${command}`);
127
+ }
128
+ }
129
+ return box;
130
+ }
131
+
132
+ const px = (value: string | undefined) => Number.parseFloat(value || "0") || 0;
133
+
134
+ function translate(transform: string, axis: "X" | "Y") {
135
+ const one = transform.match(new RegExp(`translate${axis}\\((-?[\\d.]+)px\\)`));
136
+ if (one) return Number(one[1]);
137
+ const both = transform.match(/translate\((-?[\d.]+)px(?:,\s*(-?[\d.]+)px)?\)/);
138
+ if (!both) return 0;
139
+ return axis === "X" ? Number(both[1]) : Number(both[2] ?? 0);
140
+ }
141
+
142
+ /** How far the glyph's ink centre sits from its circle's centre, in pt. */
143
+ function inkOffset(button: HTMLElement) {
144
+ const svg = button.querySelector("svg");
145
+ if (!svg) throw new Error("no glyph");
146
+ const size = Number(svg.getAttribute("width"));
147
+ const scale = size / GRID;
148
+ const box = inkBox(
149
+ Array.from(svg.querySelectorAll("path"))
150
+ .map((path) => path.getAttribute("d") ?? "")
151
+ .join(" "),
152
+ );
153
+ let dx = ((box.minX + box.maxX) / 2 - GRID / 2) * scale;
154
+ let dy = ((box.minY + box.maxY) / 2 - GRID / 2) * scale;
155
+ const frame = getComputedStyle(button);
156
+ dx += (px(frame.paddingLeft) - px(frame.paddingRight)) / 2;
157
+ dy += (px(frame.paddingTop) - px(frame.paddingBottom)) / 2;
158
+ for (let el: Element | null = svg; el && el !== button; el = el.parentElement) {
159
+ const style = getComputedStyle(el);
160
+ dx +=
161
+ (px(style.marginLeft) +
162
+ px(style.marginInlineStart) -
163
+ px(style.marginRight) -
164
+ px(style.marginInlineEnd)) /
165
+ 2 +
166
+ translate(style.transform, "X");
167
+ dy += (px(style.marginTop) - px(style.marginBottom)) / 2 + translate(style.transform, "Y");
168
+ }
169
+ return { dx, dy, size };
170
+ }
171
+
172
+ afterEach(() => {
173
+ vi.unstubAllGlobals();
174
+ cleanup();
175
+ });
176
+
177
+ describe("glyph centre == glass circle centre (LC-176, FZ-8442)", () => {
178
+ it("the ink box reads Phosphor's bold glyphs where they draw", () => {
179
+ const plus = inkBox(
180
+ "M228,128a12,12,0,0,1-12,12H140v76a12,12,0,0,1-24,0V140H40a12,12,0,0,1,0-24h76V40a12,12,0,0,1,24,0v76h76A12,12,0,0,1,228,128Z",
181
+ );
182
+ expect([plus.minX, plus.maxX, plus.minY, plus.maxY].map(Math.round)).toEqual([
183
+ 28, 228, 28, 228,
184
+ ]);
185
+ const caret = inkBox(
186
+ "M168.49,199.51a12,12,0,0,1-17,17l-80-80a12,12,0,0,1,0-17l80-80a12,12,0,0,1,17,17L97,128Z",
187
+ );
188
+ expect(Math.round((caret.minX + caret.maxX) / 2)).toBe(120);
189
+ });
190
+
191
+ it("off glass the back caret keeps its optical pull, so the measure sees margins", () => {
192
+ renderWithProviders(<ScreenToolbar leading={<PageBackButton />} />);
193
+ const back = screen.getByLabelText("Back");
194
+ expect(back.getAttribute("data-mp-icon-button")).not.toBe("glass");
195
+ const { dx, size } = inkOffset(back);
196
+ expect(dx).toBeLessThan(-size * 0.05);
197
+ });
198
+
199
+ it("back caret, + and ✕ sit at their circle's centre within 0.5pt on iPhone", () => {
200
+ vi.stubGlobal("navigator", { userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 });
201
+ renderWithProviders(
202
+ <Surface bar>
203
+ <ScreenToolbar
204
+ leading={<PageBackButton />}
205
+ trailing={<IconButton icon={PlusIcon} aria-label="Add node" edge="end" />}
206
+ />
207
+ <IconButton icon={XIcon} aria-label="Close" edge="start" />
208
+ </Surface>,
209
+ );
210
+ for (const name of ["Back", "Add node", "Close"]) {
211
+ const button = screen.getByLabelText(name);
212
+ expect(button.getAttribute("data-mp-icon-button"), name).toBe("glass");
213
+ const { dx, dy } = inkOffset(button);
214
+ expect(Math.abs(dx), `${name} dx`).toBeLessThanOrEqual(TOLERANCE_PT);
215
+ expect(Math.abs(dy), `${name} dy`).toBeLessThanOrEqual(TOLERANCE_PT);
216
+ }
217
+ });
218
+ });