@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,13 @@
1
+ 'use client';
2
+
3
+ import { createContext } from 'react';
4
+
5
+ export interface CarouselSlideMountContextValue {
6
+ /** Whether this slide is within the carousel's mount window (the active
7
+ * slide ± its configured prefetchDistance). Kept out of the main .tsx
8
+ * file for React Fast Refresh hygiene (a file exporting only components
9
+ * refreshes more reliably than one mixing components and plain values). */
10
+ isMounted: boolean;
11
+ }
12
+
13
+ export const CarouselSlideMountContext = createContext<CarouselSlideMountContextValue | null>(null);
@@ -0,0 +1,64 @@
1
+ .dots {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: var(--sv-space-2);
6
+ }
7
+
8
+ /* Opt-in — see the `density` prop doc comment. Shrinks both the gap and each
9
+ dot's own hit target (20px -> 16px) — for a carousel with enough slides
10
+ that even the space-1 gap alone still read as too wide. The visible 6px
11
+ dot is unchanged; only the surrounding tap-target box shrinks, via the
12
+ descendant override below rather than a second dot variant, so this stays
13
+ a pure CSS opt-in with no JS/prop plumbing beyond `density` itself. */
14
+ .dotsCompact {
15
+ gap: var(--sv-space-1);
16
+ }
17
+
18
+ .dotsCompact .dot {
19
+ width: 16px;
20
+ height: 16px;
21
+ }
22
+
23
+ .dot {
24
+ /* A 6px visible dot is too small a hit target on its own — the button's
25
+ real box is larger (20px) with the dot itself painted by ::before and
26
+ centered inside, so tap area grows without changing the visible dot
27
+ size or the row's gap-based spacing math. */
28
+ position: relative;
29
+ width: 20px;
30
+ height: 20px;
31
+ padding: 0;
32
+ border: none;
33
+ background: transparent;
34
+ cursor: pointer;
35
+ }
36
+
37
+ .dot::before {
38
+ content: '';
39
+ position: absolute;
40
+ top: 50%;
41
+ left: 50%;
42
+ width: 6px;
43
+ height: 6px;
44
+ border-radius: 50%;
45
+ background: var(--sv-color-border-strong);
46
+ transform: translate(-50%, -50%);
47
+ transition: background 0.15s ease;
48
+ }
49
+
50
+ .dot:focus-visible {
51
+ outline: 2px solid var(--sv-color-focus-ring);
52
+ outline-offset: 2px;
53
+ border-radius: 50%;
54
+ }
55
+
56
+ .dotActive::before {
57
+ background: var(--sv-color-text-primary);
58
+ }
59
+
60
+ @media (hover: hover) {
61
+ .dot:not(.dotActive):hover::before {
62
+ background: var(--sv-color-text-muted);
63
+ }
64
+ }
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import styles from './SwipableMobileCarouselDots.module.css';
4
+
5
+ export interface SwipableMobileCarouselDotsProps {
6
+ count: number;
7
+ activeIndex: number;
8
+ onJump: (index: number) => void;
9
+ /** Per-dot accessible names, e.g. ["Lists", "Starred", "Groceries"]. Falls
10
+ * back to "Slide N of count" for any index without one. */
11
+ labels?: (string | undefined)[];
12
+ /** Labels the whole group — required, no generic fallback (mirrors
13
+ * SwipableMobileCarousel's own required aria-label). */
14
+ 'aria-label': string;
15
+ /** `'compact'` halves the gap between dots (`--sv-space-2` → `--sv-space-1`)
16
+ * and shrinks each dot's own hit target (20px → 16px) — for a carousel
17
+ * with enough slides that even the reduced gap alone still reads as too
18
+ * wide on a narrow viewport (e.g. a many-list mobile app). The visible
19
+ * 6px dot itself is unchanged; only its surrounding tap-target box
20
+ * shrinks. Defaults to `'default'` — every existing consumer's spacing is
21
+ * unaffected unless it opts in. */
22
+ density?: 'default' | 'compact';
23
+ className?: string;
24
+ }
25
+
26
+ /**
27
+ * SwipableMobileCarouselDots — a real, tappable, labeled slide indicator.
28
+ * Standalone (not carousel-only): reusable anywhere a set of positions needs
29
+ * a dot indicator (e.g. an image gallery, an onboarding stepper), and is also
30
+ * SwipableMobileCarousel's default `renderIndicator`.
31
+ *
32
+ * Replaces the `aria-hidden`, non-interactive dots pattern both
33
+ * sovereign-tasks and sovereign-shopper currently hand-roll — every dot here
34
+ * is a real `role="tab"` button with its own accessible name, reachable by
35
+ * keyboard. No roving tabIndex/arrow-key handling: `Tabs` (this library's
36
+ * other `role="tablist"` component) has none either, so this stays
37
+ * consistent with it rather than gold-plating past its sibling.
38
+ */
39
+ export function SwipableMobileCarouselDots({
40
+ count,
41
+ activeIndex,
42
+ onJump,
43
+ labels,
44
+ 'aria-label': ariaLabel,
45
+ density = 'default',
46
+ className,
47
+ }: SwipableMobileCarouselDotsProps) {
48
+ return (
49
+ <div
50
+ role="tablist"
51
+ aria-label={ariaLabel}
52
+ className={[styles.dots, density === 'compact' ? styles.dotsCompact : '', className]
53
+ .filter(Boolean)
54
+ .join(' ')}
55
+ >
56
+ {Array.from({ length: count }, (_, i) => {
57
+ const active = i === activeIndex;
58
+ const label = labels?.[i] ?? `Slide ${i + 1} of ${count}`;
59
+ return (
60
+ <button
61
+ key={i}
62
+ type="button"
63
+ role="tab"
64
+ aria-selected={active}
65
+ aria-label={label}
66
+ className={[styles.dot, active ? styles.dotActive : ''].filter(Boolean).join(' ')}
67
+ onClick={() => onJump(i)}
68
+ />
69
+ );
70
+ })}
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,63 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
4
+ import { SwipableMobileCarouselDots } from '../SwipableMobileCarouselDots';
5
+
6
+ afterEach(cleanup);
7
+
8
+ describe('SwipableMobileCarouselDots', () => {
9
+ it('renders a tablist with one tab per count, none aria-hidden', () => {
10
+ render(
11
+ <SwipableMobileCarouselDots count={3} activeIndex={0} onJump={() => {}} aria-label="Lists" />,
12
+ );
13
+ const list = screen.getByRole('tablist', { name: 'Lists' });
14
+ expect(list.getAttribute('aria-hidden')).toBeNull();
15
+ const tabs = screen.getAllByRole('tab');
16
+ expect(tabs).toHaveLength(3);
17
+ tabs.forEach((tab) => expect(tab.getAttribute('aria-hidden')).toBeNull());
18
+ });
19
+
20
+ it('marks only the active dot as aria-selected', () => {
21
+ render(
22
+ <SwipableMobileCarouselDots count={3} activeIndex={1} onJump={() => {}} aria-label="Lists" />,
23
+ );
24
+ const tabs = screen.getAllByRole('tab');
25
+ expect(tabs[0]?.getAttribute('aria-selected')).toBe('false');
26
+ expect(tabs[1]?.getAttribute('aria-selected')).toBe('true');
27
+ expect(tabs[2]?.getAttribute('aria-selected')).toBe('false');
28
+ });
29
+
30
+ it('uses the provided per-dot labels, falling back to "Slide N of count"', () => {
31
+ render(
32
+ <SwipableMobileCarouselDots
33
+ count={3}
34
+ activeIndex={0}
35
+ onJump={() => {}}
36
+ labels={['Lists', undefined, 'Groceries']}
37
+ aria-label="Lists"
38
+ />,
39
+ );
40
+ expect(screen.getByRole('tab', { name: 'Lists' })).toBeDefined();
41
+ expect(screen.getByRole('tab', { name: 'Slide 2 of 3' })).toBeDefined();
42
+ expect(screen.getByRole('tab', { name: 'Groceries' })).toBeDefined();
43
+ });
44
+
45
+ it('clicking a dot calls onJump with its index', () => {
46
+ const onJump = vi.fn();
47
+ render(
48
+ <SwipableMobileCarouselDots count={3} activeIndex={0} onJump={onJump} aria-label="Lists" />,
49
+ );
50
+ const thirdTab = screen.getAllByRole('tab')[2];
51
+ if (!thirdTab) throw new Error('expected a 3rd tab');
52
+ fireEvent.click(thirdTab);
53
+ expect(onJump).toHaveBeenCalledWith(2);
54
+ });
55
+
56
+ it('every dot is a real, focusable button (not a decorative element)', () => {
57
+ render(
58
+ <SwipableMobileCarouselDots count={2} activeIndex={0} onJump={() => {}} aria-label="Lists" />,
59
+ );
60
+ const tabs = screen.getAllByRole('tab');
61
+ tabs.forEach((tab) => expect(tab.tagName).toBe('BUTTON'));
62
+ });
63
+ });
@@ -0,0 +1,101 @@
1
+ .wrap {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--sv-space-3);
5
+ height: 100%;
6
+ width: 100%;
7
+ }
8
+
9
+ .stack {
10
+ position: relative;
11
+ flex: 1 1 auto;
12
+ min-height: 0;
13
+ }
14
+
15
+ .peek {
16
+ position: absolute;
17
+ inset: 0;
18
+ transform: scale(0.94) translateY(var(--sv-space-3));
19
+ opacity: 0.6;
20
+ pointer-events: none;
21
+ }
22
+
23
+ .cardSlot {
24
+ position: absolute;
25
+ inset: 0;
26
+ }
27
+
28
+ /* Styled directly rather than wrapping the Card component: Card doesn't
29
+ forward a ref, and this element must be the exact node useSwipeStack
30
+ translates/rotates as one unit — a Card wrapping an inner dragged div
31
+ would leave its own border/shadow visually static while only the inner
32
+ div moved. */
33
+ .card {
34
+ position: relative;
35
+ height: 100%;
36
+ width: 100%;
37
+ overflow: hidden;
38
+ background: var(--sv-color-surface-raised);
39
+ border: var(--sv-border-width-hairline) solid var(--sv-color-border);
40
+ border-radius: var(--sv-radius-lg);
41
+ box-shadow: var(--sv-shadow-card);
42
+ touch-action: none; /* always overridden inline with useSwipeStack's computed value */
43
+ user-select: none;
44
+ cursor: grab;
45
+ }
46
+
47
+ .card:active {
48
+ cursor: grabbing;
49
+ }
50
+
51
+ .stamp {
52
+ position: absolute;
53
+ z-index: 1;
54
+ display: flex;
55
+ align-items: center;
56
+ gap: var(--sv-space-1);
57
+ padding: var(--sv-space-1) var(--sv-space-3);
58
+ border: 2px solid var(--sv-color-text-primary);
59
+ border-radius: var(--sv-radius-md);
60
+ background: var(--sv-color-surface-raised);
61
+ color: var(--sv-color-text-primary);
62
+ font-family: var(--sv-font-family);
63
+ font-size: var(--sv-font-size-sm);
64
+ font-weight: var(--sv-font-weight-semibold);
65
+ pointer-events: none;
66
+ }
67
+
68
+ .stampUp {
69
+ top: var(--sv-space-4);
70
+ left: 50%;
71
+ transform: translateX(-50%);
72
+ opacity: var(--sv-stack-progress-up, 0);
73
+ }
74
+
75
+ .stampDown {
76
+ bottom: var(--sv-space-4);
77
+ left: 50%;
78
+ transform: translateX(-50%);
79
+ opacity: var(--sv-stack-progress-down, 0);
80
+ }
81
+
82
+ .stampLeft {
83
+ top: var(--sv-space-4);
84
+ left: var(--sv-space-4);
85
+ transform: rotate(-8deg);
86
+ opacity: var(--sv-stack-progress-left, 0);
87
+ }
88
+
89
+ .stampRight {
90
+ top: var(--sv-space-4);
91
+ right: var(--sv-space-4);
92
+ transform: rotate(8deg);
93
+ opacity: var(--sv-stack-progress-right, 0);
94
+ }
95
+
96
+ .actions {
97
+ display: flex;
98
+ justify-content: center;
99
+ gap: var(--sv-space-2);
100
+ flex: 0 0 auto;
101
+ }
@@ -0,0 +1,199 @@
1
+ 'use client';
2
+
3
+ import { Children, isValidElement, useEffect, useRef, useState } from 'react';
4
+ import type { ReactElement, ReactNode } from 'react';
5
+ import { Button } from '../Button/Button';
6
+ import { Icon, type IconName } from '../Icon/Icon';
7
+ import { useSwipeStack, SWIPE_STACK_TRANSITION_MS } from '../../hooks/useSwipeStack';
8
+ import type { SwipeDirection } from '../../hooks/useSwipeStack';
9
+ import { SwipeStackCard } from './SwipeStackCard';
10
+ import styles from './SwipeStack.module.css';
11
+
12
+ export interface SwipeDirectionMeta {
13
+ /** Visible text on the fallback button and the in-drag stamp. */
14
+ label: string;
15
+ icon?: IconName;
16
+ }
17
+
18
+ export interface SwipeStackProps {
19
+ /** Required — labels the swipeable region for assistive tech. No generic
20
+ * fallback, since one would be wrong for every actual consumer. */
21
+ 'aria-label': string;
22
+ /** Which of the 4 directions are live, and what each is called. A
23
+ * direction absent here cannot be dragged toward at all (useSwipeStack
24
+ * treats it as a wall) and gets no fallback button or stamp. SwipeStack
25
+ * itself has no opinion on what a direction *means* — that vocabulary
26
+ * belongs entirely to the caller. */
27
+ directions: Partial<Record<SwipeDirection, SwipeDirectionMeta>>;
28
+ /** Fires once a drag (or a fallback button) commits to a direction. Every
29
+ * configured direction behaves the same way: the card leaves for good —
30
+ * there is no reversible "go back" state. SwipeStack has no opinion on
31
+ * where card data lives; the caller is responsible for eventually no
32
+ * longer including this cardId in children (e.g. once a mutation lands). */
33
+ onSwipe: (direction: SwipeDirection, cardId: string) => void;
34
+ /** Effective drag distance (px) required to commit. Passed straight
35
+ * through to useSwipeStack. */
36
+ threshold?: number;
37
+ className?: string;
38
+ /** Must be SwipeStackCard elements (nullish/boolean children are safely
39
+ * skipped for conditional cards). A dev-mode warning is logged for any
40
+ * other child type — see resolveCards below. */
41
+ children: ReactNode;
42
+ }
43
+
44
+ interface ResolvedCard {
45
+ key: string;
46
+ element: ReactElement;
47
+ }
48
+
49
+ const DIRECTION_KEYS: readonly SwipeDirection[] = ['up', 'down', 'left', 'right'];
50
+
51
+ const STAMP_CLASS: Record<SwipeDirection, string> = {
52
+ up: styles.stampUp as string,
53
+ down: styles.stampDown as string,
54
+ left: styles.stampLeft as string,
55
+ right: styles.stampRight as string,
56
+ };
57
+
58
+ function describeChild(child: unknown): string {
59
+ if (isValidElement(child)) {
60
+ const type = child.type;
61
+ if (typeof type === 'string') return `<${type}>`;
62
+ if (typeof type === 'function') {
63
+ const named = type as { displayName?: string; name?: string };
64
+ return `<${named.displayName ?? named.name ?? 'Component'}>`;
65
+ }
66
+ return '<unknown>';
67
+ }
68
+ if (typeof child === 'string') return `string "${child}"`;
69
+ return String(child);
70
+ }
71
+
72
+ function resolveCards(children: ReactNode): ResolvedCard[] {
73
+ const cards: ResolvedCard[] = [];
74
+ Children.forEach(children, (child, index) => {
75
+ if (child === null || child === undefined || child === false || child === true) return;
76
+ if (!isValidElement(child) || child.type !== SwipeStackCard) {
77
+ if (process.env.NODE_ENV !== 'production') {
78
+ console.error(
79
+ '[SwipeStack] Every child must be a <SwipeStackCard>. ' +
80
+ `Found ${describeChild(child)} at position ${index}.`,
81
+ );
82
+ }
83
+ return;
84
+ }
85
+ const cardProps = child.props as { cardId: string };
86
+ cards.push({ key: cardProps.cardId, element: child });
87
+ });
88
+ return cards;
89
+ }
90
+
91
+ /**
92
+ * SwipeStack — a compound component for triaging one card at a time by
93
+ * dragging it left/right/up/down (wrapping useSwipeStack), plus an
94
+ * always-visible non-gesture fallback: a button per configured direction,
95
+ * wired to the identical commit path and exit animation as a real drag.
96
+ *
97
+ * Every direction is independently optional and behaves the same way once
98
+ * triggered — the card leaves for good, there is no reversible "go back".
99
+ * SwipeStack owns rendering, drag physics, and the stamp/fallback UI only;
100
+ * it has no opinion on where card data lives or what a direction means —
101
+ * see SwipeStackProps.onSwipe.
102
+ */
103
+ export function SwipeStack({
104
+ 'aria-label': ariaLabel,
105
+ directions,
106
+ onSwipe,
107
+ threshold,
108
+ className,
109
+ children,
110
+ }: SwipeStackProps) {
111
+ const cards = resolveCards(children);
112
+ const [dismissed, setDismissed] = useState<ReadonlySet<string>>(() => new Set());
113
+ const [exiting, setExiting] = useState<ResolvedCard | null>(null);
114
+ const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
115
+
116
+ useEffect(
117
+ () => () => {
118
+ if (exitTimerRef.current) clearTimeout(exitTimerRef.current);
119
+ },
120
+ [],
121
+ );
122
+
123
+ const available = cards.filter((c) => !dismissed.has(c.key));
124
+ const nextUp = available[0];
125
+ const current = exiting ?? nextUp ?? null;
126
+ const peek = exiting ? nextUp : available[1];
127
+
128
+ const liveDirections = DIRECTION_KEYS.filter((d) => directions[d]);
129
+
130
+ const { cardRef, wrapRef, handlers, touchAction, triggerCommit } = useSwipeStack({
131
+ directions: liveDirections,
132
+ threshold,
133
+ disabled: !!exiting || !nextUp,
134
+ onCommit(direction) {
135
+ const committed = current;
136
+ if (!committed) return;
137
+ setDismissed((prev) => new Set(prev).add(committed.key));
138
+ setExiting(committed);
139
+ if (exitTimerRef.current) clearTimeout(exitTimerRef.current);
140
+ exitTimerRef.current = setTimeout(() => {
141
+ setExiting((prev) => (prev?.key === committed.key ? null : prev));
142
+ }, SWIPE_STACK_TRANSITION_MS);
143
+ onSwipe(direction, committed.key);
144
+ },
145
+ });
146
+
147
+ return (
148
+ <div className={[styles.wrap, className].filter(Boolean).join(' ')}>
149
+ <div className={styles.stack} role="region" aria-label={ariaLabel}>
150
+ {peek && (
151
+ <div className={styles.peek} key={peek.key} aria-hidden="true">
152
+ {peek.element}
153
+ </div>
154
+ )}
155
+ {current && (
156
+ <div className={styles.cardSlot} key={current.key} ref={wrapRef}>
157
+ {liveDirections.map((d) => {
158
+ const meta = directions[d];
159
+ if (!meta) return null;
160
+ return (
161
+ <div
162
+ key={d}
163
+ className={[styles.stamp, STAMP_CLASS[d]].join(' ')}
164
+ aria-hidden="true"
165
+ >
166
+ {meta.icon && <Icon name={meta.icon} size="sm" aria-hidden />}
167
+ <span>{meta.label}</span>
168
+ </div>
169
+ );
170
+ })}
171
+ <div className={styles.card} ref={cardRef} style={{ touchAction }} {...handlers}>
172
+ {current.element}
173
+ </div>
174
+ </div>
175
+ )}
176
+ </div>
177
+ {liveDirections.length > 0 && (
178
+ <div className={styles.actions} role="group" aria-label={`${ariaLabel} actions`}>
179
+ {liveDirections.map((d) => {
180
+ const meta = directions[d];
181
+ if (!meta) return null;
182
+ return (
183
+ <Button
184
+ key={d}
185
+ variant="secondary"
186
+ size="sm"
187
+ disabled={!current}
188
+ onClick={() => triggerCommit(d)}
189
+ >
190
+ {meta.icon && <Icon name={meta.icon} size="sm" aria-hidden />}
191
+ {meta.label}
192
+ </Button>
193
+ );
194
+ })}
195
+ </div>
196
+ )}
197
+ </div>
198
+ );
199
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface SwipeStackCardProps {
4
+ /** Stable identity for this card, independent of its position among
5
+ * siblings — required. This is what SwipeStack reads (via child.props,
6
+ * not React's own `key`, which a component can't see on itself) to know
7
+ * which card is on top and what id to report through onSwipe. Reuse
8
+ * whatever id the caller already keys their `.map()` on. */
9
+ cardId: string;
10
+ children: ReactNode;
11
+ }
12
+
13
+ /**
14
+ * SwipeStackCard — a single card of a SwipeStack. Unlike
15
+ * SwipableMobileCarouselSlide, this has no mount-window logic of its own:
16
+ * SwipeStack only ever renders the top two cards' elements at all (there is
17
+ * no scroll-position reason to keep every sibling in the DOM the way the
18
+ * carousel's scroll-snap track does), so there is nothing here to gate.
19
+ */
20
+ export function SwipeStackCard({ children }: SwipeStackCardProps) {
21
+ return <>{children}</>;
22
+ }