@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,258 @@
1
+ .trigger {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ flex-shrink: 0;
7
+ width: 28px;
8
+ height: 28px;
9
+ padding: 0;
10
+ border: none;
11
+ border-radius: var(--sv-radius-md);
12
+ background: transparent;
13
+ color: var(--sv-color-text-primary);
14
+ cursor: pointer;
15
+ }
16
+
17
+ @media (hover: hover) {
18
+ .trigger:hover {
19
+ background: var(--sv-color-surface-sunken);
20
+ }
21
+ }
22
+
23
+ .badge {
24
+ position: absolute;
25
+ top: 2px;
26
+ right: 2px;
27
+ min-width: 14px;
28
+ height: 14px;
29
+ padding: 0 3px;
30
+ background: var(--sv-color-error-text);
31
+ color: white;
32
+ border-radius: var(--sv-radius-full);
33
+ font-size: 9px;
34
+ font-weight: 700;
35
+ line-height: 14px;
36
+ text-align: center;
37
+ pointer-events: none;
38
+ }
39
+
40
+ /* ── Header ─────────────────────────────────────────────────────────────── */
41
+
42
+ .header {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: space-between;
46
+ gap: var(--sv-space-2);
47
+ padding: var(--sv-space-3) var(--sv-space-3) var(--sv-space-3) var(--sv-space-4);
48
+ border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
49
+ }
50
+
51
+ .headerTitle {
52
+ font-size: var(--sv-font-size-sm);
53
+ font-weight: var(--sv-font-weight-semibold);
54
+ color: var(--sv-color-text-primary);
55
+ flex: 1;
56
+ }
57
+
58
+ .headerActions {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: var(--sv-space-2);
62
+ }
63
+
64
+ .actionBtn {
65
+ background: none;
66
+ border: none;
67
+ padding: 0;
68
+ cursor: pointer;
69
+ font-size: var(--sv-font-size-caption);
70
+ color: var(--sv-color-text-muted);
71
+ white-space: nowrap;
72
+ }
73
+
74
+ @media (hover: hover) {
75
+ .actionBtn:hover {
76
+ color: var(--sv-color-text-primary);
77
+ }
78
+ }
79
+
80
+ .closeBtn {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ width: 24px;
85
+ height: 24px;
86
+ padding: 0;
87
+ border: none;
88
+ border-radius: var(--sv-radius-sm);
89
+ background: none;
90
+ color: var(--sv-color-text-muted);
91
+ cursor: pointer;
92
+ }
93
+
94
+ @media (hover: hover) {
95
+ .closeBtn:hover {
96
+ color: var(--sv-color-text-primary);
97
+ background: var(--sv-color-surface-sunken);
98
+ }
99
+ }
100
+
101
+ /* ── List ───────────────────────────────────────────────────────────────── */
102
+
103
+ .list {
104
+ list-style: none;
105
+ margin: 0;
106
+ padding: 0;
107
+ max-height: 420px;
108
+ overflow-y: auto;
109
+ }
110
+
111
+ /* ── Empty state ────────────────────────────────────────────────────────── */
112
+
113
+ .empty {
114
+ display: flex;
115
+ flex-direction: column;
116
+ align-items: center;
117
+ gap: var(--sv-space-3);
118
+ padding: var(--sv-space-10) var(--sv-space-4);
119
+ }
120
+
121
+ .emptyIcon {
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ width: 44px;
126
+ height: 44px;
127
+ border-radius: var(--sv-radius-full);
128
+ background: var(--sv-color-surface-sunken);
129
+ color: var(--sv-color-text-muted);
130
+ }
131
+
132
+ .emptyText {
133
+ font-size: var(--sv-font-size-sm);
134
+ color: var(--sv-color-text-muted);
135
+ }
136
+
137
+ /* ── Notification item ─────────────────────────────────────────────────── */
138
+
139
+ .item {
140
+ display: flex;
141
+ align-items: flex-start;
142
+ gap: var(--sv-space-3);
143
+ padding: var(--sv-space-3);
144
+ border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
145
+ }
146
+
147
+ .item:last-child {
148
+ border-bottom: none;
149
+ }
150
+
151
+ .categoryIcon {
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ flex: none;
156
+ width: 32px;
157
+ height: 32px;
158
+ border-radius: var(--sv-radius-full);
159
+ margin-top: 1px;
160
+ background: var(--sv-color-surface-sunken);
161
+ color: var(--sv-color-text-muted);
162
+ }
163
+
164
+ .itemBody {
165
+ flex: 1 1 auto;
166
+ min-width: 0;
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 2px;
170
+ }
171
+
172
+ .itemTitle {
173
+ display: block;
174
+ width: 100%;
175
+ border: none;
176
+ padding: 0;
177
+ background: none;
178
+ font: inherit;
179
+ font-size: var(--sv-font-size-sm);
180
+ font-weight: var(--sv-font-weight-semibold);
181
+ color: var(--sv-color-text-primary);
182
+ text-decoration: none;
183
+ text-align: left;
184
+ line-height: 1.4;
185
+ cursor: pointer;
186
+ }
187
+
188
+ @media (hover: hover) {
189
+ a.itemTitle:hover,
190
+ button.itemTitle:hover {
191
+ text-decoration: underline;
192
+ }
193
+ }
194
+
195
+ .itemRead .itemTitle {
196
+ color: var(--sv-color-text-subtle);
197
+ cursor: default;
198
+ }
199
+
200
+ .itemTime {
201
+ font-size: var(--sv-font-size-caption);
202
+ color: var(--sv-color-text-muted);
203
+ }
204
+
205
+ .itemEnd {
206
+ display: flex;
207
+ align-items: center;
208
+ gap: var(--sv-space-2);
209
+ flex: none;
210
+ padding-top: 2px;
211
+ }
212
+
213
+ .unreadDot {
214
+ display: block;
215
+ width: 7px;
216
+ height: 7px;
217
+ border-radius: var(--sv-radius-full);
218
+ background: var(--sv-color-accent);
219
+ flex: none;
220
+ }
221
+
222
+ .dismissBtn {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ width: 20px;
227
+ height: 20px;
228
+ padding: 0;
229
+ border: none;
230
+ border-radius: var(--sv-radius-sm);
231
+ background: none;
232
+ color: var(--sv-color-text-muted);
233
+ cursor: pointer;
234
+ }
235
+
236
+ @media (hover: hover) {
237
+ .dismissBtn:hover {
238
+ color: var(--sv-color-text-primary);
239
+ background: var(--sv-color-surface-sunken);
240
+ }
241
+ }
242
+
243
+ .viewAll {
244
+ display: block;
245
+ text-align: center;
246
+ padding: var(--sv-space-2) var(--sv-space-3);
247
+ border-top: var(--sv-border-width-hairline) solid var(--sv-color-border);
248
+ font-size: var(--sv-font-size-caption);
249
+ color: var(--sv-color-text-muted);
250
+ text-decoration: none;
251
+ }
252
+
253
+ @media (hover: hover) {
254
+ .viewAll:hover {
255
+ color: var(--sv-color-text-primary);
256
+ background: var(--sv-color-surface-sunken);
257
+ }
258
+ }
@@ -0,0 +1,61 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { NotificationsPanel } from './NotificationsPanel';
3
+ import { Icon } from '../Icon/Icon';
4
+
5
+ const items = [
6
+ {
7
+ id: '1',
8
+ icon: <Icon name="layers" size="sm" aria-hidden />,
9
+ title: 'Added to a project',
10
+ timeLabel: '2d ago',
11
+ read: false,
12
+ onDismiss: () => {},
13
+ },
14
+ {
15
+ id: '2',
16
+ icon: <Icon name="user-round-plus" size="sm" aria-hidden />,
17
+ title: 'Jamie invited you to Notes',
18
+ timeLabel: '3d ago',
19
+ read: true,
20
+ href: '/notes',
21
+ onDismiss: () => {},
22
+ },
23
+ ];
24
+
25
+ const meta = {
26
+ title: 'Components/NotificationsPanel',
27
+ component: NotificationsPanel,
28
+ parameters: {
29
+ layout: 'centered',
30
+ docs: {
31
+ description: {
32
+ component:
33
+ "The bell trigger + dropdown, generalized from the runtime shell's own NotificationBell. Pure presentation — no fetch/SSE/mark-read logic; the consumer supplies already-resolved items and wires onOpen/onDismiss/onMarkAllRead/onClearAll.",
34
+ },
35
+ },
36
+ },
37
+ args: {
38
+ items,
39
+ unreadCount: 1,
40
+ onMarkAllRead: () => {},
41
+ onClearAll: () => {},
42
+ },
43
+ } satisfies Meta<typeof NotificationsPanel>;
44
+
45
+ export default meta;
46
+ type Story = StoryObj<typeof meta>;
47
+
48
+ export const Default: Story = {};
49
+
50
+ export const Empty: Story = {
51
+ args: {
52
+ items: [],
53
+ unreadCount: 0,
54
+ },
55
+ };
56
+
57
+ export const WithViewAll: Story = {
58
+ args: {
59
+ viewAllHref: '/inbox',
60
+ },
61
+ };
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+
3
+ import { useState, type ReactNode } from 'react';
4
+ import { Icon } from '../Icon/Icon';
5
+ import { Popover } from '../Popover/Popover';
6
+ import styles from './NotificationsPanel.module.css';
7
+
8
+ export interface NotificationsPanelItem {
9
+ id: string;
10
+ /** Category icon, already resolved (sized + colored) by the consumer —
11
+ * this component doesn't own category → color mapping. */
12
+ icon: ReactNode;
13
+ title: string;
14
+ /** Pre-formatted relative time (e.g. "2d ago") — formatting stays
15
+ * consumer-side so this component needs no Date/clock dependency. */
16
+ timeLabel: string;
17
+ read: boolean;
18
+ /** Renders the title as a link when set. */
19
+ href?: string;
20
+ /** Called when the title is clicked/tapped — typically "mark as read". */
21
+ onOpen?: () => void;
22
+ onDismiss: () => void;
23
+ }
24
+
25
+ export interface NotificationsPanelProps {
26
+ items: NotificationsPanelItem[];
27
+ unreadCount: number;
28
+ onMarkAllRead?: () => void;
29
+ onClearAll?: () => void;
30
+ emptyMessage?: string;
31
+ /** Trigger button's accessible label base — the unread count is appended
32
+ * automatically, matching the real NotificationBell. Defaults to
33
+ * "Notifications". */
34
+ 'aria-label'?: string;
35
+ align?: 'left' | 'right';
36
+ /** Optional footer link (e.g. to the consumer's own full notification
37
+ * list/detail view — RFC 0048) — mirrors the real `NotificationBell`'s
38
+ * "View all" link. Omit to render no footer, matching this component's
39
+ * behavior before this prop existed. */
40
+ viewAllHref?: string;
41
+ }
42
+
43
+ function NotificationRow({ item }: { item: NotificationsPanelItem }) {
44
+ const titleContent = item.href ? (
45
+ <a href={item.href} className={styles.itemTitle} onClick={item.onOpen}>
46
+ {item.title}
47
+ </a>
48
+ ) : !item.read ? (
49
+ <button
50
+ type="button"
51
+ className={styles.itemTitle}
52
+ aria-label={`Mark as read: ${item.title}`}
53
+ onClick={item.onOpen}
54
+ >
55
+ {item.title}
56
+ </button>
57
+ ) : (
58
+ <span className={styles.itemTitle}>{item.title}</span>
59
+ );
60
+
61
+ return (
62
+ <li className={[styles.item, item.read ? styles.itemRead : ''].filter(Boolean).join(' ')}>
63
+ <div className={styles.categoryIcon}>{item.icon}</div>
64
+ <div className={styles.itemBody}>
65
+ {titleContent}
66
+ <span className={styles.itemTime}>{item.timeLabel}</span>
67
+ </div>
68
+ <div className={styles.itemEnd}>
69
+ {!item.read && <span className={styles.unreadDot} aria-label="Unread" />}
70
+ <button
71
+ type="button"
72
+ className={styles.dismissBtn}
73
+ aria-label={`Dismiss: ${item.title}`}
74
+ onClick={item.onDismiss}
75
+ >
76
+ <Icon name="x" size="sm" aria-hidden />
77
+ </button>
78
+ </div>
79
+ </li>
80
+ );
81
+ }
82
+
83
+ /**
84
+ * NotificationsPanel — the bell trigger + dropdown, generalized from the
85
+ * runtime shell's own `NotificationBell.tsx` visual structure and copy
86
+ * (title / Mark all read / Clear all / close, then a list of category-icon +
87
+ * title + time + unread-dot + dismiss rows, or an empty state).
88
+ *
89
+ * Deliberately **presentational only** — no fetch/SSE/polling, no
90
+ * category → color mapping, no mark-read/dismiss networking. The real
91
+ * `NotificationBell` owns real platform data (`/api/account/notifications`,
92
+ * a shared cross-instance store); this component can't and shouldn't know
93
+ * about that (SDK boundary). The consumer supplies `items` already resolved
94
+ * and wires `onOpen`/`onDismiss`/`onMarkAllRead`/`onClearAll` to whatever
95
+ * its own data layer needs, against a shared panel instead of a
96
+ * `shell: minimal` plugin hand-rebuilding one locally. Built on the shared
97
+ * `Popover` primitive (self-contained open state) rather than the
98
+ * original's hand-rolled outside-click/Escape listener.
99
+ */
100
+ export function NotificationsPanel({
101
+ items,
102
+ unreadCount,
103
+ onMarkAllRead,
104
+ onClearAll,
105
+ emptyMessage = 'No notifications.',
106
+ 'aria-label': ariaLabel = 'Notifications',
107
+ align = 'right',
108
+ viewAllHref,
109
+ }: NotificationsPanelProps) {
110
+ const [open, setOpen] = useState(false);
111
+
112
+ return (
113
+ <Popover
114
+ align={align}
115
+ width={340}
116
+ aria-label={ariaLabel}
117
+ open={open}
118
+ onClose={() => setOpen(false)}
119
+ trigger={
120
+ <button
121
+ type="button"
122
+ aria-label={unreadCount > 0 ? `${ariaLabel} (${unreadCount} unread)` : ariaLabel}
123
+ aria-haspopup="true"
124
+ aria-expanded={open}
125
+ onClick={() => setOpen((v) => !v)}
126
+ className={styles.trigger}
127
+ >
128
+ {/* size="lg" (24px), not "md" (20px) — matches the real
129
+ NotificationBell's own inline SVG, which is literally
130
+ width="24" height="24", not the DS Icon scale's "md" step. */}
131
+ <Icon name="bell" size="lg" aria-hidden />
132
+ {unreadCount > 0 && (
133
+ <span className={styles.badge} aria-hidden="true">
134
+ {unreadCount > 99 ? '99+' : unreadCount}
135
+ </span>
136
+ )}
137
+ </button>
138
+ }
139
+ >
140
+ <div className={styles.header}>
141
+ <span className={styles.headerTitle}>{ariaLabel}</span>
142
+ <div className={styles.headerActions}>
143
+ {items.length > 0 && (
144
+ <>
145
+ {unreadCount > 0 && onMarkAllRead && (
146
+ <button type="button" className={styles.actionBtn} onClick={onMarkAllRead}>
147
+ Mark all read
148
+ </button>
149
+ )}
150
+ {onClearAll && (
151
+ <button type="button" className={styles.actionBtn} onClick={onClearAll}>
152
+ Clear all
153
+ </button>
154
+ )}
155
+ </>
156
+ )}
157
+ <button
158
+ type="button"
159
+ className={styles.closeBtn}
160
+ aria-label={`Close ${ariaLabel.toLowerCase()}`}
161
+ onClick={() => setOpen(false)}
162
+ >
163
+ <Icon name="x" size="sm" aria-hidden />
164
+ </button>
165
+ </div>
166
+ </div>
167
+
168
+ <ul className={styles.list} aria-label={`${ariaLabel} list`}>
169
+ {items.length === 0 ? (
170
+ <li className={styles.empty}>
171
+ <span className={styles.emptyIcon} aria-hidden="true">
172
+ <Icon name="bell" size="lg" aria-hidden />
173
+ </span>
174
+ <span className={styles.emptyText}>{emptyMessage}</span>
175
+ </li>
176
+ ) : (
177
+ items.map((item) => <NotificationRow key={item.id} item={item} />)
178
+ )}
179
+ </ul>
180
+ {viewAllHref && (
181
+ <a href={viewAllHref} className={styles.viewAll} onClick={() => setOpen(false)}>
182
+ View all
183
+ </a>
184
+ )}
185
+ </Popover>
186
+ );
187
+ }
@@ -0,0 +1,78 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
4
+ import { NotificationsPanel } from '../NotificationsPanel';
5
+
6
+ afterEach(() => {
7
+ cleanup();
8
+ });
9
+
10
+ const items = [
11
+ {
12
+ id: '1',
13
+ icon: <span>icon</span>,
14
+ title: 'Added to a project',
15
+ timeLabel: '2d ago',
16
+ read: false,
17
+ onOpen: vi.fn(),
18
+ onDismiss: vi.fn(),
19
+ },
20
+ ];
21
+
22
+ describe('NotificationsPanel', () => {
23
+ it('renders a closed trigger with an unread badge', () => {
24
+ render(<NotificationsPanel items={items} unreadCount={1} />);
25
+ expect(screen.getByRole('button', { name: /1 unread/ })).toBeTruthy();
26
+ expect(screen.queryByText('Added to a project')).toBeNull();
27
+ });
28
+
29
+ it('renders no badge when there are no unread notifications', () => {
30
+ render(<NotificationsPanel items={[]} unreadCount={0} />);
31
+ expect(screen.getByRole('button', { name: 'Notifications' })).toBeTruthy();
32
+ });
33
+
34
+ it('opens the panel on trigger click, showing items', () => {
35
+ render(<NotificationsPanel items={items} unreadCount={1} />);
36
+ fireEvent.click(screen.getByRole('button', { name: /unread/ }));
37
+ expect(screen.getByText('Added to a project')).toBeTruthy();
38
+ expect(screen.getByText('2d ago')).toBeTruthy();
39
+ });
40
+
41
+ it('shows an empty state when there are no items', () => {
42
+ render(<NotificationsPanel items={[]} unreadCount={0} />);
43
+ fireEvent.click(screen.getByRole('button', { name: 'Notifications' }));
44
+ expect(screen.getByText('No notifications.')).toBeTruthy();
45
+ });
46
+
47
+ it('calls onOpen when an unread title is clicked', () => {
48
+ render(<NotificationsPanel items={items} unreadCount={1} />);
49
+ fireEvent.click(screen.getByRole('button', { name: /unread/ }));
50
+ fireEvent.click(screen.getByRole('button', { name: 'Mark as read: Added to a project' }));
51
+ expect(items[0]?.onOpen).toHaveBeenCalledOnce();
52
+ });
53
+
54
+ it('calls onDismiss when the dismiss button is clicked', () => {
55
+ render(<NotificationsPanel items={items} unreadCount={1} />);
56
+ fireEvent.click(screen.getByRole('button', { name: /unread/ }));
57
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss: Added to a project' }));
58
+ expect(items[0]?.onDismiss).toHaveBeenCalledOnce();
59
+ });
60
+
61
+ it('calls onMarkAllRead and onClearAll from the header actions', () => {
62
+ const onMarkAllRead = vi.fn();
63
+ const onClearAll = vi.fn();
64
+ render(
65
+ <NotificationsPanel
66
+ items={items}
67
+ unreadCount={1}
68
+ onMarkAllRead={onMarkAllRead}
69
+ onClearAll={onClearAll}
70
+ />,
71
+ );
72
+ fireEvent.click(screen.getByRole('button', { name: /unread/ }));
73
+ fireEvent.click(screen.getByRole('button', { name: 'Mark all read' }));
74
+ fireEvent.click(screen.getByRole('button', { name: 'Clear all' }));
75
+ expect(onMarkAllRead).toHaveBeenCalledOnce();
76
+ expect(onClearAll).toHaveBeenCalledOnce();
77
+ });
78
+ });
@@ -6,7 +6,7 @@
6
6
  .header {
7
7
  flex-shrink: 0;
8
8
  background: var(--sv-color-surface);
9
- border-bottom: 1px solid var(--sv-color-border);
9
+ border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
10
10
  }
