@sovereignfs/ui 0.54.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 (277) 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/Markdown.module.css +24 -0
  26. package/dist/Menu.module.css +1 -0
  27. package/dist/Message.module.css +1 -1
  28. package/dist/MessageScroller.module.css +12 -1
  29. package/dist/MobileAppsDrawer.module.css +10 -9
  30. package/dist/MobileFooter.module.css +1 -1
  31. package/dist/MobileHeader.module.css +14 -1
  32. package/dist/NavList.module.css +130 -0
  33. package/dist/NavTabs.module.css +1 -1
  34. package/dist/NotificationsPanel.module.css +258 -0
  35. package/dist/OverlayHeader.module.css +1 -1
  36. package/dist/PageContainer.module.css +62 -0
  37. package/dist/PageHeader.module.css +54 -0
  38. package/dist/PageLayout.module.css +51 -0
  39. package/dist/Popover.module.css +1 -1
  40. package/dist/QuantityStepper.module.css +1 -1
  41. package/dist/Resizable.module.css +3 -3
  42. package/dist/RootLayout.module.css +12 -0
  43. package/dist/SegmentedControl.module.css +1 -1
  44. package/dist/Select.module.css +1 -1
  45. package/dist/Sheet.module.css +4 -1
  46. package/dist/SplitPane.module.css +4 -4
  47. package/dist/StatusBadge.module.css +1 -1
  48. package/dist/StepStrip.module.css +53 -0
  49. package/dist/SuggestionInput.module.css +2 -2
  50. package/dist/SwipableMobileCarousel.module.css +12 -3
  51. package/dist/SwipableMobileCarouselDots.module.css +15 -0
  52. package/dist/SwipeStack.module.css +101 -0
  53. package/dist/SystemBanner.module.css +10 -0
  54. package/dist/Table.module.css +2 -2
  55. package/dist/Tabs.module.css +1 -1
  56. package/dist/TagInput.module.css +2 -2
  57. package/dist/Textarea.module.css +9 -1
  58. package/dist/ThreeColumnLayout.module.css +35 -0
  59. package/dist/Toast.module.css +62 -3
  60. package/dist/Tooltip.module.css +1 -1
  61. package/dist/UserMenu.module.css +90 -0
  62. package/dist/index.d.ts +1047 -45
  63. package/dist/index.js +3908 -1456
  64. package/dist/tokens/primitives.css +11 -0
  65. package/dist/tokens/semantic.css +32 -0
  66. package/dist/tokens/theme-presets.css +33 -0
  67. package/package.json +25 -9
  68. package/src/__tests__/package-json.test.ts +8 -0
  69. package/src/components/Accordion/Accordion.module.css +1 -1
  70. package/src/components/Alert/Alert.module.css +1 -1
  71. package/src/components/AppsLauncher/AppsLauncher.module.css +96 -0
  72. package/src/components/AppsLauncher/AppsLauncher.stories.tsx +58 -0
  73. package/src/components/AppsLauncher/AppsLauncher.tsx +117 -0
  74. package/src/components/AppsLauncher/__tests__/AppsLauncher.test.tsx +52 -0
  75. package/src/components/Avatar/Avatar.module.css +1 -1
  76. package/src/components/Badge/Badge.module.css +30 -7
  77. package/src/components/Badge/Badge.stories.tsx +16 -4
  78. package/src/components/Badge/Badge.tsx +7 -4
  79. package/src/components/Badge/__tests__/Badge.test.tsx +4 -2
  80. package/src/components/BalanceChip/BalanceChip.module.css +1 -1
  81. package/src/components/Button/Button.module.css +18 -2
  82. package/src/components/Card/Card.module.css +1 -1
  83. package/src/components/CardTile/CardTile.module.css +181 -0
  84. package/src/components/CardTile/CardTile.stories.tsx +86 -0
  85. package/src/components/CardTile/CardTile.tsx +128 -0
  86. package/src/components/CardTile/__tests__/CardTile.test.tsx +56 -0
  87. package/src/components/CodeTextarea/CodeTextarea.module.css +1 -1
  88. package/src/components/Collapsible/Collapsible.module.css +1 -1
  89. package/src/components/ColorPicker/ColorPicker.module.css +132 -0
  90. package/src/components/ColorPicker/ColorPicker.stories.tsx +80 -0
  91. package/src/components/ColorPicker/ColorPicker.tsx +157 -0
  92. package/src/components/ColorPicker/__tests__/ColorPicker.test.tsx +117 -0
  93. package/src/components/Combobox/Combobox.module.css +2 -2
  94. package/src/components/Command/Command.module.css +1 -1
  95. package/src/components/ConfirmDialog/ConfirmDialog.module.css +2 -2
  96. package/src/components/ConfirmDialog/ConfirmDialog.tsx +28 -2
  97. package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +71 -3
  98. package/src/components/ConsentPrompt/ConsentPrompt.module.css +21 -0
  99. package/src/components/ConsentPrompt/ConsentPrompt.stories.tsx +31 -0
  100. package/src/components/ConsentPrompt/ConsentPrompt.tsx +66 -0
  101. package/src/components/ContextMenu/ContextMenu.tsx +1 -1
  102. package/src/components/DatePicker/DatePicker.module.css +1 -1
  103. package/src/components/Dialog/Dialog.module.css +130 -21
  104. package/src/components/Dialog/Dialog.stories.tsx +179 -2
  105. package/src/components/Dialog/Dialog.tsx +129 -35
  106. package/src/components/Dialog/__tests__/Dialog.test.tsx +190 -9
  107. package/src/components/DragHandleRow/DragHandleRow.tsx +2 -21
  108. package/src/components/Drawer/Drawer.module.css +36 -7
  109. package/src/components/Drawer/Drawer.tsx +9 -10
  110. package/src/components/Drawer/__tests__/Drawer.test.tsx +33 -6
  111. package/src/components/GripIcon/GripIcon.tsx +27 -0
  112. package/src/components/Header/Header.module.css +62 -0
  113. package/src/components/Header/Header.stories.tsx +452 -0
  114. package/src/components/Header/Header.tsx +102 -0
  115. package/src/components/Header/__tests__/Header.test.tsx +104 -0
  116. package/src/components/HeaderFooterLayout/HeaderFooterLayout.module.css +21 -0
  117. package/src/components/HeaderFooterLayout/HeaderFooterLayout.stories.tsx +111 -0
  118. package/src/components/HeaderFooterLayout/HeaderFooterLayout.tsx +54 -0
  119. package/src/components/HeaderFooterLayout/__tests__/HeaderFooterLayout.test.tsx +58 -0
  120. package/src/components/Icon/icons/arrow-left-right.tsx +23 -0
  121. package/src/components/Icon/icons/bold.tsx +20 -0
  122. package/src/components/Icon/icons/calendar.tsx +4 -4
  123. package/src/components/Icon/icons/carrot.tsx +3 -3
  124. package/src/components/Icon/icons/circle-chevron-left.tsx +21 -0
  125. package/src/components/Icon/icons/code.tsx +21 -0
  126. package/src/components/Icon/icons/copy.tsx +21 -0
  127. package/src/components/Icon/icons/credit-card.tsx +21 -0
  128. package/src/components/Icon/icons/database.tsx +22 -0
  129. package/src/components/Icon/icons/download.tsx +22 -0
  130. package/src/components/Icon/icons/file-text.tsx +24 -0
  131. package/src/components/Icon/icons/folder-closed.tsx +21 -0
  132. package/src/components/Icon/icons/folder-open.tsx +20 -0
  133. package/src/components/Icon/icons/folder.tsx +20 -0
  134. package/src/components/Icon/icons/folders.tsx +21 -0
  135. package/src/components/Icon/icons/hat-glasses.tsx +24 -0
  136. package/src/components/Icon/icons/history.tsx +22 -0
  137. package/src/components/Icon/icons/inbox.tsx +21 -0
  138. package/src/components/Icon/icons/index.ts +161 -1
  139. package/src/components/Icon/icons/italic.tsx +22 -0
  140. package/src/components/Icon/icons/layers.tsx +22 -0
  141. package/src/components/Icon/icons/layout-dashboard.tsx +23 -0
  142. package/src/components/Icon/icons/layout-grid.tsx +23 -0
  143. package/src/components/Icon/icons/link.tsx +21 -0
  144. package/src/components/Icon/icons/list-ordered.tsx +25 -0
  145. package/src/components/Icon/icons/list.tsx +25 -0
  146. package/src/components/Icon/icons/luggage.tsx +24 -0
  147. package/src/components/Icon/icons/map-pin.tsx +21 -0
  148. package/src/components/Icon/icons/minus.tsx +20 -0
  149. package/src/components/Icon/icons/paint-bucket.tsx +23 -0
  150. package/src/components/Icon/icons/panel-left.tsx +21 -0
  151. package/src/components/Icon/icons/pin.tsx +21 -0
  152. package/src/components/Icon/icons/rectangle-ellipsis.tsx +23 -0
  153. package/src/components/Icon/icons/refresh-cw.tsx +23 -0
  154. package/src/components/Icon/icons/rotate-cw.tsx +21 -0
  155. package/src/components/Icon/icons/route.tsx +22 -0
  156. package/src/components/Icon/icons/send.tsx +21 -0
  157. package/src/components/Icon/icons/share-2.tsx +24 -0
  158. package/src/components/Icon/icons/sheet.tsx +24 -0
  159. package/src/components/Icon/icons/square-kanban.tsx +23 -0
  160. package/src/components/Icon/icons/table.tsx +23 -0
  161. package/src/components/Icon/icons/user-round-plus.tsx +23 -0
  162. package/src/components/Icon/icons/users.tsx +23 -0
  163. package/src/components/IconPicker/IconPicker.module.css +1 -1
  164. package/src/components/Input/Input.module.css +1 -1
  165. package/src/components/Input/Input.tsx +8 -2
  166. package/src/components/Kbd/Kbd.module.css +1 -1
  167. package/src/components/Markdown/Markdown.module.css +24 -0
  168. package/src/components/Markdown/Markdown.stories.tsx +10 -0
  169. package/src/components/Markdown/Markdown.tsx +166 -29
  170. package/src/components/Markdown/__tests__/Markdown.test.tsx +144 -0
  171. package/src/components/Menu/Menu.module.css +1 -0
  172. package/src/components/Menu/Menu.tsx +72 -22
  173. package/src/components/Menu/__tests__/Menu.test.tsx +30 -0
  174. package/src/components/Menubar/Menubar.tsx +1 -1
  175. package/src/components/Message/Message.module.css +1 -1
  176. package/src/components/MessageScroller/MessageScroller.module.css +12 -1
  177. package/src/components/MessageScroller/MessageScroller.tsx +12 -0
  178. package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +10 -9
  179. package/src/components/MobileFooter/MobileFooter.module.css +1 -1
  180. package/src/components/MobileFooter/MobileFooter.tsx +15 -2
  181. package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +58 -0
  182. package/src/components/MobileHeader/MobileHeader.module.css +14 -1
  183. package/src/components/MobileHeader/MobileHeader.tsx +15 -2
  184. package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +56 -1
  185. package/src/components/NavList/NavList.module.css +130 -0
  186. package/src/components/NavList/NavList.tsx +136 -0
  187. package/src/components/NavList/__tests__/NavList.test.tsx +109 -0
  188. package/src/components/NavTabs/NavTabs.module.css +1 -1
  189. package/src/components/NotificationsPanel/NotificationsPanel.module.css +258 -0
  190. package/src/components/NotificationsPanel/NotificationsPanel.stories.tsx +61 -0
  191. package/src/components/NotificationsPanel/NotificationsPanel.tsx +187 -0
  192. package/src/components/NotificationsPanel/__tests__/NotificationsPanel.test.tsx +78 -0
  193. package/src/components/OverlayHeader/OverlayHeader.module.css +1 -1
  194. package/src/components/OverlayHeader/OverlayHeader.tsx +19 -4
  195. package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +18 -0
  196. package/src/components/PageContainer/PageContainer.module.css +62 -0
  197. package/src/components/PageContainer/PageContainer.tsx +37 -4
  198. package/src/components/PageContainer/__tests__/PageContainer.test.tsx +41 -8
  199. package/src/components/PageHeader/PageHeader.module.css +54 -0
  200. package/src/components/PageHeader/PageHeader.tsx +146 -5
  201. package/src/components/PageHeader/__tests__/PageHeader.test.tsx +103 -0
  202. package/src/components/PageLayout/PageLayout.module.css +51 -0
  203. package/src/components/PageLayout/PageLayout.stories.tsx +73 -0
  204. package/src/components/PageLayout/PageLayout.tsx +51 -0
  205. package/src/components/PageLayout/__tests__/PageLayout.test.tsx +34 -0
  206. package/src/components/Popover/Popover.module.css +1 -1
  207. package/src/components/Popover/Popover.tsx +86 -4
  208. package/src/components/Popover/__tests__/Popover.test.tsx +165 -0
  209. package/src/components/QuantityStepper/QuantityStepper.module.css +1 -1
  210. package/src/components/Resizable/Resizable.module.css +3 -3
  211. package/src/components/RootLayout/RootLayout.module.css +12 -0
  212. package/src/components/RootLayout/RootLayout.stories.tsx +125 -0
  213. package/src/components/RootLayout/RootLayout.tsx +185 -0
  214. package/src/components/RootLayout/__tests__/RootLayout.test.tsx +161 -0
  215. package/src/components/SegmentedControl/SegmentedControl.module.css +1 -1
  216. package/src/components/Select/Select.module.css +1 -1
  217. package/src/components/Sheet/Sheet.module.css +4 -1
  218. package/src/components/Sheet/Sheet.tsx +5 -5
  219. package/src/components/SplitPane/SplitPane.module.css +4 -4
  220. package/src/components/StatusBadge/StatusBadge.module.css +1 -1
  221. package/src/components/StepStrip/StepStrip.module.css +53 -0
  222. package/src/components/StepStrip/StepStrip.tsx +74 -0
  223. package/src/components/SuggestionInput/SuggestionInput.module.css +2 -2
  224. package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +12 -3
  225. package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +25 -9
  226. package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +81 -0
  227. package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +15 -0
  228. package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +12 -1
  229. package/src/components/SwipeStack/SwipeStack.module.css +101 -0
  230. package/src/components/SwipeStack/SwipeStack.tsx +199 -0
  231. package/src/components/SwipeStack/SwipeStackCard.tsx +22 -0
  232. package/src/components/SwipeStack/__tests__/SwipeStack.test.tsx +169 -0
  233. package/src/components/SystemBanner/SystemBanner.module.css +10 -0
  234. package/src/components/Table/Table.module.css +2 -2
  235. package/src/components/Tabs/Tabs.module.css +1 -1
  236. package/src/components/TagInput/TagInput.module.css +2 -2
  237. package/src/components/Textarea/Textarea.module.css +9 -1
  238. package/src/components/Textarea/Textarea.tsx +28 -5
  239. package/src/components/Textarea/__tests__/Textarea.test.tsx +23 -1
  240. package/src/components/ThreeColumnLayout/ThreeColumnLayout.module.css +35 -0
  241. package/src/components/ThreeColumnLayout/ThreeColumnLayout.stories.tsx +152 -0
  242. package/src/components/ThreeColumnLayout/ThreeColumnLayout.tsx +87 -0
  243. package/src/components/ThreeColumnLayout/__tests__/ThreeColumnLayout.test.tsx +78 -0
  244. package/src/components/Toast/Toast.module.css +62 -3
  245. package/src/components/Toast/Toast.stories.tsx +1 -1
  246. package/src/components/Tooltip/Tooltip.module.css +1 -1
  247. package/src/components/UserMenu/UserMenu.module.css +90 -0
  248. package/src/components/UserMenu/UserMenu.stories.tsx +49 -0
  249. package/src/components/UserMenu/UserMenu.tsx +132 -0
  250. package/src/components/UserMenu/__tests__/UserMenu.test.tsx +77 -0
  251. package/src/hooks/__tests__/usePublishShellChromeHeight.test.tsx +118 -0
  252. package/src/hooks/__tests__/useReorderSensors.test.ts +26 -0
  253. package/src/hooks/__tests__/useSnapCarousel.test.tsx +82 -0
  254. package/src/hooks/__tests__/useSwipeStack.test.tsx +155 -0
  255. package/src/hooks/index.ts +9 -0
  256. package/src/hooks/usePublishShellChromeHeight.ts +96 -0
  257. package/src/hooks/useReorderSensors.ts +88 -0
  258. package/src/hooks/useSnapCarousel.ts +211 -6
  259. package/src/hooks/useSwipeStack.ts +235 -0
  260. package/src/index.ts +61 -1
  261. package/src/overlay-shell.ts +65 -0
  262. package/src/stories/DesignSystemOverview.stories.tsx +617 -11
  263. package/src/stories/GripIcon.stories.tsx +29 -0
  264. package/src/stories/LayoutSystem.stories.tsx +639 -0
  265. package/src/stories/MenuEntries.stories.tsx +86 -0
  266. package/src/stories/NavList.stories.tsx +147 -0
  267. package/src/stories/PageContainer.stories.tsx +52 -7
  268. package/src/stories/PageHeader.stories.tsx +54 -1
  269. package/src/stories/StepStrip.stories.tsx +76 -0
  270. package/src/stories/SwipableMobileCarousel.stories.tsx +58 -0
  271. package/src/stories/SwipeStack.stories.tsx +200 -0
  272. package/src/stories/Textarea.stories.tsx +12 -0
  273. package/src/stories/TokenGallery.stories.tsx +50 -0
  274. package/src/tokens/primitives.css +11 -0
  275. package/src/tokens/semantic.css +32 -0
  276. package/src/tokens/theme-presets.css +33 -0
  277. package/src/tokens/theme-presets.ts +72 -0
