@dxos/react-ui 0.8.4-main.8360d9e660 → 0.8.4-main.8baae0fced

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 (300) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/chunk-A5QCIG5R.mjs +24 -0
  4. package/dist/lib/browser/chunk-A5QCIG5R.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-EJSGYGYH.mjs → chunk-LY5XDQR5.mjs} +84 -12
  6. package/dist/lib/browser/chunk-LY5XDQR5.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +1322 -811
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/testing/index.mjs +30 -24
  11. package/dist/lib/browser/testing/index.mjs.map +3 -3
  12. package/dist/lib/browser/translations.mjs +9 -0
  13. package/dist/lib/browser/translations.mjs.map +7 -0
  14. package/dist/lib/node-esm/{chunk-B7MXDDMJ.mjs → chunk-NGKLIKP3.mjs} +84 -12
  15. package/dist/lib/node-esm/chunk-NGKLIKP3.mjs.map +7 -0
  16. package/dist/lib/node-esm/chunk-XCFLA74M.mjs +26 -0
  17. package/dist/lib/node-esm/chunk-XCFLA74M.mjs.map +7 -0
  18. package/dist/lib/node-esm/index.mjs +1322 -811
  19. package/dist/lib/node-esm/index.mjs.map +4 -4
  20. package/dist/lib/node-esm/meta.json +1 -1
  21. package/dist/lib/node-esm/testing/index.mjs +30 -24
  22. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  23. package/dist/lib/node-esm/translations.mjs +10 -0
  24. package/dist/lib/node-esm/translations.mjs.map +7 -0
  25. package/dist/types/src/components/Avatars/Avatar.d.ts +1 -1
  26. package/dist/types/src/components/Avatars/Avatar.d.ts.map +1 -1
  27. package/dist/types/src/components/Avatars/Avatar.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/Avatars/AvatarGroup.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Breadcrumb/Breadcrumb.d.ts.map +1 -1
  30. package/dist/types/src/components/Breadcrumb/Breadcrumb.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Button/Button.d.ts.map +1 -1
  32. package/dist/types/src/components/Button/Button.stories.d.ts +1 -1
  33. package/dist/types/src/components/Button/Button.stories.d.ts.map +1 -1
  34. package/dist/types/src/components/Button/IconButton.d.ts +1 -0
  35. package/dist/types/src/components/Button/IconButton.d.ts.map +1 -1
  36. package/dist/types/src/components/Button/IconButton.stories.d.ts +3 -0
  37. package/dist/types/src/components/Button/IconButton.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Button/Toggle.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/Button/ToggleGroup.d.ts +2 -2
  40. package/dist/types/src/components/Button/ToggleGroup.stories.d.ts +2 -2
  41. package/dist/types/src/components/Button/ToggleGroup.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/Card/Card.d.ts +59 -42
  43. package/dist/types/src/components/Card/Card.d.ts.map +1 -1
  44. package/dist/types/src/components/Card/Card.stories.d.ts +2 -2
  45. package/dist/types/src/components/Card/Card.stories.d.ts.map +1 -1
  46. package/dist/types/src/components/Carousel/Carousel.d.ts +106 -0
  47. package/dist/types/src/components/Carousel/Carousel.d.ts.map +1 -0
  48. package/dist/types/src/components/Carousel/index.d.ts +2 -0
  49. package/dist/types/src/components/Carousel/index.d.ts.map +1 -0
  50. package/dist/types/src/components/Clipboard/ClipboardProvider.d.ts.map +1 -1
  51. package/dist/types/src/components/Clipboard/CopyButton.d.ts.map +1 -1
  52. package/dist/types/src/components/Clipboard/index.d.ts +1 -1
  53. package/dist/types/src/components/Clipboard/index.d.ts.map +1 -1
  54. package/dist/types/src/components/DensityProvider/DensityProvider.d.ts.map +1 -1
  55. package/dist/types/src/components/Dialog/AlertDialog.d.ts +34 -23
  56. package/dist/types/src/components/Dialog/AlertDialog.d.ts.map +1 -1
  57. package/dist/types/src/components/Dialog/AlertDialog.stories.d.ts.map +1 -1
  58. package/dist/types/src/components/Dialog/Dialog.d.ts +38 -21
  59. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -1
  60. package/dist/types/src/components/Dialog/Dialog.stories.d.ts +3 -2
  61. package/dist/types/src/components/Dialog/Dialog.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/ElevationProvider/ElevationProvider.d.ts.map +1 -1
  63. package/dist/types/src/components/ErrorFallback/ErrorFallback.d.ts.map +1 -1
  64. package/dist/types/src/components/ErrorFallback/ErrorFallback.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/ErrorFallback/ErrorStack.d.ts +14 -3
  66. package/dist/types/src/components/ErrorFallback/ErrorStack.d.ts.map +1 -1
  67. package/dist/types/src/components/ErrorFallback/ThrowError.d.ts.map +1 -1
  68. package/dist/types/src/components/Focus/Focus.d.ts +36 -0
  69. package/dist/types/src/components/Focus/Focus.d.ts.map +1 -0
  70. package/dist/types/src/components/Focus/Focus.stories.d.ts +9 -0
  71. package/dist/types/src/components/Focus/Focus.stories.d.ts.map +1 -0
  72. package/dist/types/src/components/Focus/index.d.ts +2 -0
  73. package/dist/types/src/components/Focus/index.d.ts.map +1 -0
  74. package/dist/types/src/components/Icon/Icon.d.ts +4 -0
  75. package/dist/types/src/components/Icon/Icon.d.ts.map +1 -1
  76. package/dist/types/src/components/Icon/Icon.stories.d.ts +11 -3
  77. package/dist/types/src/components/Icon/Icon.stories.d.ts.map +1 -1
  78. package/dist/types/src/components/Image/Image.d.ts +2 -1
  79. package/dist/types/src/components/Image/Image.d.ts.map +1 -1
  80. package/dist/types/src/components/Image/Image.stories.d.ts +3 -2
  81. package/dist/types/src/components/Image/Image.stories.d.ts.map +1 -1
  82. package/dist/types/src/components/Input/Input.d.ts +12 -15
  83. package/dist/types/src/components/Input/Input.d.ts.map +1 -1
  84. package/dist/types/src/components/Input/Input.stories.d.ts +3 -3
  85. package/dist/types/src/components/Input/Input.stories.d.ts.map +1 -1
  86. package/dist/types/src/components/Link/Link.d.ts.map +1 -1
  87. package/dist/types/src/components/Link/Link.stories.d.ts.map +1 -1
  88. package/dist/types/src/components/List/List.d.ts +5 -3
  89. package/dist/types/src/components/List/List.d.ts.map +1 -1
  90. package/dist/types/src/components/List/List.stories.d.ts +3 -1
  91. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  92. package/dist/types/src/components/List/ListDropIndicator.d.ts.map +1 -1
  93. package/dist/types/src/components/List/Tree.d.ts +2 -2
  94. package/dist/types/src/components/List/Tree.d.ts.map +1 -1
  95. package/dist/types/src/components/List/Tree.stories.d.ts.map +1 -1
  96. package/dist/types/src/components/List/TreeDropIndicator.d.ts.map +1 -1
  97. package/dist/types/src/components/List/Treegrid.d.ts +5 -9
  98. package/dist/types/src/components/List/Treegrid.d.ts.map +1 -1
  99. package/dist/types/src/components/List/Treegrid.stories.d.ts.map +1 -1
  100. package/dist/types/src/components/Main/Main.d.ts +7 -3
  101. package/dist/types/src/components/Main/Main.d.ts.map +1 -1
  102. package/dist/types/src/components/Main/Main.stories.d.ts.map +1 -1
  103. package/dist/types/src/components/Main/useSwipeToDismiss.d.ts.map +1 -1
  104. package/dist/types/src/components/MediaPlayer/MediaPlayer.d.ts +46 -0
  105. package/dist/types/src/components/MediaPlayer/MediaPlayer.d.ts.map +1 -0
  106. package/dist/types/src/components/MediaPlayer/MediaPlayer.stories.d.ts +16 -0
  107. package/dist/types/src/components/MediaPlayer/MediaPlayer.stories.d.ts.map +1 -0
  108. package/dist/types/src/components/MediaPlayer/index.d.ts +2 -0
  109. package/dist/types/src/components/MediaPlayer/index.d.ts.map +1 -0
  110. package/dist/types/src/components/Menu/ContextMenu.d.ts.map +1 -1
  111. package/dist/types/src/components/Menu/ContextMenu.stories.d.ts.map +1 -1
  112. package/dist/types/src/components/Menu/DropdownMenu.d.ts +13 -6
  113. package/dist/types/src/components/Menu/DropdownMenu.d.ts.map +1 -1
  114. package/dist/types/src/components/Menu/DropdownMenu.stories.d.ts +9 -1
  115. package/dist/types/src/components/Menu/DropdownMenu.stories.d.ts.map +1 -1
  116. package/dist/types/src/components/Message/Message.d.ts +1 -1
  117. package/dist/types/src/components/Message/Message.d.ts.map +1 -1
  118. package/dist/types/src/components/Message/Message.stories.d.ts +2 -2
  119. package/dist/types/src/components/Message/Message.stories.d.ts.map +1 -1
  120. package/dist/types/src/components/Popover/Popover.d.ts +14 -3
  121. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
  122. package/dist/types/src/components/Popover/Popover.stories.d.ts.map +1 -1
  123. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts +12 -9
  124. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts.map +1 -1
  125. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts +18 -5
  126. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts.map +1 -1
  127. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts +42 -13
  128. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts.map +1 -1
  129. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts +5 -6
  130. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts.map +1 -1
  131. package/dist/types/src/components/Select/Select.d.ts.map +1 -1
  132. package/dist/types/src/components/Select/Select.stories.d.ts +2 -2
  133. package/dist/types/src/components/Select/Select.stories.d.ts.map +1 -1
  134. package/dist/types/src/components/Skeleton/Skeleton.stories.d.ts.map +1 -1
  135. package/dist/types/src/components/Splitter/Splitter.d.ts +19 -17
  136. package/dist/types/src/components/Splitter/Splitter.d.ts.map +1 -1
  137. package/dist/types/src/components/Splitter/Splitter.stories.d.ts.map +1 -1
  138. package/dist/types/src/components/Status/Status.d.ts +3 -4
  139. package/dist/types/src/components/Status/Status.d.ts.map +1 -1
  140. package/dist/types/src/components/Status/Status.stories.d.ts.map +1 -1
  141. package/dist/types/src/components/Tag/Tag.stories.d.ts.map +1 -1
  142. package/dist/types/src/components/ThemeProvider/ThemeProvider.d.ts +1 -1
  143. package/dist/types/src/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  144. package/dist/types/src/components/ThemeProvider/ThemeProvider.stories.d.ts +1 -1
  145. package/dist/types/src/components/ThemeProvider/ThemeProvider.stories.d.ts.map +1 -1
  146. package/dist/types/src/components/ThemeProvider/TranslationsProvider.d.ts +54 -55
  147. package/dist/types/src/components/ThemeProvider/TranslationsProvider.d.ts.map +1 -1
  148. package/dist/types/src/components/ThemeProvider/index.d.ts +1 -1
  149. package/dist/types/src/components/ThemeProvider/index.d.ts.map +1 -1
  150. package/dist/types/src/components/Toast/Toast.d.ts +4 -4
  151. package/dist/types/src/components/Toast/Toast.d.ts.map +1 -1
  152. package/dist/types/src/components/Toast/Toast.stories.d.ts.map +1 -1
  153. package/dist/types/src/components/Toolbar/Toolbar.d.ts +11 -15
  154. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  155. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  156. package/dist/types/src/components/Tooltip/Tooltip.d.ts +8 -8
  157. package/dist/types/src/components/Tooltip/Tooltip.d.ts.map +1 -1
  158. package/dist/types/src/components/Tooltip/Tooltip.stories.d.ts +2 -2
  159. package/dist/types/src/components/Tooltip/Tooltip.stories.d.ts.map +1 -1
  160. package/dist/types/src/components/index.d.ts +3 -0
  161. package/dist/types/src/components/index.d.ts.map +1 -1
  162. package/dist/types/src/exemplars/generics.stories.d.ts +6 -5
  163. package/dist/types/src/exemplars/generics.stories.d.ts.map +1 -1
  164. package/dist/types/src/exemplars/slot.stories.d.ts +1 -0
  165. package/dist/types/src/exemplars/slot.stories.d.ts.map +1 -1
  166. package/dist/types/src/exemplars/tabster.stories.d.ts.map +1 -1
  167. package/dist/types/src/exemplars/virtualizer.stories.d.ts.map +1 -1
  168. package/dist/types/src/hooks/useDensityContext.d.ts +1 -1
  169. package/dist/types/src/hooks/useDensityContext.d.ts.map +1 -1
  170. package/dist/types/src/hooks/useElevationContext.d.ts.map +1 -1
  171. package/dist/types/src/hooks/useIconHref.d.ts.map +1 -1
  172. package/dist/types/src/hooks/useSafeArea.d.ts.map +1 -1
  173. package/dist/types/src/hooks/useSafeCollisionPadding.d.ts.map +1 -1
  174. package/dist/types/src/hooks/useVisualViewport.d.ts.map +1 -1
  175. package/dist/types/src/playground/Controls.stories.d.ts.map +1 -1
  176. package/dist/types/src/playground/Custom.stories.d.ts +1 -1
  177. package/dist/types/src/playground/Custom.stories.d.ts.map +1 -1
  178. package/dist/types/src/playground/Typography.stories.d.ts.map +1 -1
  179. package/dist/types/src/primitives/Column/Column.d.ts +21 -14
  180. package/dist/types/src/primitives/Column/Column.d.ts.map +1 -1
  181. package/dist/types/src/primitives/Column/Column.stories.d.ts +19 -0
  182. package/dist/types/src/primitives/Column/Column.stories.d.ts.map +1 -1
  183. package/dist/types/src/primitives/Container/Container.d.ts +4 -5
  184. package/dist/types/src/primitives/Container/Container.d.ts.map +1 -1
  185. package/dist/types/src/primitives/Container/Container.stories.d.ts.map +1 -1
  186. package/dist/types/src/primitives/Flex/Flex.d.ts +5 -7
  187. package/dist/types/src/primitives/Flex/Flex.d.ts.map +1 -1
  188. package/dist/types/src/primitives/Flex/Flex.stories.d.ts.map +1 -1
  189. package/dist/types/src/primitives/Grid/Grid.d.ts +3 -8
  190. package/dist/types/src/primitives/Grid/Grid.d.ts.map +1 -1
  191. package/dist/types/src/primitives/Grid/Grid.stories.d.ts.map +1 -1
  192. package/dist/types/src/primitives/Panel/Panel.d.ts +24 -15
  193. package/dist/types/src/primitives/Panel/Panel.d.ts.map +1 -1
  194. package/dist/types/src/primitives/Panel/Panel.stories.d.ts.map +1 -1
  195. package/dist/types/src/testing/Loading.d.ts.map +1 -1
  196. package/dist/types/src/testing/decorators/withLayout.d.ts.map +1 -1
  197. package/dist/types/src/testing/decorators/withLayoutVariants.d.ts.map +1 -1
  198. package/dist/types/src/testing/decorators/withTheme.d.ts +1 -1
  199. package/dist/types/src/testing/decorators/withTheme.d.ts.map +1 -1
  200. package/dist/types/src/translations.d.ts +8 -3
  201. package/dist/types/src/translations.d.ts.map +1 -1
  202. package/dist/types/src/util/usePx.d.ts.map +1 -1
  203. package/dist/types/tsconfig.tsbuildinfo +1 -1
  204. package/package.json +29 -26
  205. package/src/components/Avatars/Avatar.stories.tsx +2 -3
  206. package/src/components/Avatars/Avatar.tsx +1 -2
  207. package/src/components/Avatars/AvatarGroup.stories.tsx +0 -1
  208. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +1 -2
  209. package/src/components/Button/Button.stories.tsx +0 -1
  210. package/src/components/Button/Button.tsx +3 -11
  211. package/src/components/Button/IconButton.stories.tsx +6 -4
  212. package/src/components/Button/IconButton.tsx +3 -3
  213. package/src/components/Button/Toggle.stories.tsx +0 -1
  214. package/src/components/Button/ToggleGroup.stories.tsx +0 -1
  215. package/src/components/Card/Card.stories.tsx +15 -15
  216. package/src/components/Card/Card.tsx +190 -66
  217. package/src/components/Carousel/Carousel.tsx +379 -0
  218. package/src/components/Carousel/index.ts +5 -0
  219. package/src/components/Clipboard/CopyButton.tsx +5 -6
  220. package/src/components/Dialog/AlertDialog.stories.tsx +5 -6
  221. package/src/components/Dialog/AlertDialog.tsx +51 -93
  222. package/src/components/Dialog/Dialog.stories.tsx +64 -9
  223. package/src/components/Dialog/Dialog.tsx +66 -56
  224. package/src/components/ErrorFallback/ErrorFallback.stories.tsx +3 -8
  225. package/src/components/ErrorFallback/ErrorStack.tsx +36 -2
  226. package/src/components/Focus/AUDIT.md +43 -0
  227. package/src/components/Focus/Focus.stories.tsx +230 -0
  228. package/src/components/Focus/Focus.tsx +201 -0
  229. package/src/components/Focus/index.ts +5 -0
  230. package/src/components/Icon/Icon.stories.tsx +43 -13
  231. package/src/components/Icon/Icon.tsx +13 -3
  232. package/src/components/Image/Image.stories.tsx +3 -3
  233. package/src/components/Image/Image.tsx +31 -8
  234. package/src/components/Input/Input.stories.tsx +3 -4
  235. package/src/components/Input/Input.tsx +3 -3
  236. package/src/components/Link/Link.stories.tsx +0 -1
  237. package/src/components/Link/Link.tsx +10 -2
  238. package/src/components/List/List.stories.tsx +1 -2
  239. package/src/components/List/List.tsx +7 -6
  240. package/src/components/List/ListDropIndicator.tsx +0 -1
  241. package/src/components/List/Tree.stories.tsx +2 -3
  242. package/src/components/List/Tree.tsx +0 -1
  243. package/src/components/List/Treegrid.stories.tsx +26 -27
  244. package/src/components/List/Treegrid.tsx +14 -14
  245. package/src/components/Main/Main.stories.tsx +0 -1
  246. package/src/components/Main/Main.tsx +0 -1
  247. package/src/components/MediaPlayer/MediaPlayer.stories.tsx +50 -0
  248. package/src/components/MediaPlayer/MediaPlayer.tsx +153 -0
  249. package/src/components/MediaPlayer/index.ts +5 -0
  250. package/src/components/Menu/ContextMenu.stories.tsx +0 -1
  251. package/src/components/Menu/DropdownMenu.stories.tsx +0 -1
  252. package/src/components/Menu/DropdownMenu.tsx +3 -3
  253. package/src/components/Message/Message.stories.tsx +7 -8
  254. package/src/components/Message/Message.tsx +23 -10
  255. package/src/components/Popover/Popover.stories.tsx +4 -5
  256. package/src/components/Popover/Popover.tsx +1 -1
  257. package/src/components/ScrollArea/ScrollArea.stories.tsx +89 -30
  258. package/src/components/ScrollArea/ScrollArea.tsx +39 -23
  259. package/src/components/ScrollContainer/ScrollContainer.stories.tsx +19 -17
  260. package/src/components/ScrollContainer/ScrollContainer.tsx +199 -92
  261. package/src/components/Select/Select.stories.tsx +5 -6
  262. package/src/components/Skeleton/Skeleton.stories.tsx +0 -1
  263. package/src/components/Splitter/Splitter.stories.tsx +29 -29
  264. package/src/components/Splitter/Splitter.tsx +35 -34
  265. package/src/components/Status/Status.stories.tsx +0 -1
  266. package/src/components/Status/Status.tsx +8 -5
  267. package/src/components/Tag/Tag.stories.tsx +0 -1
  268. package/src/components/ThemeProvider/ThemeProvider.stories.tsx +0 -1
  269. package/src/components/ThemeProvider/ThemeProvider.tsx +5 -4
  270. package/src/components/ThemeProvider/index.ts +1 -1
  271. package/src/components/Toast/Toast.stories.tsx +0 -1
  272. package/src/components/Toolbar/Toolbar.stories.tsx +0 -1
  273. package/src/components/Toolbar/Toolbar.tsx +19 -15
  274. package/src/components/Tooltip/Tooltip.stories.tsx +7 -8
  275. package/src/components/Tooltip/Tooltip.tsx +14 -13
  276. package/src/components/index.ts +3 -0
  277. package/src/exemplars/generics.stories.tsx +7 -15
  278. package/src/exemplars/slot.stories.tsx +65 -57
  279. package/src/exemplars/tabster.stories.tsx +1 -1
  280. package/src/exemplars/virtualizer.stories.tsx +4 -5
  281. package/src/hooks/useDensityContext.ts +2 -2
  282. package/src/playground/Custom.stories.tsx +6 -9
  283. package/src/primitives/Column/AUDIT.md +148 -0
  284. package/src/primitives/Column/Column.stories.tsx +122 -19
  285. package/src/primitives/Column/Column.tsx +73 -41
  286. package/src/primitives/Container/Container.stories.tsx +0 -1
  287. package/src/primitives/Container/Container.tsx +5 -8
  288. package/src/primitives/Flex/Flex.stories.tsx +0 -1
  289. package/src/primitives/Flex/Flex.tsx +10 -12
  290. package/src/primitives/Grid/Grid.stories.tsx +0 -1
  291. package/src/primitives/Grid/Grid.tsx +4 -9
  292. package/src/primitives/Panel/Panel.stories.tsx +8 -7
  293. package/src/primitives/Panel/Panel.tsx +64 -63
  294. package/src/testing/Loading.tsx +25 -4
  295. package/src/testing/decorators/withLayout.tsx +7 -17
  296. package/src/testing/decorators/withTheme.tsx +10 -7
  297. package/src/translations.ts +8 -3
  298. package/src/util/usePx.ts +1 -0
  299. package/dist/lib/browser/chunk-EJSGYGYH.mjs.map +0 -7
  300. package/dist/lib/node-esm/chunk-B7MXDDMJ.mjs.map +0 -7
