@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,102 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import styles from './Header.module.css';
3
+
4
+ export interface HeaderPlugin {
5
+ /** Plugin display name, rendered next to its icon. */
6
+ name: string;
7
+ /** Plugin icon — already sized (28px) and built by the consumer (typically
8
+ * an `<img src="/plugin-icons/<id>.svg">`), matching `MobileHeader`'s own
9
+ * router-agnostic convention for icon-shaped slots. */
10
+ icon?: ReactNode;
11
+ /** Where the plugin name/icon links to — typically the plugin's own
12
+ * `routePrefix`. Omit to render plain (non-interactive) text instead of a
13
+ * link — e.g. a plugin's own home page linking to itself would be a no-op. */
14
+ href?: string;
15
+ }
16
+
17
+ export interface HeaderProps extends HTMLAttributes<HTMLElement> {
18
+ /** Instance name — derives the brand badge's initial (its first character,
19
+ * uppercased) and the badge's accessible label. Does not render as text
20
+ * anywhere in this component (unlike `MobileHeader`'s optional `title`). */
21
+ instanceName: string;
22
+ /** Optional instance logo image; falls back to `instanceName`'s initial
23
+ * (matching the runtime shell's own sidebar/mobile-header fallback). */
24
+ instanceLogoUrl?: string;
25
+ /** Where the brand badge links — typically the Launcher plugin's route. */
26
+ homeHref: string;
27
+ /** The active plugin, rendered next to the brand badge. Omit for a page
28
+ * with no single active plugin (e.g. the Launcher itself). */
29
+ plugin?: HeaderPlugin;
30
+ /** Apps launcher trigger — already wired by the consumer (icon button plus
31
+ * whatever popover/drawer it opens). Always rendered, matching
32
+ * `MobileHeader`'s bell/avatarMenu boundary: this component owns layout
33
+ * only, never launcher/notification/account interaction logic. */
34
+ launcher: ReactNode;
35
+ /** Notification bell trigger — always rendered; not overridable. Same
36
+ * "just an icon slot" boundary as `MobileHeader`'s own `bell` prop. */
37
+ notifications: ReactNode;
38
+ /** Avatar / account menu trigger — always rendered; not overridable. */
39
+ avatarMenu: ReactNode;
40
+ }
41
+
42
+ /**
43
+ * The web top-bar header: brand badge (+ optional active plugin) on the
44
+ * left, launcher · notifications · avatar menu on the right. This is the
45
+ * `shell: minimal` counterpart to the runtime shell's own sidebar — a plugin
46
+ * that owns its whole viewport (no platform sidebar) renders this instead,
47
+ * the same way it renders `MobileHeader` in place of the platform's mobile
48
+ * chrome. Extracted into a shared component so a `shell: minimal` plugin
49
+ * doesn't have to hand-roll its own top bar from scratch.
50
+ *
51
+ * Presentational only — no data fetching, no account/notification/launcher
52
+ * logic of its own. `launcher`/`notifications`/`avatarMenu` are supplied
53
+ * fully built, exactly like `MobileHeader`'s `bell`/`avatarMenu`: this
54
+ * component stays router-agnostic and interaction-agnostic, and never
55
+ * duplicates a real `AccountMenu`/notification-panel/apps-switcher
56
+ * implementation.
57
+ */
58
+ export function Header({
59
+ instanceName,
60
+ instanceLogoUrl,
61
+ homeHref,
62
+ plugin,
63
+ launcher,
64
+ notifications,
65
+ avatarMenu,
66
+ className,
67
+ ...rest
68
+ }: HeaderProps) {
69
+ const brandInitial = instanceName.charAt(0).toUpperCase() || 'S';
70
+ const cls = [styles.header, className].filter(Boolean).join(' ');
71
+
72
+ return (
73
+ <header className={cls} {...rest}>
74
+ <div className={styles.left}>
75
+ <a href={homeHref} className={styles.brandBadge} aria-label={`${instanceName} home`}>
76
+ {instanceLogoUrl ? (
77
+ <img src={instanceLogoUrl} alt="" className={styles.brandLogoImg} />
78
+ ) : (
79
+ brandInitial
80
+ )}
81
+ </a>
82
+ {plugin &&
83
+ (plugin.href ? (
84
+ <a href={plugin.href} className={styles.plugin}>
85
+ {plugin.icon}
86
+ <span className={styles.pluginName}>{plugin.name}</span>
87
+ </a>
88
+ ) : (
89
+ <span className={styles.plugin}>
90
+ {plugin.icon}
91
+ <span className={styles.pluginName}>{plugin.name}</span>
92
+ </span>
93
+ ))}
94
+ </div>
95
+ <div className={styles.right}>
96
+ {launcher}
97
+ {notifications}
98
+ {avatarMenu}
99
+ </div>
100
+ </header>
101
+ );
102
+ }
@@ -0,0 +1,104 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import { cleanup, render, screen } from '@testing-library/react';
4
+ import { Header } from '../Header';
5
+
6
+ afterEach(() => {
7
+ cleanup();
8
+ });
9
+
10
+ describe('Header', () => {
11
+ it('always renders the brand badge, launcher, notifications, and avatar menu slots', () => {
12
+ render(
13
+ <Header
14
+ instanceName="Sovereign"
15
+ homeHref="/launcher"
16
+ launcher={<span data-testid="launcher">launcher</span>}
17
+ notifications={<span data-testid="bell">bell</span>}
18
+ avatarMenu={<span data-testid="avatar">avatar</span>}
19
+ />,
20
+ );
21
+
22
+ expect(screen.getByText('S')).toBeDefined();
23
+ expect(screen.getByTestId('launcher')).toBeDefined();
24
+ expect(screen.getByTestId('bell')).toBeDefined();
25
+ expect(screen.getByTestId('avatar')).toBeDefined();
26
+ });
27
+
28
+ it('derives the brand badge initial from instanceName, uppercased', () => {
29
+ render(
30
+ <Header
31
+ instanceName="acme"
32
+ homeHref="/launcher"
33
+ launcher={<span />}
34
+ notifications={<span />}
35
+ avatarMenu={<span />}
36
+ />,
37
+ );
38
+
39
+ expect(screen.getByText('A')).toBeDefined();
40
+ });
41
+
42
+ it('links the brand badge to homeHref', () => {
43
+ render(
44
+ <Header
45
+ instanceName="Sovereign"
46
+ homeHref="/launcher"
47
+ launcher={<span />}
48
+ notifications={<span />}
49
+ avatarMenu={<span />}
50
+ />,
51
+ );
52
+
53
+ expect(screen.getByRole('link', { name: 'Sovereign home' })).toHaveProperty(
54
+ 'href',
55
+ expect.stringContaining('/launcher'),
56
+ );
57
+ });
58
+
59
+ it('renders no plugin by default', () => {
60
+ render(
61
+ <Header
62
+ instanceName="Sovereign"
63
+ homeHref="/launcher"
64
+ launcher={<span />}
65
+ notifications={<span />}
66
+ avatarMenu={<span />}
67
+ />,
68
+ );
69
+
70
+ expect(screen.queryByText('Notes')).toBeNull();
71
+ });
72
+
73
+ it('renders the active plugin as a link when href is provided', () => {
74
+ render(
75
+ <Header
76
+ instanceName="Sovereign"
77
+ homeHref="/launcher"
78
+ plugin={{ name: 'Notes', icon: <span>icon</span>, href: '/notes' }}
79
+ launcher={<span />}
80
+ notifications={<span />}
81
+ avatarMenu={<span />}
82
+ />,
83
+ );
84
+
85
+ const link = screen.getByRole('link', { name: /Notes/ });
86
+ expect(link).toHaveProperty('href', expect.stringContaining('/notes'));
87
+ });
88
+
89
+ it('renders the active plugin as plain text when href is omitted', () => {
90
+ render(
91
+ <Header
92
+ instanceName="Sovereign"
93
+ homeHref="/launcher"
94
+ plugin={{ name: 'Notes' }}
95
+ launcher={<span />}
96
+ notifications={<span />}
97
+ avatarMenu={<span />}
98
+ />,
99
+ );
100
+
101
+ expect(screen.getByText('Notes')).toBeDefined();
102
+ expect(screen.queryByRole('link', { name: /Notes/ })).toBeNull();
103
+ });
104
+ });
@@ -0,0 +1,21 @@
1
+ .shell {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ }
6
+
7
+ .header {
8
+ flex-shrink: 0;
9
+ overflow: hidden;
10
+ }
11
+
12
+ .main {
13
+ flex: 1;
14
+ min-height: 0;
15
+ overflow-y: auto;
16
+ }
17
+
18
+ .footer {
19
+ flex-shrink: 0;
20
+ overflow: hidden;
21
+ }
@@ -0,0 +1,111 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { HeaderFooterLayout } from './HeaderFooterLayout';
3
+
4
+ function Bar({ label }: { label: string }) {
5
+ return (
6
+ <div
7
+ style={{
8
+ display: 'flex',
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ height: '100%',
12
+ fontFamily: 'var(--sv-font-family)',
13
+ fontSize: 'var(--sv-font-size-sm)',
14
+ fontWeight: 600,
15
+ color: 'var(--sv-color-text-primary)',
16
+ background: 'var(--sv-color-surface-raised)',
17
+ borderBlock: '1px solid var(--sv-color-border)',
18
+ }}
19
+ >
20
+ {label}
21
+ </div>
22
+ );
23
+ }
24
+
25
+ function ScrollableMain() {
26
+ return (
27
+ <div style={{ padding: 'var(--sv-space-4)', fontFamily: 'var(--sv-font-family)' }}>
28
+ {Array.from({ length: 30 }, (_, index) => (
29
+ <p key={index} style={{ color: 'var(--sv-color-text-primary)' }}>
30
+ Row {index + 1} — the main region scrolls independently; header and footer stay put.
31
+ </p>
32
+ ))}
33
+ </div>
34
+ );
35
+ }
36
+
37
+ const meta = {
38
+ title: 'Components/HeaderFooterLayout',
39
+ component: HeaderFooterLayout,
40
+ parameters: {
41
+ layout: 'fullscreen',
42
+ docs: {
43
+ description: {
44
+ component:
45
+ 'Header + main + optional footer, both fixed-height and independently optional, with main always claiming the remaining height. The vertical counterpart to ThreeColumnLayout — a plain flex column, no color opinions, no responsive behavior of its own.',
46
+ },
47
+ },
48
+ },
49
+ args: {
50
+ children: <ScrollableMain />,
51
+ },
52
+ } satisfies Meta<typeof HeaderFooterLayout>;
53
+
54
+ export default meta;
55
+ type Story = StoryObj<typeof meta>;
56
+
57
+ /** The canonical usage — header + main + footer, all three present. */
58
+ export const Default: Story = {
59
+ render: () => (
60
+ <div style={{ height: '480px' }}>
61
+ <HeaderFooterLayout header={<Bar label="Header" />} footer={<Bar label="Footer" />}>
62
+ <ScrollableMain />
63
+ </HeaderFooterLayout>
64
+ </div>
65
+ ),
66
+ };
67
+
68
+ export const HeaderOnly: Story = {
69
+ render: () => (
70
+ <div style={{ height: '480px' }}>
71
+ <HeaderFooterLayout header={<Bar label="Header" />}>
72
+ <ScrollableMain />
73
+ </HeaderFooterLayout>
74
+ </div>
75
+ ),
76
+ };
77
+
78
+ export const FooterOnly: Story = {
79
+ render: () => (
80
+ <div style={{ height: '480px' }}>
81
+ <HeaderFooterLayout footer={<Bar label="Footer" />}>
82
+ <ScrollableMain />
83
+ </HeaderFooterLayout>
84
+ </div>
85
+ ),
86
+ };
87
+
88
+ export const MainOnly: Story = {
89
+ render: () => (
90
+ <div style={{ height: '480px' }}>
91
+ <HeaderFooterLayout>
92
+ <ScrollableMain />
93
+ </HeaderFooterLayout>
94
+ </div>
95
+ ),
96
+ };
97
+
98
+ export const CustomHeights: Story = {
99
+ render: () => (
100
+ <div style={{ height: '480px' }}>
101
+ <HeaderFooterLayout
102
+ header={<Bar label="Header (100px)" />}
103
+ footer={<Bar label="Footer (48px)" />}
104
+ headerHeight={100}
105
+ footerHeight={48}
106
+ >
107
+ <ScrollableMain />
108
+ </HeaderFooterLayout>
109
+ </div>
110
+ ),
111
+ };
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import styles from './HeaderFooterLayout.module.css';
5
+
6
+ export interface HeaderFooterLayoutProps {
7
+ /** Main content — required, always fills whatever height header/footer don't claim. */
8
+ children: ReactNode;
9
+ header?: ReactNode;
10
+ footer?: ReactNode;
11
+ /** px. Fixed height of the header, when present. Default 60 (matches
12
+ * MobileHeader/--sv-shell-header-height's own default). */
13
+ headerHeight?: number;
14
+ /** px. Fixed height of the footer, when present. Default 60 (matches
15
+ * MobileFooter/--sv-shell-footer-height's own default). */
16
+ footerHeight?: number;
17
+ className?: string;
18
+ }
19
+
20
+ /**
21
+ * HeaderFooterLayout — header + main + optional footer, both fixed-height
22
+ * and independently optional, with main always claiming the remaining
23
+ * height. The vertical counterpart to ThreeColumnLayout: same purely
24
+ * structural approach (no color/background opinions, no awareness of what's
25
+ * inside each slot, no responsive behavior of its own — a plain flex
26
+ * column), just rotated. "Footer always at the bottom" falls out of flex
27
+ * layout alone; there is no position: fixed or safe-area handling here, so
28
+ * a caller composing this with the platform shell's own chrome still owns
29
+ * that separately.
30
+ */
31
+ export function HeaderFooterLayout({
32
+ children,
33
+ header,
34
+ footer,
35
+ headerHeight = 60,
36
+ footerHeight = 60,
37
+ className,
38
+ }: HeaderFooterLayoutProps) {
39
+ return (
40
+ <div className={[styles.shell, className].filter(Boolean).join(' ')}>
41
+ {header && (
42
+ <div className={styles.header} style={{ height: headerHeight }}>
43
+ {header}
44
+ </div>
45
+ )}
46
+ <div className={styles.main}>{children}</div>
47
+ {footer && (
48
+ <div className={styles.footer} style={{ height: footerHeight }}>
49
+ {footer}
50
+ </div>
51
+ )}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,58 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import { cleanup, render, screen } from '@testing-library/react';
4
+ import { HeaderFooterLayout } from '../HeaderFooterLayout';
5
+
6
+ afterEach(cleanup);
7
+
8
+ describe('HeaderFooterLayout', () => {
9
+ it('renders main content with no header/footer by default', () => {
10
+ render(<HeaderFooterLayout>Main content</HeaderFooterLayout>);
11
+
12
+ expect(screen.getByText('Main content')).toBeDefined();
13
+ expect(screen.queryByText('Header')).toBeNull();
14
+ expect(screen.queryByText('Footer')).toBeNull();
15
+ });
16
+
17
+ it('renders header and footer when given', () => {
18
+ render(
19
+ <HeaderFooterLayout header={<span>Header</span>} footer={<span>Footer</span>}>
20
+ Main content
21
+ </HeaderFooterLayout>,
22
+ );
23
+
24
+ expect(screen.getByText('Header')).toBeDefined();
25
+ expect(screen.getByText('Main content')).toBeDefined();
26
+ expect(screen.getByText('Footer')).toBeDefined();
27
+ });
28
+
29
+ it('renders header without a footer', () => {
30
+ render(<HeaderFooterLayout header={<span>Header</span>}>Main content</HeaderFooterLayout>);
31
+
32
+ expect(screen.getByText('Header')).toBeDefined();
33
+ expect(screen.queryByText('Footer')).toBeNull();
34
+ });
35
+
36
+ it('renders footer without a header', () => {
37
+ render(<HeaderFooterLayout footer={<span>Footer</span>}>Main content</HeaderFooterLayout>);
38
+
39
+ expect(screen.queryByText('Header')).toBeNull();
40
+ expect(screen.getByText('Footer')).toBeDefined();
41
+ });
42
+
43
+ it('applies headerHeight and footerHeight as inline heights', () => {
44
+ render(
45
+ <HeaderFooterLayout
46
+ header={<span>Header</span>}
47
+ footer={<span>Footer</span>}
48
+ headerHeight={48}
49
+ footerHeight={72}
50
+ >
51
+ Main content
52
+ </HeaderFooterLayout>,
53
+ );
54
+
55
+ expect(screen.getByText('Header').parentElement?.style.height).toBe('48px');
56
+ expect(screen.getByText('Footer').parentElement?.style.height).toBe('72px');
57
+ });
58
+ });
@@ -0,0 +1,23 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function ArrowLeftRightIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M8 3 4 7l4 4" />
18
+ <path d="M4 7h16" />
19
+ <path d="m16 21 4-4-4-4" />
20
+ <path d="M20 17H4" />
21
+ </svg>
22
+ );
23
+ }
@@ -0,0 +1,20 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function BoldIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" />
18
+ </svg>
19
+ );
20
+ }
@@ -14,10 +14,10 @@ export function CalendarIcon(props: SVGProps<SVGSVGElement>) {
14
14
  strokeLinejoin="round"
15
15
  {...props}
16
16
  >
17
- <path d="M8 2v4" />
18
- <path d="M16 2v4" />
19
- <rect width="18" height="18" x="3" y="4" rx="2" />
20
- <path d="M3 10h18" />
17
+ <path d="M8 2v3" />
18
+ <path d="M16 2v3" />
19
+ <rect x="3" y="3" width="18" height="18" rx="2" />
20
+ <path d="M3 9h18" />
21
21
  </svg>
22
22
  );