@@ -0,0 +1,185 @@
1
+ 'use client';
2
+
3
+ import { Children } from 'react';
4
+ import type { ReactNode } from 'react';
5
+ import { HeaderFooterLayout } from '../HeaderFooterLayout/HeaderFooterLayout';
6
+ import { ResponsiveSurface } from '../ResponsiveSurface/ResponsiveSurface';
7
+ import { ThreeColumnLayout } from '../ThreeColumnLayout/ThreeColumnLayout';
8
+ import styles from './RootLayout.module.css';
9
+
10
+ export type RootLayoutVariant = 'plain' | 'sidebar' | 'header' | 'shell';
11
+
12
+ export interface RootLayoutProps {
13
+ /**
14
+ * Which chrome shape this page uses. Children are positional and their
15
+ * count is fixed per variant:
16
+ * - `'plain'` — 1 child: `[main]`. No chrome on either breakpoint.
17
+ * - `'sidebar'` — 2 children: `[sidebar, main]`. Sidebar + main on web
18
+ * only — on mobile, sidebar is dropped and only main renders.
19
+ * - `'header'` — 2 children: `[header, main]`. Header + main on both
20
+ * breakpoints — same structure, but the header's own height differs
21
+ * per breakpoint (`headerHeight` on web, `mobileHeaderHeight` on
22
+ * mobile), matching how real `shell: minimal` plugins actually do
23
+ * this (a compact web top bar vs. the platform's mobile chrome
24
+ * height).
25
+ * - `'shell'` — 3 children: `[header, main, footer]`. Header + main +
26
+ * footer on mobile only — on web, header/footer are dropped and only
27
+ * main renders.
28
+ * Default `'plain'`.
29
+ */
30
+ variant?: RootLayoutVariant;
31
+ children: ReactNode;
32
+ /** px. Default 64 — matches the platform's own persistent nav rail
33
+ * (`--sv-shell-sidebar-width`, runtime/app/(platform)/shell.module.css).
34
+ * Only used by `'sidebar'`. */
35
+ sidebarWidth?: number;
36
+ /** px. Default 48 — matches a real `shell: minimal` plugin's own compact
37
+ * web header (e.g. Kanban's `KanbanHeader`, deliberately not the
38
+ * platform's 60px shell header since a `shell: minimal` plugin owns its
39
+ * whole viewport). Web only — used by `'header'` on web. */
40
+ headerHeight?: number;
41
+ /** px. Default 60 — matches the platform's own mobile chrome
42
+ * (`--sv-shell-header-height`, `MobileHeader`'s default). Mobile only —
43
+ * used by `'header'` and `'shell'` on mobile. */
44
+ mobileHeaderHeight?: number;
45
+ /** px. Default 60 — matches the platform's own mobile chrome
46
+ * (`--sv-shell-footer-height`, `MobileFooter`'s default). Mobile only —
47
+ * used by `'shell'`. */
48
+ footerHeight?: number;
49
+ className?: string;
50
+ }
51
+
52
+ const EXPECTED_CHILD_COUNT: Record<RootLayoutVariant, number> = {
53
+ plain: 1,
54
+ sidebar: 2,
55
+ header: 2,
56
+ shell: 3,
57
+ };
58
+
59
+ interface RootLayoutWebProps {
60
+ variant: RootLayoutVariant;
61
+ items: ReactNode[];
62
+ sidebarWidth: number;
63
+ headerHeight: number;
64
+ }
65
+
66
+ // internal — not exported
67
+ function RootLayoutWeb({ variant, items, sidebarWidth, headerHeight }: RootLayoutWebProps) {
68
+ if (variant === 'sidebar') {
69
+ const [sidebar, main] = items;
70
+ return (
71
+ <ThreeColumnLayout sidebarWidth={sidebarWidth}>
72
+ {sidebar}
73
+ {main}
74
+ </ThreeColumnLayout>
75
+ );
76
+ }
77
+ if (variant === 'header') {
78
+ const [header, main] = items;
79
+ return (
80
+ <HeaderFooterLayout header={header} headerHeight={headerHeight}>
81
+ {main}
82
+ </HeaderFooterLayout>
83
+ );
84
+ }
85
+ // 'plain' and 'shell' both render plain on web — shell's chrome is mobile-only.
86
+ const main = variant === 'shell' ? items[1] : items[0];
87
+ return <>{main}</>;
88
+ }
89
+
90
+ interface RootLayoutMobileProps {
91
+ variant: RootLayoutVariant;
92
+ items: ReactNode[];
93
+ mobileHeaderHeight: number;
94
+ footerHeight: number;
95
+ }
96
+
97
+ // internal — not exported
98
+ function RootLayoutMobile({
99
+ variant,
100
+ items,
101
+ mobileHeaderHeight,
102
+ footerHeight,
103
+ }: RootLayoutMobileProps) {
104
+ if (variant === 'header') {
105
+ const [header, main] = items;
106
+ return (
107
+ <HeaderFooterLayout header={header} headerHeight={mobileHeaderHeight}>
108
+ {main}
109
+ </HeaderFooterLayout>
110
+ );
111
+ }
112
+ if (variant === 'shell') {
113
+ const [header, main, footer] = items;
114
+ return (
115
+ <HeaderFooterLayout
116
+ header={header}
117
+ footer={footer}
118
+ headerHeight={mobileHeaderHeight}
119
+ footerHeight={footerHeight}
120
+ >
121
+ {main}
122
+ </HeaderFooterLayout>
123
+ );
124
+ }
125
+ // 'plain' and 'sidebar' both render plain on mobile — sidebar's chrome is web-only.
126
+ const main = variant === 'sidebar' ? items[1] : items[0];
127
+ return <>{main}</>;
128
+ }
129
+
130
+ /**
131
+ * RootLayout — the root-level layout a plugin's page tree sits in. Enforces
132
+ * only structure and dimensions (`width: 100%; height: 100%`, filling
133
+ * whatever real height the platform shell's own ancestor chain already
134
+ * establishes — not viewport units, since a plugin isn't alone on the
135
+ * page). A definite height, not min-height: ThreeColumnLayout/
136
+ * HeaderFooterLayout's own .shell need one to resolve their own height:
137
+ * 100% against (see RootLayout.module.css's comment). All content comes
138
+ * from children; RootLayout has no opinion on what's inside any region.
139
+ *
140
+ * The web/mobile fork is `ResponsiveSurface` — only one of
141
+ * `RootLayoutWeb`/`RootLayoutMobile` is ever mounted, never a CSS squeeze of
142
+ * the same tree. Each variant's actual shape is delegated entirely to
143
+ * `ThreeColumnLayout`/`HeaderFooterLayout`; RootLayout itself contains no
144
+ * media queries.
145
+ */
146
+ export function RootLayout({
147
+ variant = 'plain',
148
+ children,
149
+ sidebarWidth = 64,
150
+ headerHeight = 48,
151
+ mobileHeaderHeight = 60,
152
+ footerHeight = 60,
153
+ className,
154
+ }: RootLayoutProps) {
155
+ const items = Children.toArray(children);
156
+
157
+ if (process.env.NODE_ENV !== 'production' && items.length !== EXPECTED_CHILD_COUNT[variant]) {
158
+ console.warn(
159
+ `[RootLayout] variant="${variant}" expects ${EXPECTED_CHILD_COUNT[variant]} children; received ${items.length}.`,
160
+ );
161
+ }
162
+
163
+ return (
164
+ <div className={[styles.root, className].filter(Boolean).join(' ')}>
165
+ <ResponsiveSurface
166
+ web={
167
+ <RootLayoutWeb
168
+ variant={variant}
169
+ items={items}
170
+ sidebarWidth={sidebarWidth}
171
+ headerHeight={headerHeight}
172
+ />
173
+ }
174
+ mobile={
175
+ <RootLayoutMobile
176
+ variant={variant}
177
+ items={items}
178
+ mobileHeaderHeight={mobileHeaderHeight}
179
+ footerHeight={footerHeight}
180
+ />
181
+ }
182
+ />
183
+ </div>
184
+ );
185
+ }
@@ -0,0 +1,161 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { cleanup, render, screen } from '@testing-library/react';
4
+ import { RootLayout } from '../RootLayout';
5
+
6
+ let matches = false;
7
+
8
+ function installMatchMedia() {
9
+ vi.stubGlobal(
10
+ 'matchMedia',
11
+ vi.fn().mockImplementation((query: string) => ({
12
+ matches,
13
+ media: query,
14
+ addEventListener: vi.fn(),
15
+ removeEventListener: vi.fn(),
16
+ addListener: vi.fn(),
17
+ removeListener: vi.fn(),
18
+ dispatchEvent: vi.fn(),
19
+ })),
20
+ );
21
+ }
22
+
23
+ describe('RootLayout', () => {
24
+ beforeEach(() => {
25
+ matches = false;
26
+ installMatchMedia();
27
+ });
28
+ afterEach(() => {
29
+ vi.unstubAllGlobals();
30
+ cleanup();
31
+ });
32
+
33
+ it('plain: renders the single child on web and mobile', () => {
34
+ render(
35
+ <RootLayout variant="plain">
36
+ <span>Main</span>
37
+ </RootLayout>,
38
+ );
39
+ expect(screen.getByText('Main')).toBeDefined();
40
+ cleanup();
41
+
42
+ matches = true;
43
+ installMatchMedia();
44
+ render(
45
+ <RootLayout variant="plain">
46
+ <span>Main</span>
47
+ </RootLayout>,
48
+ );
49
+ expect(screen.getByText('Main')).toBeDefined();
50
+ });
51
+
52
+ it('sidebar: renders sidebar + main on web, main only on mobile', () => {
53
+ render(
54
+ <RootLayout variant="sidebar">
55
+ <nav>Sidebar</nav>
56
+ <main>Main</main>
57
+ </RootLayout>,
58
+ );
59
+ expect(screen.getByText('Sidebar')).toBeDefined();
60
+ expect(screen.getByText('Main')).toBeDefined();
61
+ cleanup();
62
+
63
+ matches = true;
64
+ installMatchMedia();
65
+ render(
66
+ <RootLayout variant="sidebar">
67
+ <nav>Sidebar</nav>
68
+ <main>Main</main>
69
+ </RootLayout>,
70
+ );
71
+ expect(screen.queryByText('Sidebar')).toBeNull();
72
+ expect(screen.getByText('Main')).toBeDefined();
73
+ });
74
+
75
+ it('header: renders header + main identically on web and mobile', () => {
76
+ render(
77
+ <RootLayout variant="header">
78
+ <header>Header</header>
79
+ <main>Main</main>
80
+ </RootLayout>,
81
+ );
82
+ expect(screen.getByText('Header')).toBeDefined();
83
+ expect(screen.getByText('Main')).toBeDefined();
84
+ cleanup();
85
+
86
+ matches = true;
87
+ installMatchMedia();
88
+ render(
89
+ <RootLayout variant="header">
90
+ <header>Header</header>
91
+ <main>Main</main>
92
+ </RootLayout>,
93
+ );
94
+ expect(screen.getByText('Header')).toBeDefined();
95
+ expect(screen.getByText('Main')).toBeDefined();
96
+ });
97
+
98
+ it('header: applies headerHeight on web and mobileHeaderHeight on mobile', () => {
99
+ render(
100
+ <RootLayout variant="header">
101
+ <header>Header</header>
102
+ <main>Main</main>
103
+ </RootLayout>,
104
+ );
105
+ expect(screen.getByText('Header').parentElement?.style.height).toBe('48px');
106
+ cleanup();
107
+
108
+ matches = true;
109
+ installMatchMedia();
110
+ render(
111
+ <RootLayout variant="header">
112
+ <header>Header</header>
113
+ <main>Main</main>
114
+ </RootLayout>,
115
+ );
116
+ expect(screen.getByText('Header').parentElement?.style.height).toBe('60px');
117
+ });
118
+
119
+ it('shell: renders main only on web, header + main + footer on mobile', () => {
120
+ render(
121
+ <RootLayout variant="shell">
122
+ <header>Header</header>
123
+ <main>Main</main>
124
+ <footer>Footer</footer>
125
+ </RootLayout>,
126
+ );
127
+ expect(screen.queryByText('Header')).toBeNull();
128
+ expect(screen.getByText('Main')).toBeDefined();
129
+ expect(screen.queryByText('Footer')).toBeNull();
130
+ cleanup();
131
+
132
+ matches = true;
133
+ installMatchMedia();
134
+ render(
135
+ <RootLayout variant="shell">
136
+ <header>Header</header>
137
+ <main>Main</main>
138
+ <footer>Footer</footer>
139
+ </RootLayout>,
140
+ );
141
+ expect(screen.getByText('Header')).toBeDefined();
142
+ expect(screen.getByText('Main')).toBeDefined();
143
+ expect(screen.getByText('Footer')).toBeDefined();
144
+ });
145
+
146
+ it('warns in development when the child count does not match the variant', () => {
147
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
148
+
149
+ render(
150
+ <RootLayout variant="sidebar">
151
+ <span>Only one child</span>
152
+ </RootLayout>,
153
+ );
154
+
155
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('variant="sidebar"'));
156
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('expects 2'));
157
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('received 1'));
158
+
159
+ warn.mockRestore();
160
+ });
161
+ });
@@ -2,7 +2,7 @@
2
2
  display: inline-flex;
