@sovereignfs/ui 0.45.0 → 0.83.1

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 (323) hide show
  1. package/dist/Accordion.module.css +1 -1
  2. package/dist/Alert.module.css +1 -1
  3. package/dist/AppsLauncher.module.css +96 -0
  4. package/dist/Avatar.module.css +1 -1
  5. package/dist/Badge.module.css +30 -7
  6. package/dist/BalanceChip.module.css +1 -1
  7. package/dist/Button.module.css +18 -2
  8. package/dist/Card.module.css +1 -1
  9. package/dist/CardTile.module.css +181 -0
  10. package/dist/CodeTextarea.module.css +1 -1
  11. package/dist/Collapsible.module.css +1 -1
  12. package/dist/ColorPicker.module.css +132 -0
  13. package/dist/Combobox.module.css +2 -2
  14. package/dist/Command.module.css +1 -1
  15. package/dist/ConfirmDialog.module.css +2 -2
  16. package/dist/ConsentPrompt.module.css +21 -0
  17. package/dist/DatePicker.module.css +1 -1
  18. package/dist/Dialog.module.css +130 -21
  19. package/dist/Drawer.module.css +36 -7
  20. package/dist/Header.module.css +62 -0
  21. package/dist/HeaderFooterLayout.module.css +21 -0
  22. package/dist/IconPicker.module.css +1 -1
  23. package/dist/Input.module.css +1 -1
  24. package/dist/Kbd.module.css +1 -1
  25. package/dist/LegalLinks.module.css +24 -0
  26. package/dist/Markdown.module.css +73 -0
  27. package/dist/Menu.module.css +1 -0
  28. package/dist/Message.module.css +1 -1
  29. package/dist/MessageScroller.module.css +12 -1
  30. package/dist/MobileAppsDrawer.module.css +82 -0
  31. package/dist/MobileFooter.module.css +73 -0
  32. package/dist/MobileHeader.module.css +76 -0
  33. package/dist/NavList.module.css +130 -0
  34. package/dist/NavTabs.module.css +1 -1
  35. package/dist/NotificationsPanel.module.css +258 -0
  36. package/dist/OverlayHeader.module.css +1 -1
  37. package/dist/PageContainer.module.css +82 -0
  38. package/dist/PageHeader.module.css +54 -0
  39. package/dist/PageLayout.module.css +51 -0
  40. package/dist/Popover.module.css +1 -1
  41. package/dist/QuantityStepper.module.css +1 -1
  42. package/dist/Resizable.module.css +3 -3
  43. package/dist/RootLayout.module.css +12 -0
  44. package/dist/SegmentedControl.module.css +1 -1
  45. package/dist/Select.module.css +1 -1
  46. package/dist/Sheet.module.css +4 -1
  47. package/dist/SplitPane.module.css +4 -4
  48. package/dist/StatusBadge.module.css +1 -1
  49. package/dist/StepStrip.module.css +53 -0
  50. package/dist/SuggestionInput.module.css +2 -2
  51. package/dist/SwipableMobileCarousel.module.css +64 -0
  52. package/dist/SwipableMobileCarouselDots.module.css +64 -0
  53. package/dist/SwipableMobileCarouselSlideParts.module.css +29 -0
  54. package/dist/SwipeStack.module.css +101 -0
  55. package/dist/SystemBanner.module.css +10 -0
  56. package/dist/Table.module.css +2 -2
  57. package/dist/Tabs.module.css +1 -1
  58. package/dist/TagInput.module.css +2 -2
  59. package/dist/Textarea.module.css +9 -1
  60. package/dist/ThreeColumnLayout.module.css +35 -0
  61. package/dist/Toast.module.css +62 -3
  62. package/dist/Tooltip.module.css +1 -1
  63. package/dist/UserMenu.module.css +90 -0
  64. package/dist/index.d.ts +1680 -30
  65. package/dist/index.js +4618 -1443
  66. package/dist/tokens/primitives.css +36 -0
  67. package/dist/tokens/semantic.css +32 -0
  68. package/dist/tokens/theme-presets.css +33 -0
  69. package/package.json +32 -12
  70. package/src/__tests__/package-json.test.ts +8 -0
  71. package/src/components/Accordion/Accordion.module.css +1 -1
  72. package/src/components/Alert/Alert.module.css +1 -1
  73. package/src/components/AppsLauncher/AppsLauncher.module.css +96 -0
  74. package/src/components/AppsLauncher/AppsLauncher.stories.tsx +58 -0
  75. package/src/components/AppsLauncher/AppsLauncher.tsx +117 -0
  76. package/src/components/AppsLauncher/__tests__/AppsLauncher.test.tsx +52 -0
  77. package/src/components/Avatar/Avatar.module.css +1 -1
  78. package/src/components/Badge/Badge.module.css +30 -7
  79. package/src/components/Badge/Badge.stories.tsx +16 -4
  80. package/src/components/Badge/Badge.tsx +8 -11
  81. package/src/components/Badge/__tests__/Badge.test.tsx +4 -2
  82. package/src/components/BalanceChip/BalanceChip.module.css +1 -1
  83. package/src/components/Button/Button.module.css +18 -2
  84. package/src/components/Card/Card.module.css +1 -1
  85. package/src/components/CardTile/CardTile.module.css +181 -0
  86. package/src/components/CardTile/CardTile.stories.tsx +86 -0
  87. package/src/components/CardTile/CardTile.tsx +128 -0
  88. package/src/components/CardTile/__tests__/CardTile.test.tsx +56 -0
  89. package/src/components/CodeTextarea/CodeTextarea.module.css +1 -1
  90. package/src/components/Collapsible/Collapsible.module.css +1 -1
  91. package/src/components/ColorPicker/ColorPicker.module.css +132 -0
  92. package/src/components/ColorPicker/ColorPicker.stories.tsx +80 -0
  93. package/src/components/ColorPicker/ColorPicker.tsx +157 -0
  94. package/src/components/ColorPicker/__tests__/ColorPicker.test.tsx +117 -0
  95. package/src/components/Combobox/Combobox.module.css +2 -2
  96. package/src/components/Command/Command.module.css +1 -1
  97. package/src/components/ConfirmDialog/ConfirmDialog.module.css +2 -2
  98. package/src/components/ConfirmDialog/ConfirmDialog.tsx +28 -2
  99. package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +71 -3
  100. package/src/components/ConsentPrompt/ConsentPrompt.module.css +21 -0
  101. package/src/components/ConsentPrompt/ConsentPrompt.stories.tsx +31 -0
  102. package/src/components/ConsentPrompt/ConsentPrompt.tsx +66 -0
  103. package/src/components/ContextMenu/ContextMenu.tsx +1 -1
  104. package/src/components/DatePicker/DatePicker.module.css +1 -1
  105. package/src/components/DeviceOnlyGate/DeviceOnlyGate.tsx +58 -0
  106. package/src/components/DeviceOnlyGate/__tests__/DeviceOnlyGate.test.tsx +45 -0
  107. package/src/components/DeviceStorageKeyGate/DeviceStorageKeyGate.tsx +100 -0
  108. package/src/components/DeviceStorageKeyGate/__tests__/DeviceStorageKeyGate.test.tsx +119 -0
  109. package/src/components/Dialog/Dialog.module.css +130 -21
  110. package/src/components/Dialog/Dialog.stories.tsx +179 -2
  111. package/src/components/Dialog/Dialog.tsx +137 -94
  112. package/src/components/Dialog/__tests__/Dialog.test.tsx +190 -9
  113. package/src/components/DragHandleRow/DragHandleRow.tsx +2 -21
  114. package/src/components/Drawer/Drawer.module.css +36 -7
  115. package/src/components/Drawer/Drawer.stories.tsx +44 -0
  116. package/src/components/Drawer/Drawer.tsx +27 -66
  117. package/src/components/Drawer/__tests__/Drawer.test.tsx +55 -6
  118. package/src/components/GripIcon/GripIcon.tsx +27 -0
  119. package/src/components/Header/Header.module.css +62 -0
  120. package/src/components/Header/Header.stories.tsx +452 -0
  121. package/src/components/Header/Header.tsx +102 -0
  122. package/src/components/Header/__tests__/Header.test.tsx +104 -0
  123. package/src/components/HeaderFooterLayout/HeaderFooterLayout.module.css +21 -0
  124. package/src/components/HeaderFooterLayout/HeaderFooterLayout.stories.tsx +111 -0
  125. package/src/components/HeaderFooterLayout/HeaderFooterLayout.tsx +54 -0
  126. package/src/components/HeaderFooterLayout/__tests__/HeaderFooterLayout.test.tsx +58 -0
  127. package/src/components/Icon/icons/arrow-left-right.tsx +23 -0
  128. package/src/components/Icon/icons/bold.tsx +20 -0
  129. package/src/components/Icon/icons/calendar.tsx +4 -4
  130. package/src/components/Icon/icons/carrot.tsx +3 -3
  131. package/src/components/Icon/icons/circle-chevron-left.tsx +21 -0
  132. package/src/components/Icon/icons/code.tsx +21 -0
  133. package/src/components/Icon/icons/copy.tsx +21 -0
  134. package/src/components/Icon/icons/credit-card.tsx +21 -0
  135. package/src/components/Icon/icons/database.tsx +22 -0
  136. package/src/components/Icon/icons/download.tsx +22 -0
  137. package/src/components/Icon/icons/file-text.tsx +24 -0
  138. package/src/components/Icon/icons/folder-closed.tsx +21 -0
  139. package/src/components/Icon/icons/folder-open.tsx +20 -0
  140. package/src/components/Icon/icons/folder.tsx +20 -0
  141. package/src/components/Icon/icons/folders.tsx +21 -0
  142. package/src/components/Icon/icons/hat-glasses.tsx +24 -0
  143. package/src/components/Icon/icons/history.tsx +22 -0
  144. package/src/components/Icon/icons/inbox.tsx +21 -0
  145. package/src/components/Icon/icons/index.ts +169 -1
  146. package/src/components/Icon/icons/italic.tsx +22 -0
  147. package/src/components/Icon/icons/layers.tsx +22 -0
  148. package/src/components/Icon/icons/layout-dashboard.tsx +23 -0
  149. package/src/components/Icon/icons/layout-grid.tsx +23 -0
  150. package/src/components/Icon/icons/link.tsx +21 -0
  151. package/src/components/Icon/icons/list-ordered.tsx +25 -0
  152. package/src/components/Icon/icons/list.tsx +25 -0
  153. package/src/components/Icon/icons/luggage.tsx +24 -0
  154. package/src/components/Icon/icons/map-pin.tsx +21 -0
  155. package/src/components/Icon/icons/menu.tsx +22 -0
  156. package/src/components/Icon/icons/minus.tsx +20 -0
  157. package/src/components/Icon/icons/paint-bucket.tsx +23 -0
  158. package/src/components/Icon/icons/panel-left.tsx +21 -0
  159. package/src/components/Icon/icons/pin.tsx +21 -0
  160. package/src/components/Icon/icons/rectangle-ellipsis.tsx +23 -0
  161. package/src/components/Icon/icons/refresh-cw.tsx +23 -0
  162. package/src/components/Icon/icons/rotate-cw.tsx +21 -0
  163. package/src/components/Icon/icons/route.tsx +22 -0
  164. package/src/components/Icon/icons/send.tsx +21 -0
  165. package/src/components/Icon/icons/share-2.tsx +24 -0
  166. package/src/components/Icon/icons/sheet.tsx +24 -0
  167. package/src/components/Icon/icons/smartphone.tsx +21 -0
  168. package/src/components/Icon/icons/square-kanban.tsx +23 -0
  169. package/src/components/Icon/icons/table.tsx +23 -0
  170. package/src/components/Icon/icons/user-round-plus.tsx +23 -0
  171. package/src/components/Icon/icons/users.tsx +23 -0
  172. package/src/components/IconPicker/IconPicker.module.css +1 -1
  173. package/src/components/Input/Input.module.css +1 -1
  174. package/src/components/Input/Input.tsx +8 -2
  175. package/src/components/Kbd/Kbd.module.css +1 -1
  176. package/src/components/LegalLinks/LegalLinks.module.css +24 -0
  177. package/src/components/LegalLinks/LegalLinks.stories.tsx +22 -0
  178. package/src/components/LegalLinks/LegalLinks.tsx +40 -0
  179. package/src/components/Markdown/Markdown.module.css +73 -0
  180. package/src/components/Markdown/Markdown.stories.tsx +40 -0
  181. package/src/components/Markdown/Markdown.tsx +305 -0
  182. package/src/components/Markdown/__tests__/Markdown.test.tsx +144 -0
  183. package/src/components/Menu/Menu.module.css +1 -0
  184. package/src/components/Menu/Menu.tsx +72 -22
  185. package/src/components/Menu/__tests__/Menu.test.tsx +30 -0
  186. package/src/components/Menubar/Menubar.tsx +1 -1
  187. package/src/components/Message/Message.module.css +1 -1
  188. package/src/components/MessageScroller/MessageScroller.module.css +12 -1
  189. package/src/components/MessageScroller/MessageScroller.tsx +12 -0
  190. package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +82 -0
  191. package/src/components/MobileAppsDrawer/MobileAppsDrawer.tsx +78 -0
  192. package/src/components/MobileAppsDrawer/__tests__/MobileAppsDrawer.test.tsx +80 -0
  193. package/src/components/MobileFooter/MobileFooter.module.css +73 -0
  194. package/src/components/MobileFooter/MobileFooter.stories.tsx +47 -0
  195. package/src/components/MobileFooter/MobileFooter.tsx +122 -0
  196. package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +129 -0
  197. package/src/components/MobileHeader/MobileHeader.module.css +76 -0
  198. package/src/components/MobileHeader/MobileHeader.stories.tsx +57 -0
  199. package/src/components/MobileHeader/MobileHeader.tsx +90 -0
  200. package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +113 -0
  201. package/src/components/NavList/NavList.module.css +130 -0
  202. package/src/components/NavList/NavList.tsx +136 -0
  203. package/src/components/NavList/__tests__/NavList.test.tsx +109 -0
  204. package/src/components/NavTabs/NavTabs.module.css +1 -1
  205. package/src/components/NavTabs/NavTabs.tsx +39 -11
  206. package/src/components/NotificationsPanel/NotificationsPanel.module.css +258 -0
  207. package/src/components/NotificationsPanel/NotificationsPanel.stories.tsx +61 -0
  208. package/src/components/NotificationsPanel/NotificationsPanel.tsx +187 -0
  209. package/src/components/NotificationsPanel/__tests__/NotificationsPanel.test.tsx +78 -0
  210. package/src/components/OfflineGate/OfflineGate.tsx +44 -0
  211. package/src/components/OfflineGate/__tests__/OfflineGate.test.tsx +66 -0
  212. package/src/components/OverlayHeader/OverlayHeader.module.css +1 -1
  213. package/src/components/OverlayHeader/OverlayHeader.tsx +19 -4
  214. package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +18 -0
  215. package/src/components/PageContainer/PageContainer.module.css +82 -0
  216. package/src/components/PageContainer/PageContainer.tsx +64 -0
  217. package/src/components/PageContainer/__tests__/PageContainer.test.tsx +70 -0
  218. package/src/components/PageHeader/PageHeader.module.css +54 -0
  219. package/src/components/PageHeader/PageHeader.tsx +157 -2
  220. package/src/components/PageHeader/__tests__/PageHeader.test.tsx +103 -0
  221. package/src/components/PageLayout/PageLayout.module.css +51 -0
  222. package/src/components/PageLayout/PageLayout.stories.tsx +73 -0
  223. package/src/components/PageLayout/PageLayout.tsx +51 -0
  224. package/src/components/PageLayout/__tests__/PageLayout.test.tsx +34 -0
  225. package/src/components/Popover/Popover.module.css +1 -1
  226. package/src/components/Popover/Popover.tsx +86 -4
  227. package/src/components/Popover/__tests__/Popover.test.tsx +165 -0
  228. package/src/components/QuantityStepper/QuantityStepper.module.css +1 -1
  229. package/src/components/Resizable/Resizable.module.css +3 -3
  230. package/src/components/ResponsiveSurface/ResponsiveSurface.tsx +24 -0
  231. package/src/components/ResponsiveSurface/__tests__/ResponsiveSurface.test.tsx +73 -0
  232. package/src/components/RootLayout/RootLayout.module.css +12 -0
  233. package/src/components/RootLayout/RootLayout.stories.tsx +125 -0
  234. package/src/components/RootLayout/RootLayout.tsx +185 -0
  235. package/src/components/RootLayout/__tests__/RootLayout.test.tsx +161 -0
  236. package/src/components/SegmentedControl/SegmentedControl.module.css +1 -1
  237. package/src/components/Select/Select.module.css +1 -1
  238. package/src/components/Sheet/Sheet.module.css +4 -1
  239. package/src/components/Sheet/Sheet.tsx +14 -56
  240. package/src/components/SplitPane/SplitPane.module.css +4 -4
  241. package/src/components/StatusBadge/StatusBadge.module.css +1 -1
  242. package/src/components/StepStrip/StepStrip.module.css +53 -0
  243. package/src/components/StepStrip/StepStrip.tsx +74 -0
  244. package/src/components/SuggestionInput/SuggestionInput.module.css +2 -2
  245. package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +64 -0
  246. package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +243 -0
  247. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlide.tsx +35 -0
  248. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody.tsx +42 -0
  249. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter.tsx +18 -0
  250. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideHeader.tsx +21 -0
  251. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideParts.module.css +29 -0
  252. package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +214 -0
  253. package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarouselDevGuard.test.tsx +61 -0
  254. package/src/components/SwipableMobileCarousel/context.ts +13 -0
  255. package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +64 -0
  256. package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +73 -0
  257. package/src/components/SwipableMobileCarouselDots/__tests__/SwipableMobileCarouselDots.test.tsx +63 -0
  258. package/src/components/SwipeStack/SwipeStack.module.css +101 -0
  259. package/src/components/SwipeStack/SwipeStack.tsx +199 -0
  260. package/src/components/SwipeStack/SwipeStackCard.tsx +22 -0
  261. package/src/components/SwipeStack/__tests__/SwipeStack.test.tsx +169 -0
  262. package/src/components/SystemBanner/SystemBanner.module.css +10 -0
  263. package/src/components/Table/Table.module.css +2 -2
  264. package/src/components/Tabs/Tabs.module.css +1 -1
  265. package/src/components/TagInput/TagInput.module.css +2 -2
  266. package/src/components/Textarea/Textarea.module.css +9 -1
  267. package/src/components/Textarea/Textarea.tsx +28 -5
  268. package/src/components/Textarea/__tests__/Textarea.test.tsx +23 -1
  269. package/src/components/ThreeColumnLayout/ThreeColumnLayout.module.css +35 -0
  270. package/src/components/ThreeColumnLayout/ThreeColumnLayout.stories.tsx +152 -0
  271. package/src/components/ThreeColumnLayout/ThreeColumnLayout.tsx +87 -0
  272. package/src/components/ThreeColumnLayout/__tests__/ThreeColumnLayout.test.tsx +78 -0
  273. package/src/components/Toast/Toast.module.css +62 -3
  274. package/src/components/Toast/Toast.stories.tsx +1 -1
  275. package/src/components/Tooltip/Tooltip.module.css +1 -1
  276. package/src/components/UserMenu/UserMenu.module.css +90 -0
  277. package/src/components/UserMenu/UserMenu.stories.tsx +49 -0
  278. package/src/components/UserMenu/UserMenu.tsx +132 -0
  279. package/src/components/UserMenu/__tests__/UserMenu.test.tsx +77 -0
  280. package/src/hooks/__tests__/useCarouselRouteSync.test.tsx +126 -0
  281. package/src/hooks/__tests__/useIsOffline.test.tsx +53 -0
  282. package/src/hooks/__tests__/useOfflineTileState.test.tsx +69 -0
  283. package/src/hooks/__tests__/usePublishShellChromeHeight.test.tsx +118 -0
  284. package/src/hooks/__tests__/useReorderSensors.test.ts +26 -0
  285. package/src/hooks/__tests__/useResponsiveLayout.test.tsx +87 -0
  286. package/src/hooks/__tests__/useSnapCarousel.test.tsx +175 -0
  287. package/src/hooks/__tests__/useSwipeReveal.test.tsx +110 -0
  288. package/src/hooks/__tests__/useSwipeStack.test.tsx +155 -0
  289. package/src/hooks/index.ts +27 -0
  290. package/src/hooks/useCarouselRouteSync.ts +92 -0
  291. package/src/hooks/useIsOffline.ts +41 -0
  292. package/src/hooks/useOfflineTileState.ts +45 -0
  293. package/src/hooks/usePublishShellChromeHeight.ts +96 -0
  294. package/src/hooks/useReorderSensors.ts +88 -0
  295. package/src/hooks/useResponsiveLayout.ts +36 -0
  296. package/src/hooks/useSnapCarousel.ts +284 -0
  297. package/src/hooks/useSwipeReveal.ts +107 -0
  298. package/src/hooks/useSwipeStack.ts +235 -0
  299. package/src/index.ts +124 -3
  300. package/src/overlay-shell.ts +159 -0
  301. package/src/stories/DesignSystemOverview.stories.tsx +933 -6
  302. package/src/stories/DeviceOnlyGate.stories.tsx +35 -0
  303. package/src/stories/DeviceStorageKeyGate.stories.tsx +57 -0
  304. package/src/stories/GripIcon.stories.tsx +29 -0
  305. package/src/stories/InteractionHooks.stories.tsx +483 -0
  306. package/src/stories/LayoutSystem.stories.tsx +639 -0
  307. package/src/stories/MenuEntries.stories.tsx +86 -0
  308. package/src/stories/MobilePatterns.stories.tsx +247 -2
  309. package/src/stories/NavList.stories.tsx +147 -0
  310. package/src/stories/NavTabs.stories.tsx +18 -0
  311. package/src/stories/OfflineGate.stories.tsx +42 -0
  312. package/src/stories/PageContainer.stories.tsx +82 -0
  313. package/src/stories/PageHeader.stories.tsx +65 -1
  314. package/src/stories/ResponsiveSurface.stories.tsx +47 -0
  315. package/src/stories/StepStrip.stories.tsx +76 -0
  316. package/src/stories/SwipableMobileCarousel.stories.tsx +362 -0
  317. package/src/stories/SwipeStack.stories.tsx +200 -0
  318. package/src/stories/Textarea.stories.tsx +12 -0
  319. package/src/stories/TokenGallery.stories.tsx +115 -0
  320. package/src/tokens/primitives.css +36 -0
  321. package/src/tokens/semantic.css +32 -0
  322. package/src/tokens/theme-presets.css +33 -0
  323. package/src/tokens/theme-presets.ts +72 -0