23
23
  }
@@ -14,9 +14,9 @@ export function CarrotIcon(props: SVGProps<SVGSVGElement>) {
14
14
  strokeLinejoin="round"
15
15
  {...props}
16
16
  >
17
- <path d="M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46" />
18
- <path d="M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z" />
19
- <path d="M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z" />
17
+ <path d="M15 16a1 1 0 0 0-7-7q-4 4-5.987 12.385a.5.5 0 0 0 .602.602Q11 20 15 16l-3-3" />
18
+ <path d="M15 9q4 4 7 0-3-4-7 0 4-4 0-7-4 3 0 7" />
19
+ <path d="m8 15-2.58-2.58" />
20
20
  </svg>
21
21
  );
22
22
  }
@@ -0,0 +1,21 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function CircleChevronLeftIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <circle cx="12" cy="12" r="10" />
18
+ <path d="m14 16-4-4 4-4" />
19
+ </svg>
20
+ );
21
+ }
@@ -0,0 +1,21 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function CodeIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="m16 18 6-6-6-6" />
18
+ <path d="m8 6-6 6 6 6" />
19
+ </svg>
20
+ );
21
+ }
@@ -0,0 +1,21 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function CopyIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
18
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
19
+ </svg>
20
+ );
21
+ }
@@ -0,0 +1,21 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function CreditCardIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <rect width="20" height="14" x="2" y="5" rx="2" />
18
+ <line x1="2" x2="22" y1="10" y2="10" />
19
+ </svg>
20
+ );
21
+ }
@@ -0,0 +1,22 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function DatabaseIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
18
+ <path d="M3 5V19A9 3 0 0 0 21 19V5" />
19
+ <path d="M3 12A9 3 0 0 0 21 12" />
20
+ </svg>
21
+ );
22
+ }
@@ -0,0 +1,22 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function DownloadIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M12 15V3" />
18
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
19
+ <path d="m7 10 5 5 5-5" />
20
+ </svg>
21
+ );
22
+ }