3
3
  background: var(--sv-color-surface-sunken);
4
4
  border-radius: var(--sv-radius-full);
5
- border: 1px solid var(--sv-color-border);
5
+ border: var(--sv-border-width-hairline) solid var(--sv-color-border);
6
6
  padding: 2px;
7
7
  gap: 2px;
8
8
  }
@@ -18,7 +18,7 @@
18
18
  line-height: 1.5;
19
19
  color: var(--sv-color-text-primary);
20
20
  background-color: var(--sv-color-surface);
21
- border: 1px solid var(--sv-color-border-strong);
21
+ border: var(--sv-border-width-hairline) solid var(--sv-color-border-strong);
22
22
  border-radius: var(--sv-radius-md);
23
23
  cursor: pointer;
24
24
  /* Remove native OS chevron */
@@ -1,6 +1,9 @@
1
1
  /* Sheet — fills the same rect as a plugin's content area (between the shell's
2
2
  * sticky header and footer nav — see --sv-shell-header-height /
3
- * --sv-shell-footer-height, set by the shell), sliding in from an edge
3
+ * --sv-shell-footer-height, set by the platform shell or, for a plugin that
4
+ * self-renders MobileHeader/MobileFooter instead (shellConfig.mobileHeader/
5
+ * mobileFooter: false), by that component itself — see
6
+ * usePublishShellChromeHeight), sliding in from an edge
4
7
  * instead of Dialog's centered box or Drawer's partial-height sheet-over-
5
8
  * scrim. z-index matches Dialog/Drawer's scrim so it layers consistently
6
9
  * with any other overlay. No scrim of its own — see the component doc
@@ -3,6 +3,7 @@
3
3
  import { type ReactNode, useRef } from 'react';
4
4
  import { useMountTransition, usePrefersReducedMotion } from '../../motion';
5
5
  import {
6
+ OVERLAY_MOTION_DURATION_MS,
6
7
  useOverlayFocusCapture,
7
8
  useOverlayKeyboardTrap,
8
9
  useOverlayScrollLock,
@@ -40,10 +41,6 @@ export interface SheetProps {
40
41
  children: ReactNode;
41
42
  }
42
43
 
43
- // Matches --sv-motion-duration-base (Sheet.module.css) — see Dialog.tsx's
44
- // identical constant for why this stays a plain JS number.
45
- const MOTION_DURATION_MS = 250;
46
-
47
44
  /**
48
45
  * Sheet — a full-page overlay that replaces a plugin's content area between
49
46
  * the shell's fixed header and footer (RFC 0013's mobile chrome stays visible
@@ -73,7 +70,10 @@ export function Sheet({
73
70
  }: SheetProps) {
74
71
  const panelRef = useRef<HTMLDivElement>(null);
75
72
  const reducedMotion = usePrefersReducedMotion();
76
- const { mounted, phase } = useMountTransition(open, reducedMotion ? 0 : MOTION_DURATION_MS);
73
+ const { mounted, phase } = useMountTransition(
74
+ open,
75
+ reducedMotion ? 0 : OVERLAY_MOTION_DURATION_MS,
76
+ );
77
77
 
78
78
  useOverlayScrollLock(mounted);
79
79
  useOverlayFocusCapture(panelRef, mounted);
@@ -5,7 +5,7 @@
5
5
  minmax(0, calc(100% - var(--split-pane-primary-size, 50%)));
6
6
  width: 100%;
7
7
  min-height: 320px;
8
- border: 1px solid var(--sv-color-border);
8
+ border: var(--sv-border-width-hairline) solid var(--sv-color-border);
9
9
  border-radius: var(--sv-radius-lg);
10
10
  overflow: hidden;
11
11
  background: var(--sv-color-surface);
@@ -25,7 +25,7 @@
25
25
  }
26
26
 
27
27
  .pane + .pane {
28
- border-left: 1px solid var(--sv-color-border);
28
+ border-left: var(--sv-border-width-hairline) solid var(--sv-color-border);
29
29
  }
30
30
 
31
31
  .handle {
@@ -34,7 +34,7 @@
34
34
  min-width: 12px;
35
35
  padding: 0;
36
36
  border: 0;
37
- border-inline: 1px solid var(--sv-color-border);
37
+ border-inline: var(--sv-border-width-hairline) solid var(--sv-color-border);
38
38
  cursor: col-resize;
39
39
  background: var(--sv-color-surface-sunken);
40
40
  }
@@ -79,7 +79,7 @@
79
79
  }
80
80
 
81
81
  .pane + .pane {
82
- border-top: 1px solid var(--sv-color-border);
82
+ border-top: var(--sv-border-width-hairline) solid var(--sv-color-border);
83
83
  border-left: 0;
84
84
  }
85
85
  }
@@ -5,7 +5,7 @@
5
5
  min-height: 24px;
6
6
  gap: var(--sv-space-1);
7
7
  padding: 2px var(--sv-space-2);
8
- border: 1px solid var(--sv-color-border);
8
+ border: var(--sv-border-width-hairline) solid var(--sv-color-border);
9
9
  border-radius: var(--sv-radius-full);
10
10
  font-family: var(--sv-font-family);
11
11
  font-size: var(--sv-font-size-xs);
@@ -0,0 +1,53 @@
1
+ .strip {
2
+ display: flex;
3
+ align-items: stretch;
4
+ overflow-x: auto;
5
+ }
6
+
7
+ .step {
8
+ display: flex;
9
+ align-items: center;
10
+ flex-shrink: 0;
11
+ }
12
+
13
+ .connector {
14
+ width: var(--sv-space-4);
15
+ height: 2px;
16
+ flex-shrink: 0;
17
+ background: var(--sv-color-border);
18
+ }
19
+
20
+ .connectorDashed {
21
+ background-image: repeating-linear-gradient(
22
+ to right,
23
+ var(--sv-color-border) 0,
24
+ var(--sv-color-border) 4px,
25
+ transparent 4px,
26
+ transparent 7px
27
+ );
28
+ }
29
+
30
+ .addChip {
31
+ all: unset;
32
+ box-sizing: border-box;
33
+ flex-shrink: 0;
34
+ display: flex;
35
+ align-items: center;
36
+ gap: var(--sv-space-1);
37
+ padding: var(--sv-space-3) var(--sv-space-4);
38
+ border: var(--sv-border-width-hairline) dashed var(--sv-color-border);
39
+ border-radius: var(--sv-radius-lg);
40
+ color: var(--sv-color-text-muted);
41
+ font-size: var(--sv-font-size-sm);
42
+ cursor: pointer;
43
+ }
44
+
45
+ .addChip:hover {
46
+ background: var(--sv-color-surface-sunken);
47
+ color: var(--sv-color-text-primary);
48
+ }
49
+
50
+ .addChip:focus-visible {
51
+ outline: 2px solid var(--sv-color-focus-ring);
52
+ outline-offset: 2px;
53
+ }
@@ -0,0 +1,74 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Icon } from '../Icon/Icon';
3
+ import styles from './StepStrip.module.css';
4
+
5
+ export interface StepStripItem {
6
+ id: string;
7
+ }
8
+
9
+ export interface StepStripProps<T extends StepStripItem> {
10
+ items: T[];
11
+ activeId: string | null;
12
+ /** Full control over each chip's content, click handling, and (if needed) drag-reorder wiring. */
13
+ renderItem: (item: T, state: { isActive: boolean; index: number }) => ReactNode;
14
+ /** Trailing dashed "add" chip — omit to render the strip with no add affordance. */
15
+ onAdd?: () => void;
16
+ addLabel?: string;
17
+ className?: string;
18
+ 'aria-label'?: string;
19
+ }
20
+
21
+ /**
22
+ * StepStrip — a horizontal, connected sequence of steps (a route or
23
+ * itinerary strip): ordered chips joined by a thin connector line, with an
24
+ * optional trailing dashed "add" chip. Purely presentational — it owns
25
+ * layout and the connector lines, nothing else.
26
+ *
27
+ * Each chip's content, active styling, and click handling are entirely up
28
+ * to `renderItem`; if a consumer needs drag-reorder, `renderItem` attaches
29
+ * its own dnd-kit `useSortable` ref/listeners to whatever it returns. This
30
+ * mirrors `DragHandleRow`'s existing split (DS owns chrome, consumer wires
31
+ * dnd-kit via passed-through props) generalized for a horizontal,
32
+ * handle-less strip where the whole rendered item — not a separate handle
33
+ * icon — is both the click target and the drag surface.
34
+ */
35
+ export function StepStrip<T extends StepStripItem>({
36
+ items,
37
+ activeId,
38
+ renderItem,
39
+ onAdd,
40
+ addLabel = 'Add',
41
+ className,
42
+ 'aria-label': ariaLabel,
43
+ }: StepStripProps<T>) {
44
+ return (
45
+ <div
46
+ className={[styles.strip, className].filter(Boolean).join(' ')}
47
+ role="list"
48
+ aria-label={ariaLabel}
49
+ >
50
+ {items.map((item, index) => (
51
+ <div key={item.id} className={styles.step} role="listitem">
52
+ {renderItem(item, { isActive: item.id === activeId, index })}
53
+ {(index < items.length - 1 || onAdd) && (
54
+ <div
55
+ className={[
56
+ styles.connector,
57
+ index === items.length - 1 ? styles.connectorDashed : '',
58
+ ]
59
+ .filter(Boolean)
60
+ .join(' ')}
61
+ aria-hidden="true"
62
+ />
63
+ )}
64
+ </div>
65
+ ))}
66
+ {onAdd && (
67
+ <button type="button" className={styles.addChip} onClick={onAdd}>
68
+ <Icon name="plus" size="sm" aria-hidden={true} />
69
+ {addLabel}
70
+ </button>
71
+ )}
72
+ </div>
73
+ );
74
+ }
@@ -7,7 +7,7 @@
7
7
  line-height: 1.5;