@@ -0,0 +1,230 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { type PropsWithChildren, useState } from 'react';
7
+
8
+ import { mx } from '@dxos/ui-theme';
9
+
10
+ import { withLayout, withTheme } from '../../testing';
11
+ import { type ThemedClassName } from '../../util';
12
+ import { Focus } from './Focus';
13
+
14
+ type Item = { id: string; label: string };
15
+
16
+ const ITEMS: Item[] = [
17
+ { id: '1', label: 'Item 1' },
18
+ { id: '2', label: 'Item 2' },
19
+ { id: '3', label: 'Item 3' },
20
+ { id: '4', label: 'Item 4' },
21
+ { id: '5', label: 'Item 5' },
22
+ ];
23
+
24
+ const itemClassName = 'flex items-center gap-3 px-3 py-2 aria-current:bg-neutral-75 dark:aria-current:bg-neutral-800';
25
+
26
+ const Container = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => {
27
+ return (
28
+ <div className='dx-container grid grid-cols-[1fr_2fr_1fr]'>
29
+ <div className='border-e border-separator' />
30
+ <div className='dx-expander grid grid-rows-[1fr_2fr_1fr]'>
31
+ <div className='border-b border-separator' />
32
+ <div className={mx('h-full flex flex-col gap-2', classNames)}>{children}</div>
33
+ <div className='border-t border-separator' />
34
+ </div>
35
+ <div className='border-s border-separator' />
36
+ </div>
37
+ );
38
+ };
39
+
40
+ const Text = ({ children }: PropsWithChildren) => {
41
+ return <p className='p-1 text-sm text-subdued'>{children}</p>;
42
+ };
43
+
44
+ //
45
+ // Default (vertical list)
46
+ //
47
+
48
+ const DefaultStory = () => {
49
+ const [current, setCurrent] = useState<string | undefined>('1');
50
+
51
+ return (
52
+ <Container>
53
+ <Text>Tab into the group, then use arrow keys to navigate. Press Enter to select.</Text>
54
+ <Focus.Group classNames='h-full'>
55
+ {ITEMS.map((item) => (
56
+ <Focus.Item
57
+ key={item.id}
58
+ current={current === item.id}
59
+ onCurrentChange={() => setCurrent(item.id)}
60
+ className={itemClassName}
61
+ >
62
+ <span className='size-2 bg-primary-500 opacity-0 aria-[current]:opacity-100' />
63
+ <span>{item.label}</span>
64
+ </Focus.Item>
65
+ ))}
66
+ </Focus.Group>
67
+ <Text>Selected: {current ?? 'none'}</Text>
68
+ </Container>
69
+ );
70
+ };
71
+
72
+ //
73
+ // Horizontal group
74
+ //
75
+
76
+ const HorizontalStory = () => {
77
+ const [current, setCurrent] = useState<string | undefined>();
78
+
79
+ return (
80
+ <Container>
81
+ <Text>Horizontal arrow-key navigation between cards.</Text>
82
+ <Focus.Group classNames='h-full flex flex-row gap-2 items-center justify-center' orientation='horizontal'>
83
+ {ITEMS.map((item) => (
84
+ <Focus.Item
85
+ key={item.id}
86
+ current={current === item.id}
87
+ onCurrentChange={() => setCurrent(item.id)}
88
+ className='flex flex-col items-center justify-center w-20 h-20 border border-separator aria-current:border-primary-500 aria-current:bg-primary-50 dark:aria-current:bg-primary-900/20 cursor-pointer'
89
+ >
90
+ <span className='text-xs mt-1'>{item.label}</span>
91
+ </Focus.Item>
92
+ ))}
93
+ </Focus.Group>
94
+ <Text>Selected: {current ?? 'none'}</Text>
95
+ </Container>
96
+ );
97
+ };
98
+
99
+ //
100
+ // Grid (demonstrates border prop and overflow-hidden clipping)
101
+ //
102
+
103
+ const GridCell = ({ border, items }: { border?: boolean; items: Item[] }) => {
104
+ const [current, setCurrent] = useState<string | undefined>();
105
+ return (
106
+ <div className='overflow-hidden bg-base-surface'>
107
+ <Focus.Group classNames='h-full' border={border}>
108
+ {items.map((item) => (
109
+ <Focus.Item
110
+ key={item.id}
111
+ current={current === item.id}
112
+ onCurrentChange={() => setCurrent(item.id)}
113
+ className={itemClassName}
114
+ >
115
+ <span>{item.label}</span>
116
+ </Focus.Item>
117
+ ))}
118
+ </Focus.Group>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ const GridStory = () => {
124
+ return (
125
+ <div className='p-8 space-y-8'>
126
+ <Text>Tab between groups to compare. Each cell has overflow-hidden.</Text>
127
+
128
+ {/* Default vs border. */}
129
+ <div className='grid grid-cols-2 gap-4'>
130
+ <div className='space-y-2'>
131
+ <p className='text-xs font-mono text-subdued'>Default (invisible until focused)</p>
132
+ <div className='h-48'>
133
+ <GridCell items={ITEMS} />
134
+ </div>
135
+ </div>
136
+ <div className='space-y-2'>
137
+ <p className='text-xs font-mono text-subdued'>border (always visible)</p>
138
+ <div className='h-48'>
139
+ <GridCell border items={ITEMS} />
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ {/* Tight grid — border. */}
145
+ <p className='text-xs font-mono text-subdued'>Tight grid — border</p>
146
+ <div className='grid grid-cols-3'>
147
+ {[0, 1, 2].map((col) => (
148
+ <div key={col} className='overflow-hidden'>
149
+ <GridCell border items={ITEMS.slice(0, 3)} />
150
+ </div>
151
+ ))}
152
+ </div>
153
+
154
+ {/* Tight grid — default. */}
155
+ <p className='text-xs font-mono text-subdued'>Tight grid — default</p>
156
+ <div className='grid grid-cols-3'>
157
+ {[0, 1, 2].map((col) => (
158
+ <div key={col} className='overflow-hidden'>
159
+ <GridCell items={ITEMS.slice(0, 3)} />
160
+ </div>
161
+ ))}
162
+ </div>
163
+
164
+ {/* Item-level border. */}
165
+ <p className='text-xs font-mono text-subdued'>Item-level border</p>
166
+ <div className='h-48 overflow-hidden'>
167
+ <Focus.Group classNames='h-full'>
168
+ {ITEMS.slice(0, 3).map((item) => (
169
+ <Focus.Item key={item.id} border className={itemClassName}>
170
+ <span>{item.label}</span>
171
+ </Focus.Item>
172
+ ))}
173
+ </Focus.Group>
174
+ </div>
175
+
176
+ {/* Error state. */}
177
+ <p className='text-xs font-mono text-subdued'>Error state</p>
178
+ <div className='h-48 overflow-hidden'>
179
+ <Focus.Group classNames='h-full' data-focus-state='error'>
180
+ {ITEMS.slice(0, 3).map((item) => (
181
+ <Focus.Item key={item.id} className={itemClassName}>
182
+ <span>{item.label}</span>
183
+ </Focus.Item>
184
+ ))}
185
+ </Focus.Group>
186
+ </div>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ //
192
+ // asChild (slot) usage
193
+ //
194
+
195
+ const AsChildStory = () => {
196
+ const [current, setCurrent] = useState<string | undefined>();
197
+
198
+ return (
199
+ <Container>
200
+ <Text>Focus.Item rendered as a custom element via asChild.</Text>
201
+ <Focus.Group classNames='h-full'>
202
+ {ITEMS.slice(0, 3).map((item) => (
203
+ <Focus.Item key={item.id} asChild current={current === item.id} onCurrentChange={() => setCurrent(item.id)}>
204
+ <button className='flex w-full items-center gap-3 px-3 py-2 text-left hover:bg-neutral-75 dark:hover:bg-neutral-800 aria-current:bg-primary-50 dark:aria-current:bg-primary-900/20 aria-current:text-primary-600'>
205
+ <span>{item.label}</span>
206
+ </button>
207
+ </Focus.Item>
208
+ ))}
209
+ </Focus.Group>
210
+ </Container>
211
+ );
212
+ };
213
+
214
+ //
215
+ // Meta
216
+ //
217
+
218
+ const meta: Meta = {
219
+ title: 'ui/react-ui-core/components/Focus',
220
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
221
+ };
222
+
223
+ export default meta;
224
+
225
+ type Story = StoryObj<typeof meta>;
226
+
227
+ export const Default: Story = { render: DefaultStory };
228
+ export const Horizontal: Story = { render: HorizontalStory };
229
+ export const Grid: Story = { render: GridStory };
230
+ export const AsChild: Story = { render: AsChildStory };
@@ -0,0 +1,201 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import {
6
+ useArrowNavigationGroup,
7
+ useFocusableGroup,
8
+ useMergedTabsterAttributes_unstable,
9
+ } from '@fluentui/react-tabster';
10
+ import { useComposedRefs } from '@radix-ui/react-compose-refs';
11
+ import { Primitive } from '@radix-ui/react-primitive';
12
+ import { Slot } from '@radix-ui/react-slot';
13
+ import React, {
14
+ type FocusEvent,
15
+ type KeyboardEvent,
16
+ type MouseEvent,
17
+ createContext,
18
+ useCallback,
19
+ useContext,
20
+ useRef,
21
+ useState,
22
+ } from 'react';
23
+
24
+ import { composableProps, slottable } from '@dxos/ui-theme';
25
+ import { type Axis } from '@dxos/ui-types';
26
+
27
+ import { useThemeContext } from '../../hooks';
28
+
29
+ //
30
+ // Context
31
+ //
32
+
33
+ type FocusState = 'active' | 'error';
34
+
35
+ const FOCUS_STATE_ATTR = 'focus-state';
36
+
37
+ type ContextValue = {
38
+ setFocus?: (state: FocusState | undefined) => void;
39
+ /** True when any item within the group has DOM focus. */
40
+ groupHasFocus?: boolean;
41
+ };
42
+
43
+ const FocusContext = createContext<ContextValue>({});
44
+
45
+ const useFocus = () => useContext(FocusContext);
46
+
47
+ //
48
+ // Group
49
+ //
50
+
51
+ type GroupProps = {
52
+ orientation?: Axis;
53
+ /** Show a subdued ring when unfocused (e.g., as a cell border). */
54
+ border?: boolean;
55
+ onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
56
+ };
57
+
58
+ /**
59
+ * Provides arrow-key navigation across focusable children via tabster.
60
+ * Does not manage `aria-current` — use `Focus.Item` on each child for that.
61
+ */
62
+ // TODO(wittjosiah): Consider how this could integrate with with react-ui-attention.
63
+ // Perhaps react-ui-attention comes under the mosaic umbrella as it supports selection?
64
+ const Group = slottable<HTMLDivElement, GroupProps>(
65
+ ({ children, asChild, orientation = 'vertical', border = false, ...props }, forwardedRef) => {
66
+ const Comp = asChild ? Slot : Primitive.div;
67
+ const { tx } = useThemeContext();
68
+ const rootRef = useRef<HTMLDivElement>(null);
69
+ const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' });
70
+ const arrowNavigationAttrs = useArrowNavigationGroup({ axis: orientation, memorizeCurrent: true });
71
+ const tabsterAttrs = useMergedTabsterAttributes_unstable(focusableGroupAttrs, arrowNavigationAttrs);
72
+ const [state, setState] = useState<FocusState | undefined>();
73
+ const [groupHasFocus, setGroupHasFocus] = useState(false);
74
+
75
+ const handleFocusIn = useCallback(() => setGroupHasFocus(true), []);
76
+ const handleFocusOut = useCallback((event: FocusEvent<HTMLDivElement>) => {
77
+ const related = event.relatedTarget as HTMLElement | null;
78
+ if (!related || !rootRef.current?.contains(related)) {
79
+ setGroupHasFocus(false);
80
+ }
81
+ }, []);
82
+
83
+ const { className, ...rest } = composableProps(props);
84
+ return (
85
+ <FocusContext.Provider value={{ setFocus: setState, groupHasFocus }}>
86
+ <Comp
87
+ {...rest}
88
+ tabIndex={0}
89
+ className={tx('focus.group', { border }, className)}
90
+ {...tabsterAttrs}
91
+ {...(state && {
92
+ [`data-${FOCUS_STATE_ATTR}`]: state,
93
+ })}
94
+ onBlur={handleFocusOut}
95
+ onFocus={handleFocusIn}
96
+ ref={useComposedRefs<HTMLDivElement>(rootRef, forwardedRef)}
97
+ >
98
+ {children}
99
+ </Comp>
100
+ </FocusContext.Provider>
101
+ );
102
+ },
103
+ );
104
+
105
+ //
106
+ // Item
107
+ //
108
+
109
+ type ItemProps = {
110
+ current?: boolean;
111
+ /** Show a subdued ring when unfocused (e.g., as a cell border). */
112
+ border?: boolean;
113
+ onCurrentChange?: () => void;
114
+ };
115
+
116
+ /**
117
+ * Focusable item within a `Focus.Group`.
118
+ * Uses `useFocusableGroup` so the parent Group's arrow navigation treats this as a single unit
119
+ * (internal buttons are not arrow-navigation targets; Enter/Escape to go in/out).
120
+ * Supports controlled (`current` prop) and uncontrolled (focus-driven) `aria-current`.
121
+ */
122
+ const Item = slottable<HTMLDivElement, ItemProps>(
123
+ (
124
+ { children, asChild, current, border = false, onCurrentChange, onClick, onFocus, onBlur, ...props },
125
+ forwardedRef,
126
+ ) => {
127
+ const Comp = asChild ? Slot : Primitive.div;
128
+ const { tx } = useThemeContext();
129
+ // Tell tabster's groupper to ignore Enter so it doesn't move focus into the group.
130
+ const focusableGroupAttrs = useFocusableGroup({ ignoreDefaultKeydown: { Enter: true } });
131
+ const [focused, setFocused] = useState(false);
132
+
133
+ const handleClick = useCallback(
134
+ (event: MouseEvent<HTMLDivElement>) => {
135
+ onCurrentChange?.();
136
+ onClick?.(event);
137
+ },
138
+ [onCurrentChange, onClick],
139
+ );
140
+
141
+ const handleKeyDown = useCallback(
142
+ (event: KeyboardEvent<HTMLDivElement>) => {
143
+ if (event.key === 'Enter') {
144
+ onCurrentChange?.();
145
+ }
146
+ },
147
+ [onCurrentChange],
148
+ );
149
+
150
+ const handleFocus = useCallback(
151
+ (event: FocusEvent<HTMLDivElement>) => {
152
+ setFocused(true);
153
+ onFocus?.(event);
154
+ },
155
+ [onFocus],
156
+ );
157
+
158
+ const handleBlur = useCallback(
159
+ (event: FocusEvent<HTMLDivElement>) => {
160
+ setFocused(false);
161
+ onBlur?.(event);
162
+ },
163
+ [onBlur],
164
+ );
165
+
166
+ // Controlled `current` prop takes precedence (e.g., virtualized items that scroll back into view).
167
+ // Otherwise fall back to DOM focus state.
168
+ const isCurrent = current ?? focused;
169
+
170
+ const { className, ...rest } = composableProps(props);
171
+ return (
172
+ <Comp
173
+ {...rest}
174
+ tabIndex={0}
175
+ className={tx('focus.item', { border }, className)}
176
+ {...focusableGroupAttrs}
177
+ aria-current={isCurrent || undefined}
178
+ onClick={handleClick}
179
+ onKeyDown={handleKeyDown}
180
+ onFocus={handleFocus}
181
+ onBlur={handleBlur}
182
+ ref={forwardedRef}
183
+ >
184
+ {children}
185
+ </Comp>
186
+ );
187
+ },
188
+ );
189
+
190
+ //
191
+ // Focus
192
+ //
193
+
194
+ export const Focus = {
195
+ Group,
196
+ Item,
197
+ };
198
+
199
+ export type { GroupProps as FocusGroupProps, ItemProps as FocusItemProps };
200
+
201
+ export { useFocus };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './Focus';
@@ -6,10 +6,9 @@ import { IconBase, type IconProps, type IconWeight } from '@phosphor-icons/react
6
6
  import { type Meta, type StoryObj } from '@storybook/react-vite';
7
7
  import React, { type FC, type ReactElement, type SVGProps, forwardRef } from 'react';
8
8
 
9
- import { getSize, mx } from '@dxos/ui-theme';
9
+ import { getSize, iconSize, mx } from '@dxos/ui-theme';
10
10
 
11
11
  import { withTheme } from '../../testing';
12
-
13
12
  import { Icon } from './Icon';
14
13
 
15
14
  /**
@@ -50,29 +49,60 @@ const createIcon = ({
50
49
  return CustomIcon;
51
50
  };
52
51
 
53
- const DefaultStory = ({ CustomIcon }: { CustomIcon: FC<IconProps> }) => {
54
- return (
55
- <div className='grid grid-cols-2 gap-8'>
56
- <CustomIcon weight={'regular'} className={mx(getSize(16))} />
57
- <Icon icon='ph--github-logo--regular' classNames={mx(getSize(16))} />
58
- </div>
59
- );
60
- };
61
-
62
52
  const meta = {
63
53
  title: 'ui/react-ui-core/components/Icon',
64
- render: DefaultStory,
54
+ component: Icon,
65
55
  decorators: [withTheme()],
66
56
  parameters: {
67
57
  layout: 'centered',
68
58
  },
69
- } satisfies Meta<typeof DefaultStory>;
59
+ } satisfies Meta<typeof Icon>;
70
60
 
71
61
  export default meta;
72
62
 
73
63
  type Story = StoryObj<typeof meta>;
74
64
 
75
65
  export const Default: Story = {
66
+ args: {
67
+ icon: 'ph--github-logo--regular',
68
+ },
69
+ };
70
+
71
+ export const Static: Story = {
72
+ args: {
73
+ icon: 'ph--github-logo--regular',
74
+ classNames: getSize(8),
75
+ },
76
+ };
77
+
78
+ export const Dynamic: Story = {
79
+ args: {
80
+ icon: 'ph--github-logo--regular',
81
+ },
82
+ render: (args) => {
83
+ return (
84
+ <div className='flex gap-4'>
85
+ <Icon {...args} />
86
+ <div className='flex gap-4' style={iconSize(8)}>
87
+ <Icon {...args} />
88
+ <div className='flex gap-4' style={iconSize(null)}>
89
+ <Icon {...args} />
90
+ </div>
91
+ </div>
92
+ </div>
93
+ );
94
+ },
95
+ };
96
+
97
+ export const Custom = {
98
+ render: ({ CustomIcon }: { CustomIcon: FC<IconProps> }) => {
99
+ return (
100
+ <div className='grid grid-cols-2 gap-8'>
101
+ <CustomIcon weight={'regular'} className={mx(getSize(16))} />
102
+ <Icon icon='ph--github-logo--regular' classNames={mx(getSize(16))} />
103
+ </div>
104
+ );
105
+ },
76
106
  args: {
77
107
  CustomIcon: createIcon({
78
108
  name: 'GithubLogo',
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Primitive } from '@radix-ui/react-primitive';
6
- import React, { type ComponentPropsWithRef, forwardRef, memo } from 'react';
6
+ import React, { type ComponentPropsWithRef, forwardRef, memo, useMemo } from 'react';
7
7
 
8
8
  import { type Size } from '@dxos/ui-types';
9
9
 
@@ -13,15 +13,25 @@ import { type ThemedClassName } from '../../util';
13
13
  export type IconProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.svg>> & {
14
14
  icon: string;
15
15
  size?: Size;
16
+ synchronized?: boolean;
16
17
  };
17
18
 
19
+ /**
20
+ * The Icon's size can be set directly or inherited from the `--dx-icon-size` CSS variable.
21
+ */
18
22
  export const Icon = memo(
19
- forwardRef<SVGSVGElement, IconProps>(({ icon, classNames, size, ...props }, forwardedRef) => {
23
+ forwardRef<SVGSVGElement, IconProps>(({ classNames, icon, size, synchronized, style, ...props }, forwardedRef) => {
20
24
  const { tx } = useThemeContext();
25
+ const spinDelay = useMemo(() => (synchronized ? `${-(Date.now() % 1_000)}ms` : undefined), [synchronized]);
21
26
  const href = useIconHref(icon);
22
27
 
23
28
  return (
24
- <svg {...props} className={tx('icon.root', { size }, classNames)} ref={forwardedRef}>
29
+ <svg
30
+ {...props}
31
+ style={{ ...style, animationDelay: spinDelay }}
32
+ className={tx('icon.root', { size }, classNames)}
33
+ ref={forwardedRef}
34
+ >
25
35
  <use href={href} />
26
36
  </svg>
27
37
  );
@@ -5,14 +5,14 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useMemo } from 'react';
7
7
 
8
- import { faker } from '@dxos/random';
8
+ import { random } from '@dxos/random';
9
9
  import { withTheme } from '@dxos/react-ui/testing';
10
10
 
11
11
  import { Image } from './Image';
12
12
 
13
13
  const seed = Math.random();
14
14
 
15
- faker.seed(seed);
15
+ random.seed(seed);
16
16
 
17
17
  const meta = {
18
18
  title: 'ui/react-ui-mosaic/Image',
@@ -34,7 +34,7 @@ type Story = StoryObj<typeof meta>;
34
34
 
35
35
  export const Default: Story = {
36
36
  args: {
37
- src: faker.image.url(),
37
+ src: random.image.url(),
38
38
  },
39
39
  };
40
40
 
@@ -13,15 +13,16 @@ export type ImageProps = ThemedClassName<
13
13
  {
14
14
  src: string;
15
15
  alt?: string;
16
+ fit?: 'contain' | 'cover';
16
17
  crossOrigin?: 'anonymous' | 'use-credentials' | '';
17
18
  } & ColorOptions
18
19
  >;
19
20
 
20
- // TODO(burdon): Option for neutral background color.
21
21
  export const Image = ({
22
22
  classNames,
23
23
  src,
24
24
  alt = '',
25
+ fit = 'contain',
25
26
  crossOrigin = 'anonymous',
26
27
  sampleSize = 64,
27
28
  contrast = 0.9,
@@ -68,7 +69,16 @@ export const Image = ({
68
69
 
69
70
  return (
70
71
  <div
71
- className={mx(`relative flex w-full justify-center overflow-hidden transition-all duration-700`, classNames)}
72
+ // `isolate` (`isolation: isolate`) creates a new stacking context so
73
+ // the inner <img>'s `z-10` stays scoped to this wrapper. Without it
74
+ // the z-10 leaks into the parent's stacking context and elevates the
75
+ // image above any pseudo-element rings (e.g. Focus.Item's
76
+ // `dx-ring-pseudo` `::after`) painted on ancestors — most visibly,
77
+ // the focus ring on a Card containing a Card.Poster.
78
+ className={mx(
79
+ `relative flex w-full justify-center overflow-hidden transition-all duration-700 isolate`,
80
+ classNames,
81
+ )}
72
82
  style={{
73
83
  backgroundColor: dominantColor,
74
84
  }}
@@ -94,7 +104,10 @@ export const Image = ({
94
104
  crossOrigin={crossOriginState}
95
105
  onError={handleImageError}
96
106
  onLoad={handleImageLoad}
97
- className='z-10 object-contain transition-opacity duration-500'
107
+ className={mx(
108
+ 'z-10 transition-opacity duration-500',
109
+ fit === 'cover' ? 'w-full h-full object-cover' : 'object-contain',
110
+ )}
98
111
  style={{
99
112
  opacity: imageLoaded ? 1 : 0,
100
113
  }}
@@ -162,7 +175,9 @@ const extractDominantColor = (
162
175
  const alpha = pixels[i + 3];
163
176
 
164
177
  // Skip transparent pixels.
165
- if (alpha === 0) continue;
178
+ if (alpha === 0) {
179
+ continue;
180
+ }
166
181
 
167
182
  // Calculate saturation to weight vibrant colors more.
168
183
  const max = Math.max(red, green, blue);
@@ -202,21 +217,29 @@ const isTransparent = (pixels: Uint8ClampedArray, sampleSize: number, threshold:
202
217
  for (let x = 0; x < sampleSize; x++) {
203
218
  // Top edge.
204
219
  const topIndex = x * 4;
205
- if (pixels[topIndex + 3] === 0) edgeTransparentPixels++;
220
+ if (pixels[topIndex + 3] === 0) {
221
+ edgeTransparentPixels++;
222
+ }
206
223
 
207
224
  // Bottom edge.
208
225
  const bottomIndex = ((sampleSize - 1) * sampleSize + x) * 4;
209
- if (pixels[bottomIndex + 3] === 0) edgeTransparentPixels++;
226
+ if (pixels[bottomIndex + 3] === 0) {
227
+ edgeTransparentPixels++;
228
+ }
210
229
  }
211
230
 
212
231
  for (let y = 1; y < sampleSize - 1; y++) {
213
232
  // Left edge.
214
233
  const leftIndex = y * sampleSize * 4;
215
- if (pixels[leftIndex + 3] === 0) edgeTransparentPixels++;
234
+ if (pixels[leftIndex + 3] === 0) {
235
+ edgeTransparentPixels++;
236
+ }
216
237
 
217
238
  // Right edge.
218
239
  const rightIndex = (y * sampleSize + sampleSize - 1) * 4;
219
- if (pixels[rightIndex + 3] === 0) edgeTransparentPixels++;
240
+ if (pixels[rightIndex + 3] === 0) {
241
+ edgeTransparentPixels++;
242
+ }
220
243
  }
221
244
 
222
245
  return edgeTransparentPixels / edgePixels > threshold;