@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
@@ -63,16 +63,24 @@ describe('Drawer', () => {
63
63
  expect(panel.getAttribute('aria-modal')).toBe('true');
64
64
  });
65
65
 
66
- it('calls onClose on Escape', () => {
66
+ it('calls onClose exactly once on Escape with focus inside the panel', () => {
67
67
  const onClose = vi.fn();
68
68
  render(
69
- <Drawer open onClose={onClose}>
70
- Body
69
+ <Drawer open onClose={onClose} aria-label="Panel">
70
+ <button>Focus me</button>
71
71
  </Drawer>,
72
72
  );
73
- // Keyboard handler is registered on document (not the drawer element) so
74
- // screen readers and keyboard users can dismiss from anywhere in the page.
75
- fireEvent.keyDown(document, { key: 'Escape' });
73
+ // Dispatched from the actually-focused element (matching real usage,
74
+ // where useOverlayFocusCapture moves focus into the panel on open) so the
75
+ // keydown bubbles through the scrim on its way to document — the exact
76
+ // path that used to trigger both the scrim's own (now-removed) Escape
77
+ // handler and useOverlayKeyboardTrap's document-level listener, double-
78
+ // firing onClose. Dispatching straight on `document` (the previous form
79
+ // of this test) skips that bubble path and would pass even if the bug
80
+ // were reintroduced.
81
+ const button = screen.getByRole('button', { name: 'Focus me' });
82
+ button.focus();
83
+ fireEvent.keyDown(button, { key: 'Escape' });
76
84
  expect(onClose).toHaveBeenCalledOnce();
77
85
  });
78
86
 
@@ -120,6 +128,25 @@ describe('Drawer', () => {
120
128
  expect(screen.getAllByRole('button')).toHaveLength(1);
121
129
  });
122
130
 
131
+ it('renders children inside a scrollable body, separate from the grab handle', () => {
132
+ // Regression test: the panel used to be the sole scroll container
133
+ // (overflow-y: auto on .panel itself), so a tall drawer scrolled the
134
+ // grab handle out of view along with the content. The handle must stay
135
+ // a sibling of the scrollable region, not a descendant of it.
136
+ render(
137
+ <Drawer open onClose={() => {}} aria-label="Nav">
138
+ <button type="button">Own action</button>
139
+ </Drawer>,
140
+ );
141
+ const panel = screen.getByRole('dialog');
142
+ const handle = panel.firstElementChild as HTMLElement;
143
+ const button = screen.getByRole('button', { name: 'Own action' });
144
+ const body = button.parentElement as HTMLElement;
145
+ expect(body).not.toBe(panel);
146
+ expect(body.contains(handle)).toBe(false);
147
+ expect(panel.contains(body)).toBe(true);
148
+ });
149
+
123
150
  it('renders OverlayHeader (title + close) when title is provided', () => {
124
151
  const onClose = vi.fn();
125
152
  render(
@@ -0,0 +1,27 @@
1
+ export interface GripIconProps {
2
+ size?: number;
3
+ className?: string;
4
+ }
5
+
6
+ /** Six-dot drag-handle affordance. No baked-in pointer-events or color —
7
+ * a consumer that needs `pointer-events: none` (e.g. a button wrapping it)
8
+ * or a specific color passes those via `className`/`currentColor`. */
9
+ export function GripIcon({ size = 14, className }: GripIconProps) {
10
+ return (
11
+ <svg
12
+ width={size}
13
+ height={size}
14
+ viewBox="0 0 14 14"
15
+ fill="none"
16
+ aria-hidden="true"
17
+ className={className}
18
+ >
19
+ {/* Two columns of three dots */}
20
+ {[3, 7, 11].map((cy) =>
21
+ [4, 10].map((cx) => (
22
+ <circle key={`${cx}-${cy}`} cx={cx} cy={cy} r={1.2} fill="currentColor" />
23
+ )),
24
+ )}
25
+ </svg>
26
+ );
27
+ }
@@ -0,0 +1,62 @@
1
+ .header {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: space-between;
5
+ min-height: 48px;
6
+ padding: var(--sv-space-2) var(--sv-space-4);
7
+ border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
8
+ background-color: var(--sv-color-surface);
9
+ }
10
+
11
+ .left {
12
+ display: flex;
13
+ align-items: center;
14
+ gap: var(--sv-space-2);
15
+ min-width: 0;
16
+ }
17
+
18
+ .right {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: var(--sv-space-1);
22
+ flex-shrink: 0;
23
+ }
24
+
25
+ .brandBadge {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ flex-shrink: 0;
30
+ width: 28px;
31
+ height: 28px;
32
+ border-radius: var(--sv-radius-md);
33
+ background-color: var(--sv-color-accent);
34
+ color: var(--sv-color-text-on-accent);
35
+ font-size: var(--sv-font-size-xs);
36
+ font-weight: var(--sv-font-weight-semibold);
37
+ text-decoration: none;
38
+ overflow: hidden;
39
+ }
40
+
41
+ .brandLogoImg {
42
+ width: 100%;
43
+ height: 100%;
44
+ object-fit: cover;
45
+ }
46
+
47
+ .plugin {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: var(--sv-space-1);
51
+ min-width: 0;
52
+ text-decoration: none;
53
+ color: var(--sv-color-text-primary);
54
+ }
55
+
56
+ .pluginName {
57
+ font-size: var(--sv-font-size-md);
58
+ font-weight: var(--sv-font-weight-semibold);
59
+ overflow: hidden;
60
+ text-overflow: ellipsis;
61
+ white-space: nowrap;
62
+ }
@@ -0,0 +1,452 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Header } from './Header';
3
+ import { MobileHeader } from '../MobileHeader/MobileHeader';
4
+ import { AppsLauncher } from '../AppsLauncher/AppsLauncher';
5
+ import { Icon, type IconName } from '../Icon/Icon';
6
+ import { NotificationsPanel } from '../NotificationsPanel/NotificationsPanel';
7
+ import { UserMenu } from '../UserMenu/UserMenu';
8
+ import { useIsMobile } from '../../hooks';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Shared doc primitives (mirrors Overview/Mobile Patterns' own doc styling)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const ff = 'var(--sv-font-family)';
15
+
16
+ function Heading({ level = 2, children }: { level?: 1 | 2 | 3; children: React.ReactNode }) {
17
+ const sizes: Record<number, string> = { 1: '1.75rem', 2: '1.125rem', 3: '0.9375rem' };
18
+ const weights: Record<number, number> = { 1: 700, 2: 600, 3: 600 };
19
+ const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
20
+ return (
21
+ <Tag
22
+ style={{
23
+ fontFamily: ff,
24
+ fontSize: sizes[level],
25
+ fontWeight: weights[level],
26
+ color: 'var(--sv-color-text-primary)',
27
+ margin: 0,
28
+ lineHeight: 1.3,
29
+ }}
30
+ >
31
+ {children}
32
+ </Tag>
33
+ );
34
+ }
35
+
36
+ function Body({ children }: { children: React.ReactNode }) {
37
+ return (
38
+ <p
39
+ style={{
40
+ fontFamily: ff,
41
+ fontSize: 'var(--sv-font-size-sm)',
42
+ color: 'var(--sv-color-text-muted)',
43
+ margin: '0.5rem 0 0',
44
+ lineHeight: 1.6,
45
+ }}
46
+ >
47
+ {children}
48
+ </p>
49
+ );
50
+ }
51
+
52
+ function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }) {
53
+ return (
54
+ <div
55
+ style={{
56
+ paddingBottom: 'var(--sv-space-3)',
57
+ borderBottom: '2px solid var(--sv-color-accent)',
58
+ marginBottom: 'var(--sv-space-5)',
59
+ }}
60
+ >
61
+ <Heading level={2}>{title}</Heading>
62
+ {subtitle && <Body>{subtitle}</Body>}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ function Callout({
68
+ type = 'info',
69
+ children,
70
+ }: {
71
+ type?: 'info' | 'warn' | 'tip';
72
+ children: React.ReactNode;
73
+ }) {
74
+ const map = {
75
+ info: {
76
+ bg: 'var(--sv-color-info-surface)',
77
+ border: 'var(--sv-color-info-border)',
78
+ text: 'var(--sv-color-info-text)',
79
+ label: 'ℹ',
80
+ },
81
+ warn: {
82
+ bg: 'var(--sv-color-warning-surface)',
83
+ border: 'var(--sv-color-warning-border)',
84
+ text: 'var(--sv-color-warning-text)',
85
+ label: '⚠',
86
+ },
87
+ tip: {
88
+ bg: 'var(--sv-color-success-surface)',
89
+ border: 'var(--sv-color-success-border)',
90
+ text: 'var(--sv-color-success-text)',
91
+ label: '✓',
92
+ },
93
+ };
94
+ const c = map[type];
95
+ return (
96
+ <div
97
+ style={{
98
+ background: c.bg,
99
+ border: `1px solid ${c.border}`,
100
+ borderRadius: 'var(--sv-radius-md)',
101
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
102
+ display: 'flex',
103
+ gap: 'var(--sv-space-2)',
104
+ alignItems: 'flex-start',
105
+ marginBottom: 'var(--sv-space-4)',
106
+ }}
107
+ >
108
+ <span style={{ color: c.text, fontWeight: 700, flexShrink: 0, fontSize: '0.875rem' }}>
109
+ {c.label}
110
+ </span>
111
+ <p
112
+ style={{
113
+ fontFamily: ff,
114
+ fontSize: 'var(--sv-font-size-sm)',
115
+ color: c.text,
116
+ margin: 0,
117
+ lineHeight: 1.55,
118
+ }}
119
+ >
120
+ {children}
121
+ </p>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function CodeBlock({ children }: { children: string }) {
127
+ return (
128
+ <pre
129
+ style={{
130
+ fontFamily: 'var(--sv-font-family-mono)',
131
+ fontSize: '0.75rem',
132
+ background: 'var(--sv-color-surface-sunken)',
133
+ border: '1px solid var(--sv-color-border)',
134
+ borderRadius: 'var(--sv-radius-md)',
135
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
136
+ marginTop: 'var(--sv-space-3)',
137
+ overflowX: 'auto',
138
+ color: 'var(--sv-color-text-primary)',
139
+ lineHeight: 1.6,
140
+ }}
141
+ >
142
+ {children}
143
+ </pre>
144
+ );
145
+ }
146
+
147
+ function Frame({ children }: { children: React.ReactNode }) {
148
+ return (
149
+ <div
150
+ style={{
151
+ border: '1px solid var(--sv-color-border)',
152
+ borderRadius: 'var(--sv-radius-lg)',
153
+ overflow: 'hidden',
154
+ }}
155
+ >
156
+ {children}
157
+ </div>
158
+ );
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Demo pieces — the real, shared AppsLauncher/NotificationsPanel/UserMenu,
163
+ // not story-local mockups.
164
+ // ---------------------------------------------------------------------------
165
+
166
+ /**
167
+ * The plugin icon renders at the same 28px as the brand badge next to it,
168
+ * not a smaller inline glyph. `Icon`'s largest built-in size (`lg`, 24px)
169
+ * reads slightly small in a 28px box (lucide-style glyphs carry their own
170
+ * internal inset), so it's scaled up the remaining bit to actually fill the
171
+ * box, the same way a real 28px plugin-icon `<img>` asset would.
172
+ */
173
+ function HeaderPluginIcon({ name }: { name: IconName }) {
174
+ return (
175
+ <span
176
+ aria-hidden="true"
177
+ style={{
178
+ display: 'inline-flex',
179
+ alignItems: 'center',
180
+ justifyContent: 'center',
181
+ width: 28,
182
+ height: 28,
183
+ }}
184
+ >
185
+ <span style={{ display: 'inline-flex', transform: 'scale(1.15)' }}>
186
+ <Icon name={name} size="lg" aria-hidden />
187
+ </span>
188
+ </span>
189
+ );
190
+ }
191
+
192
+ const launcherItems = [
193
+ {
194
+ key: 'home',
195
+ icon: <Icon name="house" size="md" aria-hidden />,
196
+ label: 'Home',
197
+ href: '/launcher',
198
+ },
199
+ {
200
+ key: 'console',
201
+ icon: <Icon name="layout-dashboard" size="md" aria-hidden />,
202
+ label: 'Console',
203
+ href: '/console',
204
+ },
205
+ {
206
+ key: 'notes',
207
+ icon: <Icon name="file" size="md" aria-hidden />,
208
+ label: 'Notes',
209
+ href: '/notes',
210
+ },
211
+ ];
212
+
213
+ const notificationItems = [
214
+ {
215
+ id: '1',
216
+ icon: <Icon name="layers" size="sm" aria-hidden />,
217
+ title: 'Added to a project',
218
+ timeLabel: '2d ago',
219
+ read: false,
220
+ onDismiss: () => {},
221
+ },
222
+ ];
223
+
224
+ const accountItems = [
225
+ { label: 'Account', icon: 'user' as const, href: '/account' },
226
+ { label: 'Preferences', icon: 'sliders-horizontal' as const, href: '/account/preferences' },
227
+ { label: 'Sign out', icon: 'log-out' as const, destructive: true, onSelect: () => {} },
228
+ ];
229
+
230
+ function Launcher() {
231
+ return <AppsLauncher items={launcherItems} />;
232
+ }
233
+
234
+ function Notifications() {
235
+ return <NotificationsPanel items={notificationItems} unreadCount={1} />;
236
+ }
237
+
238
+ function Account() {
239
+ return <UserMenu name="Kasun Benthara" email="kasun@openfs.io" items={accountItems} size="sm" />;
240
+ }
241
+
242
+ /**
243
+ * `Header` is web-only (`shell: minimal`'s desktop top bar) — a plugin
244
+ * renders its own mobile equivalent alongside it, gated with `useIsMobile`
245
+ * (not CSS, to avoid mounting the wrong chrome's hooks on the wrong
246
+ * surface). Resize the Storybook viewport below 768px to see `MobileHeader`
247
+ * render instead. The launcher/notifications/avatar triggers are the real,
248
+ * shared `AppsLauncher`/`NotificationsPanel`/`UserMenu` components — shared
249
+ * verbatim across both branches, not two implementations of the same
250
+ * dropdown, and fully clickable here.
251
+ */
252
+ function ResponsiveHeaderDemo() {
253
+ const isMobile = useIsMobile();
254
+
255
+ if (isMobile) {
256
+ return <MobileHeader title="Sovereign" bell={<Notifications />} avatarMenu={<Account />} />;
257
+ }
258
+
259
+ return (
260
+ <Header
261
+ instanceName="Sovereign"
262
+ homeHref="/launcher"
263
+ plugin={{
264
+ name: 'Notes',
265
+ icon: <HeaderPluginIcon name="file" />,
266
+ href: '/notes',
267
+ }}
268
+ launcher={<Launcher />}
269
+ notifications={<Notifications />}
270
+ avatarMenu={<Account />}
271
+ />
272
+ );
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Default — the documentation page
277
+ // ---------------------------------------------------------------------------
278
+
279
+ function HeaderDoc() {
280
+ return (
281
+ <div
282
+ style={{
283
+ padding: 'var(--sv-space-6)',
284
+ background: 'var(--sv-color-surface)',
285
+ minHeight: '100vh',
286
+ fontFamily: ff,
287
+ maxWidth: 760,
288
+ margin: '0 auto',
289
+ }}
290
+ >
291
+ <div style={{ marginBottom: 'var(--sv-space-8)' }}>
292
+ <Heading level={1}>Header</Heading>
293
+ <Body>
294
+ The web top-bar header for <code>shell: minimal</code> plugins — a plugin that owns its
295
+ whole viewport (no platform sidebar) renders this instead, so it doesn't have to hand-roll
296
+ its own top bar from scratch.
297
+ </Body>
298
+ </div>
299
+
300
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
301
+ <SectionHeader
302
+ title="Live demo"
303
+ subtitle="Real, clickable components — not mockups. Resize the Storybook viewport below 768px to see MobileHeader render instead of a squeezed desktop bar."
304
+ />
305
+ <Frame>
306
+ <ResponsiveHeaderDemo />
307
+ </Frame>
308
+ </section>
309
+
310
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
311
+ <SectionHeader
312
+ title="Anatomy"
313
+ subtitle="Brand badge + optional active plugin on the left; launcher, notifications, and avatar menu on the right — always rendered, never optional."
314
+ />
315
+ <ul style={{ margin: 0, paddingLeft: '1.25rem', color: 'var(--sv-color-text-muted)' }}>
316
+ <li>
317
+ <strong style={{ color: 'var(--sv-color-text-primary)' }}>Brand badge</strong> — derives
318
+ its initial from <code>instanceName</code>, links to <code>homeHref</code>.
319
+ </li>
320
+ <li>
321
+ <strong style={{ color: 'var(--sv-color-text-primary)' }}>Active plugin</strong> (
322
+ <code>plugin</code>, optional) — icon + name, links to <code>plugin.href</code>.
323
+ </li>
324
+ <li>
325
+ <strong style={{ color: 'var(--sv-color-text-primary)' }}>launcher</strong> — the real{' '}
326
+ <code>AppsLauncher</code>.
327
+ </li>
328
+ <li>
329
+ <strong style={{ color: 'var(--sv-color-text-primary)' }}>notifications</strong> — the
330
+ real <code>NotificationsPanel</code>.
331
+ </li>
332
+ <li>
333
+ <strong style={{ color: 'var(--sv-color-text-primary)' }}>avatarMenu</strong> — the real{' '}
334
+ <code>UserMenu</code> (<code>size=&quot;sm&quot;</code> in this context — see Sizing
335
+ below).
336
+ </li>
337
+ </ul>
338
+ <CodeBlock>{`<Header
339
+ instanceName="Sovereign"
340
+ homeHref="/launcher"
341
+ plugin={{ name: 'Notes', icon: <PluginIcon />, href: '/notes' }}
342
+ launcher={<AppsLauncher items={apps} />}
343
+ notifications={<NotificationsPanel items={items} unreadCount={n} />}
344
+ avatarMenu={<UserMenu name={user.name} email={user.email} items={menuItems} size="sm" />}
345
+ />`}</CodeBlock>
346
+ </section>
347
+
348
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
349
+ <SectionHeader title="Boundary — presentational only" />
350
+ <Body>
351
+ <code>Header</code> owns layout only — never launcher/notification/account interaction
352
+ logic, matching <code>MobileHeader</code>&apos;s own boundary (RFC 0088).{' '}
353
+ <code>launcher</code>/<code>notifications</code>/<code>avatarMenu</code> are supplied
354
+ fully built by the consumer. In practice that consumer is almost always the real{' '}
355
+ <code>AppsLauncher</code>/<code>NotificationsPanel</code>/<code>UserMenu</code> — see
356
+ their own Components pages — not a bespoke reimplementation.
357
+ </Body>
358
+ </section>
359
+
360
+ <section style={{ marginBottom: 'var(--sv-space-6)' }}>
361
+ <SectionHeader
362
+ title="Sizing"
363
+ subtitle="A compact top bar, deliberately smaller than the platform sidebar — this plugin owns its whole viewport, so it isn't bound to platform chrome dimensions."
364
+ />
365
+ <ul style={{ margin: 0, paddingLeft: '1.25rem', color: 'var(--sv-color-text-muted)' }}>
366
+ <li>Bar height: 49px (48px content + 1px border).</li>
367
+ <li>Brand badge / plugin icon: 28px.</li>
368
+ <li>Launcher / notifications trigger buttons: 28px, with a 20–24px icon inside.</li>
369
+ <li>
370
+ Avatar: 32px here (<code>UserMenu size=&quot;sm&quot;</code>) — smaller than the
371
+ platform sidebar&apos;s own 36px avatar (<code>UserMenu</code>&apos;s default), to suit
372
+ this compact top bar&apos;s own proportions.
373
+ </li>
374
+ </ul>
375
+ <Callout type="tip">
376
+ Want to try different <code>instanceName</code>/<code>homeHref</code>/<code>plugin</code>{' '}
377
+ values live? See the <strong>Playground</strong> story — its Controls panel is wired up
378
+ for exactly that.
379
+ </Callout>
380
+ </section>
381
+ </div>
382
+ );
383
+ }
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // Story
387
+ // ---------------------------------------------------------------------------
388
+
389
+ const meta = {
390
+ title: 'Components/Header',
391
+ component: Header,
392
+ parameters: {
393
+ // fullscreen, not padded — this is a full-bleed top bar; Storybook's own
394
+ // canvas gutter around a `padded` story reads as extra height in a
395
+ // screenshot comparison against the real, edge-to-edge running app. The
396
+ // Default doc page supplies its own centered/padded wrapper internally.
397
+ layout: 'fullscreen',
398
+ docs: {
399
+ description: {
400
+ component:
401
+ 'The web top-bar header for shell: minimal plugins — the counterpart to the platform sidebar for a plugin that owns its whole viewport. Brand badge and optional active plugin on the left; launcher (AppsLauncher), notifications (NotificationsPanel), and avatar menu (UserMenu) on the right. Below 768px, plugins render MobileHeader instead.',
402
+ },
403
+ },
404
+ },
405
+ argTypes: {
406
+ instanceName: { control: 'text' },
407
+ homeHref: { control: 'text' },
408
+ plugin: { control: 'object' },
409
+ launcher: { control: false },
410
+ notifications: { control: false },
411
+ avatarMenu: { control: false },
412
+ },
413
+ args: {
414
+ instanceName: 'Sovereign',
415
+ homeHref: '/launcher',
416
+ plugin: {
417
+ name: 'Notes',
418
+ icon: <HeaderPluginIcon name="file" />,
419
+ href: '/notes',
420
+ },
421
+ launcher: <Launcher />,
422
+ notifications: <Notifications />,
423
+ avatarMenu: <Account />,
424
+ },
425
+ } satisfies Meta<typeof Header>;
426
+
427
+ export default meta;
428
+ type StoryDetail = StoryObj<typeof meta>;
429
+
430
+ export const Story: StoryDetail = {
431
+ parameters: {
432
+ controls: { disable: true },
433
+ },
434
+ render: () => <HeaderDoc />,
435
+ };
436
+
437
+ /**
438
+ * A plain, bare `Header` — use the Controls panel below to try different
439
+ * `instanceName`/`homeHref`/`plugin` values live. `launcher`/`notifications`/
440
+ * `avatarMenu` stay fixed to the real shared components (they're ReactNode
441
+ * slots — not meaningfully editable as Controls). `plugin.icon` is omitted
442
+ * here (unlike the live demo above) so the object control stays plain,
443
+ * editable JSON rather than a React element it can't serialize.
444
+ */
445
+ export const Playground: StoryDetail = {
446
+ args: {
447
+ plugin: {
448
+ name: 'Notes',
449
+ href: '/notes',
450
+ },
451
+ },
452
+ };