8
8
  color: var(--sv-color-text-primary);
9
9
  background-color: var(--sv-color-surface);
10
- border: 1px solid var(--sv-color-border-strong);
10
+ border: var(--sv-border-width-hairline) solid var(--sv-color-border-strong);
11
11
  border-radius: var(--sv-radius-md);
12
12
  transition:
13
13
  border-color 0.15s ease,
@@ -99,5 +99,5 @@
99
99
 
100
100
  .divider {
101
101
  margin: var(--sv-space-1) var(--sv-space-2);
102
- border-top: 1px solid var(--sv-color-border);
102
+ border-top: var(--sv-border-width-hairline) solid var(--sv-color-border);
103
103
  }
@@ -39,10 +39,19 @@
39
39
  flex-direction: column;
40
40
  }
41
41
 
42
- .dots {
42
+ .indicatorSlot {
43
43
  /* Overlays the bottom of .wrap rather than taking its own layout row —
44
44
  matches both sovereign-tasks' and sovereign-shopper's existing dots
45
- placement. */
45
+ placement. Applied by the carousel itself around WHATEVER
46
+ renderIndicator returns (the default SwipableMobileCarouselDots or a
47
+ caller-supplied override) — a custom renderIndicator has no way to
48
+ import this module's locally-scoped class itself (CSS Modules classnames
49
+ aren't cross-file importable), so positioning must live here, not be
50
+ something each indicator implementation opts into. Previously this class
51
+ was only ever applied to the *default* indicator branch, which silently
52
+ left any custom renderIndicator unpositioned — a static, in-flow sibling
53
+ pushed below the full-height .scroller and clipped out of view instead
54
+ of overlaid on top of it. */
46
55
  position: absolute;
47
56
  left: 0;
48
57
  right: 0;
@@ -50,6 +59,6 @@
50
59
  pointer-events: none;
51
60
  }
52
61
 
53
- .dots > * {
62
+ .indicatorSlot > * {
54
63
  pointer-events: auto;
55
64
  }