11
11
 
12
12
  .row {
@@ -5,8 +5,12 @@ import styles from './OverlayHeader.module.css';
5
5
  export interface OverlayHeaderProps {
6
6
  /** Overlay title, shown between the optional back button and the trailing
7
7
  * slots. Optional — some consumers (e.g. `Dialog`'s mobile bar) need the
8
- * row's close button present even when no title applies. */
9
- title?: string;
8
+ * row's close button present even when no title applies. Accepts any
9
+ * `ReactNode`, not just a string — e.g. `Dialog`'s `header` prop passes
10
+ * arbitrary content through here (an icon next to text, etc.); the
11
+ * row's title slot still assumes single-line content sized like text
12
+ * (see the component's own doc comment for the line this doesn't cross). */
13
+ title?: ReactNode;
10
14
  /** Always renders a close button; called on click. */
11
15
  onClose: () => void;
12
16
  /** When provided, renders a back-chevron button before the title (for
@@ -18,6 +22,15 @@ export interface OverlayHeaderProps {
18
22
  /** A second row below the title row — e.g. a tab strip. */
19
23
  secondRow?: ReactNode;
20
24
  className?: string;
25
+ /** Override the title row's own className (e.g. tighter padding to match
26
+ * a consumer's own compact header elsewhere in the same surface) — the
27
+ * row keeps its standard title/back/action/close layout, just restyled.
28
+ * For a genuinely different shape, don't use `OverlayHeader` at all (see
29
+ * this component's own doc comment). */
30
+ rowClassName?: string;
31
+ /** Override the title text's own className (e.g. a smaller font size to
32
+ * match a consumer's own compact header elsewhere in the same surface). */
33
+ titleClassName?: string;
21
34
  }
22
35
 
23
36
  /**
@@ -41,16 +54,18 @@ export function OverlayHeader({
41
54
  action,
42
55
  secondRow,
43
56
  className,
57
+ rowClassName,
58
+ titleClassName,
44
59
  }: OverlayHeaderProps) {
45
60
  return (
46
61
  <div className={[styles.header, className].filter(Boolean).join(' ')}>
47
- <div className={styles.row}>
62
+ <div className={[styles.row, rowClassName].filter(Boolean).join(' ')}>
48
63
  {onBack && (
49
64
  <button type="button" className={styles.backButton} aria-label="Back" onClick={onBack}>
50
65
  <Icon name="chevron-left" size="md" aria-hidden />
51
66
  </button>
52
67
  )}
53
- <span className={styles.title}>{title}</span>
68
+ <span className={[styles.title, titleClassName].filter(Boolean).join(' ')}>{title}</span>
54
69
  {action && <div className={styles.action}>{action}</div>}
55
70
  <button type="button" className={styles.closeButton} aria-label="Close" onClick={onClose}>
56
71
  <Icon name="x" size="sm" aria-hidden />
@@ -48,4 +48,22 @@ describe('OverlayHeader', () => {
48
48
  render(<OverlayHeader title="Account" onClose={() => {}} secondRow={<nav>Tab strip</nav>} />);
49
49
  expect(screen.getByText('Tab strip')).toBeTruthy();
50
50
  });
51
+
52
+ it('accepts arbitrary ReactNode content for title, not just a string', () => {
53
+ // Locks in the type widening (title?: string -> title?: ReactNode) that
54
+ // Dialog's `header` prop relies on (Dialog.tsx passes header through
55
+ // here as `title`) — a plain string title must keep working unchanged.
56
+ render(
57
+ <OverlayHeader
58
+ title={
59
+ <>
60
+ <strong>Card</strong> detail
61
+ </>
62
+ }
63
+ onClose={() => {}}
64
+ />,
65
+ );
66
+ expect(screen.getByText('Card').tagName).toBe('STRONG');
67
+ expect(screen.getByText('detail')).toBeTruthy();
68
+ });
51
69
  });