@@ -0,0 +1,155 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { cleanup, fireEvent, render } from '@testing-library/react';
4
+ import { useSwipeStack, type SwipeDirection, type UseSwipeStackOptions } from '../useSwipeStack';
5
+
6
+ // jsdom doesn't implement pointer capture — stub it as a no-op so
7
+ // `e.currentTarget.setPointerCapture(...)` doesn't throw.
8
+ beforeEach(() => {
9
+ if (!HTMLElement.prototype.setPointerCapture) {
10
+ HTMLElement.prototype.setPointerCapture = vi.fn();
11
+ }
12
+ });
13
+
14
+ afterEach(cleanup);
15
+
16
+ function Harness(props: UseSwipeStackOptions & { triggerDirection?: SwipeDirection }) {
17
+ const { triggerDirection, ...options } = props;
18
+ const { cardRef, wrapRef, handlers, touchAction, triggerCommit } = useSwipeStack(options);
19
+ return (
20
+ <div data-testid="wrap" ref={wrapRef}>
21
+ <div data-testid="card" ref={cardRef} data-touch-action={touchAction} {...handlers}>
22
+ card
23
+ </div>
24
+ {triggerDirection && (
25
+ <button data-testid="trigger" onClick={() => triggerCommit(triggerDirection)}>
26
+ trigger
27
+ </button>
28
+ )}
29
+ </div>
30
+ );
31
+ }
32
+
33
+ function pointer(x: number, y: number) {
34
+ return { clientX: x, clientY: y, pointerId: 1 };
35
+ }
36
+
37
+ function drag(el: HTMLElement, dx: number, dy: number) {
38
+ fireEvent.pointerDown(el, pointer(0, 0));
39
+ fireEvent.pointerMove(el, pointer(dx, dy));
40
+ fireEvent.pointerUp(el, pointer(dx, dy));
41
+ }
42
+
43
+ describe('useSwipeStack', () => {
44
+ it('commits when a drag crosses threshold in a live direction', () => {
45
+ const onCommit = vi.fn();
46
+ const { getByTestId } = render(
47
+ <Harness directions={['up', 'down', 'left', 'right']} threshold={100} onCommit={onCommit} />,
48
+ );
49
+ drag(getByTestId('card'), 0, -120);
50
+ expect(onCommit).toHaveBeenCalledWith('up');
51
+ expect(onCommit).toHaveBeenCalledOnce();
52
+ });
53
+
54
+ it('springs back without committing when under threshold', () => {
55
+ const onCommit = vi.fn();
56
+ const { getByTestId } = render(
57
+ <Harness directions={['up', 'down', 'left', 'right']} threshold={100} onCommit={onCommit} />,
58
+ );
59
+ drag(getByTestId('card'), 0, -40);
60
+ expect(onCommit).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it('does not commit within the deadzone even if threshold is set very low', () => {
64
+ const onCommit = vi.fn();
65
+ const { getByTestId } = render(
66
+ <Harness directions={['up']} threshold={2} onCommit={onCommit} />,
67
+ );
68
+ drag(getByTestId('card'), 0, -5);
69
+ expect(onCommit).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it('treats a non-configured direction as a wall — no commit even past what would be threshold distance', () => {
73
+ const onCommit = vi.fn();
74
+ const { getByTestId } = render(
75
+ <Harness directions={['up', 'down']} threshold={100} onCommit={onCommit} />,
76
+ );
77
+ drag(getByTestId('card'), 150, 0);
78
+ expect(onCommit).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it('does nothing when disabled', () => {
82
+ const onCommit = vi.fn();
83
+ const { getByTestId } = render(
84
+ <Harness
85
+ directions={['up', 'down', 'left', 'right']}
86
+ threshold={100}
87
+ onCommit={onCommit}
88
+ disabled
89
+ />,
90
+ );
91
+ drag(getByTestId('card'), 0, -150);
92
+ expect(onCommit).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it('ignores pointerup without a preceding pointerdown', () => {
96
+ const onCommit = vi.fn();
97
+ const { getByTestId } = render(
98
+ <Harness directions={['up', 'down', 'left', 'right']} threshold={100} onCommit={onCommit} />,
99
+ );
100
+ fireEvent.pointerUp(getByTestId('card'), pointer(0, -150));
101
+ expect(onCommit).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it('triggerCommit commits a live direction without a drag', () => {
105
+ const onCommit = vi.fn();
106
+ const { getByTestId } = render(
107
+ <Harness directions={['up']} onCommit={onCommit} triggerDirection="up" />,
108
+ );
109
+ fireEvent.click(getByTestId('trigger'));
110
+ expect(onCommit).toHaveBeenCalledWith('up');
111
+ expect(onCommit).toHaveBeenCalledOnce();
112
+ });
113
+
114
+ it('triggerCommit no-ops for a direction that is not live', () => {
115
+ const onCommit = vi.fn();
116
+ const { getByTestId } = render(
117
+ <Harness directions={['up']} onCommit={onCommit} triggerDirection="down" />,
118
+ );
119
+ fireEvent.click(getByTestId('trigger'));
120
+ expect(onCommit).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it('triggerCommit does nothing when disabled', () => {
124
+ const onCommit = vi.fn();
125
+ const { getByTestId } = render(
126
+ <Harness directions={['up']} onCommit={onCommit} triggerDirection="up" disabled />,
127
+ );
128
+ fireEvent.click(getByTestId('trigger'));
129
+ expect(onCommit).not.toHaveBeenCalled();
130
+ });
131
+
132
+ describe('touchAction', () => {
133
+ it('is none when both axes have a live direction', () => {
134
+ const { getByTestId } = render(
135
+ <Harness directions={['left', 'right', 'up', 'down']} onCommit={vi.fn()} />,
136
+ );
137
+ expect(getByTestId('card').dataset.touchAction).toBe('none');
138
+ });
139
+
140
+ it('is pan-y when only horizontal directions are live', () => {
141
+ const { getByTestId } = render(<Harness directions={['left', 'right']} onCommit={vi.fn()} />);
142
+ expect(getByTestId('card').dataset.touchAction).toBe('pan-y');
143
+ });
144
+
145
+ it('is pan-x when only vertical directions are live', () => {
146
+ const { getByTestId } = render(<Harness directions={['up', 'down']} onCommit={vi.fn()} />);
147
+ expect(getByTestId('card').dataset.touchAction).toBe('pan-x');
148
+ });
149
+
150
+ it('is auto when no directions are live', () => {
151
+ const { getByTestId } = render(<Harness directions={[]} onCommit={vi.fn()} />);
152
+ expect(getByTestId('card').dataset.touchAction).toBe('auto');
153
+ });
154
+ });
155
+ });
@@ -2,5 +2,32 @@ export { useLongPress } from './useLongPress';
2
2
  export type { UseLongPressOptions, LongPressHandlers } from './useLongPress';
3
3
  export { useDoubleTapHandler, useSingleOrDoubleTap } from './useDoubleTap';
4
4
  export { useIsMobile, MOBILE_BREAKPOINT_PX } from './useIsMobile';
5
+ export { useIsOffline } from './useIsOffline';
6
+ export { useOfflineTileState } from './useOfflineTileState';
7
+ export type { OfflineTileState } from './useOfflineTileState';
5
8
  export { useCommitOnEnterOrBlur } from './useCommitOnEnterOrBlur';
6
9
  export type { CommitOnEnterOrBlurHandlers } from './useCommitOnEnterOrBlur';
10
+ export { useSwipeReveal } from './useSwipeReveal';
11
+ export type {
12
+ UseSwipeRevealOptions,
13
+ UseSwipeRevealHandlers,
14
+ UseSwipeRevealResult,
15
+ } from './useSwipeReveal';
16
+ export { useSnapCarousel } from './useSnapCarousel';
17
+ export type { UseSnapCarouselOptions, UseSnapCarouselResult } from './useSnapCarousel';
18
+ export { useResponsiveLayout } from './useResponsiveLayout';
19
+ export type { UseResponsiveLayoutOptions, UseResponsiveLayoutResult } from './useResponsiveLayout';
20
+ export { useCarouselRouteSync } from './useCarouselRouteSync';
21
+ export type {
22
+ UseCarouselRouteSyncOptions,
23
+ UseCarouselRouteSyncResult,
24
+ } from './useCarouselRouteSync';
25
+ export { useReorderSensors, shouldHandleDndEvent } from './useReorderSensors';
26
+ export type { UseReorderSensorsOptions } from './useReorderSensors';
27
+ export { useSwipeStack, SWIPE_STACK_TRANSITION_MS } from './useSwipeStack';
28
+ export type {
29
+ SwipeDirection,
30
+ UseSwipeStackOptions,
31
+ UseSwipeStackHandlers,
32
+ UseSwipeStackResult,
33
+ } from './useSwipeStack';
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+
5
+ export interface UseCarouselRouteSyncOptions {
6
+ /** Maps the current pathname to a slide index — the caller's own routing
7
+ * logic (e.g. special-casing a virtual "starred" slide), unchanged from
8
+ * whatever a plugin already has today. This hook has no idea what a
9
+ * plugin's routes look like, by design — it never imports a router. */
10
+ indexForPathname: (pathname: string) => number;
11
+ /** Inverse mapping — given a settled slide index, the path to navigate to. */
12
+ pathForIndex: (index: number) => string;
13
+ /** The current pathname. The caller reads this via their own router (e.g.
14
+ * `usePathname()` from `next/navigation`) and passes it in every render —
15
+ * this package stays framework-generic and has zero runtime dependencies. */
16
+ pathname: string;
17
+ /** Called when this hook wants to navigate — typically
18
+ * `(path) => router.replace(path, { scroll: false })`. */
19
+ onNavigate: (path: string) => void;
20
+ }
21
+
22
+ export interface UseCarouselRouteSyncResult {
23
+ /** Feed directly into SwipableMobileCarousel's `activeIndex` prop. */
24
+ activeIndex: number;
25
+ /** Feed directly into SwipableMobileCarousel's `onSettle` prop. */
26
+ onSettle: (index: number) => void;
27
+ }
28
+
29
+ /**
30
+ * useCarouselRouteSync — centralizes the pathname↔slide-index mapping and the
31
+ * "was this pathname change our own settle, or an external navigation"
32
+ * distinction that carousel-driving plugins otherwise hand-roll per plugin
33
+ * (a tapped `<Link>`, browser back/forward, vs. this hook's own
34
+ * `router.replace` after a swipe settles). Router-agnostic on purpose: the
35
+ * caller owns the actual `usePathname()`/`router.replace()` calls and only
36
+ * hands this hook plain values/callbacks.
37
+ *
38
+ * Deliberately does not own neighbor-prefetch (SwipableMobileCarousel's
39
+ * `prefetchDistance`/mount-window covers that) or scroll mechanics (the
40
+ * carousel component reacts to `activeIndex` changes itself) — this hook is
41
+ * pathname↔index mapping only, keeping routing glue decoupled from
42
+ * data-fetching and DOM scrolling.
43
+ */
44
+ export function useCarouselRouteSync({
45
+ indexForPathname,
46
+ pathForIndex,
47
+ pathname,
48
+ onNavigate,
49
+ }: UseCarouselRouteSyncOptions): UseCarouselRouteSyncResult {
50
+ // Kept in refs and refreshed every render (not effect deps) so an inline
51
+ // arrow function passed by the caller each render doesn't retrigger the
52
+ // pathname-sync effect below — only an actual pathname change should.
53
+ const indexForPathnameRef = useRef(indexForPathname);
54
+ indexForPathnameRef.current = indexForPathname;
55
+ const pathForIndexRef = useRef(pathForIndex);
56
+ pathForIndexRef.current = pathForIndex;
57
+ const onNavigateRef = useRef(onNavigate);
58
+ onNavigateRef.current = onNavigate;
59
+
60
+ const [activeIndex, setActiveIndex] = useState(() => indexForPathname(pathname));
61
+ // Set synchronously inside onSettle, right before this hook's own
62
+ // navigation call — lets the pathname-sync effect below tell "this
63
+ // pathname change is our own settle, already reflected in activeIndex"
64
+ // apart from a genuinely external navigation.
65
+ const isInternalNavRef = useRef(false);
66
+ const didMountRef = useRef(false);
67
+
68
+ useEffect(() => {
69
+ if (!didMountRef.current) {
70
+ // The very first render's activeIndex already reflects pathname via
71
+ // the lazy useState initializer above — nothing to resync on mount.
72
+ didMountRef.current = true;
73
+ return;
74
+ }
75
+ if (isInternalNavRef.current) {
76
+ isInternalNavRef.current = false;
77
+ return;
78
+ }
79
+ const newIndex = indexForPathnameRef.current(pathname);
80
+ setActiveIndex((current) => (newIndex === current ? current : newIndex));
81
+ // Only re-run when pathname itself changes — indexForPathname is read
82
+ // through a ref precisely so its identity doesn't matter here.
83
+ }, [pathname]);
84
+
85
+ function onSettle(index: number) {
86
+ isInternalNavRef.current = true;
87
+ setActiveIndex(index);
88
+ onNavigateRef.current(pathForIndexRef.current(index));
89
+ }
90
+
91
+ return { activeIndex, onSettle };
92
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ /**
6
+ * useIsOffline — whether the browser currently has no network connection.
7
+ *
8
+ * SSR-safe: always initialises to `false` (online) so the server-rendered HTML
9
+ * matches the first client render, then reads the real `navigator.onLine`
10
+ * value in `useEffect`. This picks up the case where the page loaded already
11
+ * offline (e.g. served from the service-worker cache) at the cost of an
12
+ * imperceptible one-frame delay before the offline state appears — the same
13
+ * trade-off `OfflineBanner` makes, which this hook was extracted from so every
14
+ * consumer shares one connectivity source instead of re-deriving it slightly
15
+ * differently (research 0012, epic task 2.32).
16
+ *
17
+ * Reflects only `navigator.onLine` — true whenever the OS reports a network
18
+ * interface is up, even without real upstream connectivity (e.g. Wi-Fi with no
19
+ * internet). That is a known limitation of the browser API itself, not of this
20
+ * hook; there is currently no better cross-browser signal available.
21
+ */
22
+ export function useIsOffline(): boolean {
23
+ const [isOffline, setIsOffline] = useState(false);
24
+
25
+ useEffect(() => {
26
+ setIsOffline(!navigator.onLine);
27
+
28
+ const handleOffline = () => setIsOffline(true);
29
+ const handleOnline = () => setIsOffline(false);
30
+
31
+ window.addEventListener('offline', handleOffline);
32
+ window.addEventListener('online', handleOnline);
33
+
34
+ return () => {
35
+ window.removeEventListener('offline', handleOffline);
36
+ window.removeEventListener('online', handleOnline);
37
+ };
38
+ }, []);
39
+
40
+ return isOffline;
41
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { useIsOffline } from './useIsOffline';
4
+
5
+ /**
6
+ * The two reasons an app tile can be non-interactive, kept structurally
7
+ * distinct because they have different causes and must never read as the same
8
+ * thing to a user (research 0012, epic task 2.33):
9
+ *
10
+ * - `'connectivity-dimmed'` — a no-offline-tier app, dimmed only while the
11
+ * device is **actually offline right now**. Reactive and temporary; clears
12
+ * the instant connectivity returns.
13
+ * - `'capability-restricted'` — a `device-only` app on a surface without the
14
+ * durable, encrypted, device-auth-gated store it needs. Static and
15
+ * unrelated to connectivity — it stays restricted even when fully online,
16
+ * and must never say "offline" to a user who plainly isn't.
17
+ */
18
+ export type OfflineTileState = 'connectivity-dimmed' | 'capability-restricted' | null;
19
+
20
+ /**
21
+ * Which of the two states, if any, applies to an app tile given its declared
22
+ * offline tier.
23
+ *
24
+ * `deviceOnlyAvailable` is the caller's own
25
+ * `isDeviceOnlyTierAvailable()` result (from
26
+ * `@sovereignfs/sdk/device-client`) — deliberately taken as a plain boolean
27
+ * parameter rather than computed inside this hook, so `@sovereignfs/ui` never
28
+ * depends on `@sovereignfs/sdk` (this package must stay usable standalone,
29
+ * outside the plugin runtime — RFC 0073).
30
+ *
31
+ * An `offline-first` app is never restricted by either state: that tier's
32
+ * whole point is working with no network, so it stays fully interactive both
33
+ * online and offline. A `device-only` app that *is* available behaves the
34
+ * same way — the restriction is about availability, not connectivity.
35
+ */
36
+ export function useOfflineTileState(
37
+ offline: 'offline-first' | 'device-only' | undefined,
38
+ deviceOnlyAvailable: boolean,
39
+ ): OfflineTileState {
40
+ const isOffline = useIsOffline();
41
+
42
+ if (offline === 'device-only' && !deviceOnlyAvailable) return 'capability-restricted';
43
+ if (offline === undefined && isOffline) return 'connectivity-dimmed';
44
+ return null;
45
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import { useLayoutEffect } from 'react';
4
+ import type { RefObject } from 'react';
5
+
6
+ /**
7
+ * Internal implementation detail of `MobileHeader`/`MobileFooter` — not part
8
+ * of the package's public surface (not re-exported from `hooks/index.ts`).
9
+ *
10
+ * Measures `ref`'s own rendered height and publishes it as an inline
11
+ * `var(--sv-shell-header-height|--sv-shell-footer-height)` override on
12
+ * `#sv-app-shell` (the runtime shell's stable root id, `runtime/app/
13
+ * (platform)/layout.tsx`), removing it on unmount. This is what makes a
14
+ * *self-rendered* `MobileHeader`/`MobileFooter` — the sanctioned pattern for
15
+ * a plugin that opts out of the platform's own chrome via
16
+ * `shellConfig.mobileHeader`/`mobileFooter: false` (RFC 0075/0088) — work
17
+ * correctly with `Sheet`/`Drawer`/`Dialog`, which all size themselves against
18
+ * these same two variables so they stop above/below the chrome instead of
19
+ * sliding underneath it.
20
+ *
21
+ * Before this existed, only the *platform's own* header/footer had its
22
+ * height accounted for (a fixed CSS `calc()` in `shell.module.css`, driven by
23
+ * `data-mobile-header-hidden`/`data-mobile-footer-hidden`, which collapses
24
+ * the variable to `0px` whenever a plugin hides that chrome) — a plugin
25
+ * self-rendering its own via this same component instead had no path to tell
26
+ * the shell what its real height was, and silently inherited that `0px`.
27
+ * `Sheet`/`Drawer` then extended full-height, and since the header/footer's
28
+ * own `z-index` (101) beats an overlay's (100), the chrome visibly covered
29
+ * the overlay's last ~60px. First found and fixed ad hoc in `sovereign-tasks`
30
+ * (measuring in the plugin itself, `getBoundingClientRect()` in a
31
+ * `useLayoutEffect`); moved here so every consumer of these two components —
32
+ * present and future, in this repo or externally maintained — gets it for
33
+ * free, without having to know the mechanism exists. See
34
+ * `docs/architecture-rules.md` for the full rule.
35
+ *
36
+ * Deliberately **not** `ResizeObserver`: in live testing (both this repo's
37
+ * own Chromium-based browser-preview tooling and a real WebKit iOS Simulator
38
+ * session) a freshly created `ResizeObserver` never fired its callback even
39
+ * once for an already-rendered, stably-sized, non-zero element — cause not
40
+ * fully root-caused. A synchronous `getBoundingClientRect()` read in
41
+ * `useLayoutEffect`, re-run on `window`'s `resize` event, worked immediately
42
+ * and reliably in both environments instead.
43
+ *
44
+ * No-ops safely wherever `#sv-app-shell` doesn't exist (Storybook, a unit
45
+ * test, or any host other than the platform's own runtime shell) — every CSS
46
+ * consumer reads these variables with a `60px` fallback, so an absent
47
+ * override is harmless.
48
+ *
49
+ * For `--sv-shell-header-height` specifically, this also mirrors the same
50
+ * measured height onto `--sv-dialog-inset-top` on `:root` (`document.
51
+ * documentElement`, not `#sv-app-shell`) — `Dialog`'s mobile `.scrim` is
52
+ * `position: fixed` and reads that variable for its own `top` offset, and
53
+ * per `globals.css`'s own comment, iOS Safari has a documented quirk where a
54
+ * `position: fixed` element inherits custom properties from `:root` rather
55
+ * than from its DOM ancestors, so a value set only on `#sv-app-shell` (an
56
+ * ancestor, not `:root`) silently never reaches it there. The platform's own
57
+ * header already keeps `--sv-dialog-inset-top` correct this way, via
58
+ * `ClientShell.tsx`'s own `syncViewport()` — but that function is part of
59
+ * `(platform)/layout.tsx` and never runs for a `shell: minimal` plugin, so a
60
+ * self-rendered `MobileHeader` (this hook's entire reason to exist) had no
61
+ * equivalent, and `--sv-dialog-inset-top` silently kept `globals.css`'s
62
+ * hardcoded platform-header-height fallback (60px) regardless of this
63
+ * header's real height. Found live in `sovereign-plugin-kanban`: its
64
+ * self-rendered header measures 69px, so its card-detail `Dialog` opened
65
+ * with a real, reproducible ~9px misalignment against that header's own
66
+ * bottom edge — confirmed via `getBoundingClientRect()` on both elements,
67
+ * not just visually. */
68
+ export function usePublishShellChromeHeight(
69
+ ref: RefObject<HTMLElement | null>,
70
+ cssVariable: '--sv-shell-header-height' | '--sv-shell-footer-height',
71
+ ) {
72
+ useLayoutEffect(() => {
73
+ const el = ref.current;
74
+ if (!el) return;
75
+
76
+ function publish() {
77
+ const shell = document.getElementById('sv-app-shell');
78
+ if (!shell || !el) return;
79
+ const height = `${el.getBoundingClientRect().height}px`;
80
+ shell.style.setProperty(cssVariable, height);
81
+ if (cssVariable === '--sv-shell-header-height') {
82
+ document.documentElement.style.setProperty('--sv-dialog-inset-top', height);
83
+ }
84
+ }
85
+
86
+ publish();
87
+ window.addEventListener('resize', publish);
88
+ return () => {
89
+ window.removeEventListener('resize', publish);
90
+ document.getElementById('sv-app-shell')?.style.removeProperty(cssVariable);
91
+ if (cssVariable === '--sv-shell-header-height') {
92
+ document.documentElement.style.removeProperty('--sv-dialog-inset-top');
93
+ }
94
+ };
95
+ }, [ref, cssVariable]);
96
+ }
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import {
4
+ KeyboardSensor,
5
+ MouseSensor as LibMouseSensor,
6
+ TouchSensor as LibTouchSensor,
7
+ useSensor,
8
+ useSensors,
9
+ } from '@dnd-kit/core';
10
+ import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
11
+ import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
12
+
13
+ /** Plain pointer drag (desktop, handle-initiated). */
14
+ const DEFAULT_MOUSE_ACTIVATION_DISTANCE_PX = 8;
15
+
16
+ /** Long-press-to-lift on touch. `delay` is how long a still hold takes before
17
+ * the row lifts; `tolerance` is how far the finger may drift during that
18
+ * hold before it's treated as a scroll instead — a finger that moves further
19
+ * than this within the delay window cancels activation and the native
20
+ * vertical scroll wins. */
21
+ const DEFAULT_TOUCH_ACTIVATION_DELAY_MS = 300;
22
+ const DEFAULT_TOUCH_ACTIVATION_TOLERANCE_PX = 8;
23
+
24
+ /**
25
+ * True when a drag should be allowed to start from `target`. Refused when
26
+ * `target` sits inside an element marked `data-no-dnd` — a nested control
27
+ * (e.g. a visibility toggle) opts out so tapping it fires its own handler
28
+ * instead of lifting the row. Exported standalone so it's unit-testable
29
+ * without spinning up dnd-kit.
30
+ */
31
+ export function shouldHandleDndEvent(target: EventTarget | null): boolean {
32
+ if (!(target instanceof Element)) return true;
33
+ return target.closest('[data-no-dnd]') === null;
34
+ }
35
+
36
+ export interface UseReorderSensorsOptions {
37
+ mouseActivationDistancePx?: number;
38
+ touchActivationDelayMs?: number;
39
+ touchActivationTolerancePx?: number;
40
+ }
41
+
42
+ // dnd-kit's documented pattern for scoping sensor activation to specific
43
+ // elements: subclass the built-in sensor and replace its static `activators`
44
+ // with a handler that inspects the originating event's target before
45
+ // deferring to the base activation logic (distance/delay/tolerance), which
46
+ // dnd-kit re-checks internally regardless of what this returns.
47
+ class MouseSensor extends LibMouseSensor {
48
+ static override activators = [
49
+ {
50
+ eventName: 'onMouseDown' as const,
51
+ handler: ({ nativeEvent: event }: ReactMouseEvent) => shouldHandleDndEvent(event.target),
52
+ },
53
+ ];
54
+ }
55
+
56
+ class TouchSensor extends LibTouchSensor {
57
+ static override activators = [
58
+ {
59
+ eventName: 'onTouchStart' as const,
60
+ handler: ({ nativeEvent: event }: ReactTouchEvent) => shouldHandleDndEvent(event.target),
61
+ },
62
+ ];
63
+ }
64
+
65
+ /**
66
+ * Sensor set for a `data-no-dnd`-aware drag-reorder list — MouseSensor for
67
+ * desktop's handle-initiated drag, TouchSensor for mobile's long-press lift
68
+ * (fixes reordering being unusable on iOS PWA/Safari, which never implements
69
+ * the native HTML5 Drag-and-Drop API for touch input), KeyboardSensor
70
+ * unchanged. A bare call keeps the historical 8px/300ms/8px defaults.
71
+ */
72
+ export function useReorderSensors(options: UseReorderSensorsOptions = {}) {
73
+ const {
74
+ mouseActivationDistancePx = DEFAULT_MOUSE_ACTIVATION_DISTANCE_PX,
75
+ touchActivationDelayMs = DEFAULT_TOUCH_ACTIVATION_DELAY_MS,
76
+ touchActivationTolerancePx = DEFAULT_TOUCH_ACTIVATION_TOLERANCE_PX,
77
+ } = options;
78
+ return useSensors(
79
+ useSensor(MouseSensor, { activationConstraint: { distance: mouseActivationDistancePx } }),
80
+ useSensor(TouchSensor, {
81
+ activationConstraint: {
82
+ delay: touchActivationDelayMs,
83
+ tolerance: touchActivationTolerancePx,
84
+ },
85
+ }),
86
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
87
+ );
88
+ }
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { useIsMobile } from './useIsMobile';
4
+
5
+ export interface UseResponsiveLayoutOptions<TWeb, TMobile> {
6
+ web: TWeb;
7
+ mobile: TMobile;
8
+ breakpointPx?: number;
9
+ }
10
+
11
+ export interface UseResponsiveLayoutResult<TWeb, TMobile> {
12
+ isMobile: boolean;
13
+ value: TWeb | TMobile;
14
+ }
15
+
16
+ /**
17
+ * useResponsiveLayout — picks between a `web` and `mobile` value for the
18
+ * current viewport, built on `useIsMobile` (same breakpoint/SSR-safe-default
19
+ * behavior, not reimplemented here). Generic over the value type so it works
20
+ * for JSX (the common case — forking an entire component tree, not just a
21
+ * class name) or any other per-breakpoint value a plugin wants to pick
22
+ * without repeating the isMobile-ternary boilerplate every call site.
23
+ *
24
+ * This formalizes the `if (isMobile) return <mobile/>; return <web/>;` fork
25
+ * plugins have hand-rolled per-plugin (e.g. a `MobileAwareShell` component
26
+ * that swaps an entire routed page tree for a client-only mobile one) —
27
+ * see `ResponsiveSurface` for the equivalent as a plain JSX component.
28
+ */
29
+ export function useResponsiveLayout<TWeb, TMobile>({
30
+ web,
31
+ mobile,
32
+ breakpointPx,
33
+ }: UseResponsiveLayoutOptions<TWeb, TMobile>): UseResponsiveLayoutResult<TWeb, TMobile> {
34
+ const isMobile = useIsMobile(breakpointPx);
35
+ return { isMobile, value: isMobile ? mobile : web };
36
+ }