@columenlabs/lumen-ui 0.0.0-stage → 0.2.0

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 (297) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +12 -0
  3. package/README.md +4 -2
  4. package/dist/add-column.d.ts +23 -0
  5. package/dist/add-column.js +62 -0
  6. package/dist/api-fetch.d.ts +9 -0
  7. package/dist/api-fetch.js +60 -0
  8. package/dist/app-switcher.d.ts +21 -0
  9. package/dist/app-switcher.js +12 -0
  10. package/dist/app-views.d.ts +18 -0
  11. package/dist/app-views.js +41 -0
  12. package/dist/assistant-context.d.ts +72 -0
  13. package/dist/assistant-context.js +30 -0
  14. package/dist/assistant-panel.d.ts +58 -0
  15. package/dist/assistant-panel.js +244 -0
  16. package/dist/assistant-screen.d.ts +124 -0
  17. package/dist/assistant-screen.js +252 -0
  18. package/dist/card-switch.d.ts +16 -0
  19. package/dist/card-switch.js +16 -0
  20. package/dist/color-dot.d.ts +14 -0
  21. package/dist/color-dot.js +19 -0
  22. package/dist/confirm.d.ts +18 -0
  23. package/dist/confirm.js +5 -0
  24. package/dist/connections.d.ts +45 -0
  25. package/dist/connections.js +275 -0
  26. package/dist/coverage-calendar-map.d.ts +27 -0
  27. package/dist/coverage-calendar-map.js +84 -0
  28. package/dist/coverage-cells.d.ts +55 -0
  29. package/dist/coverage-cells.js +85 -0
  30. package/dist/coverage-map.d.ts +112 -0
  31. package/dist/coverage-map.js +428 -0
  32. package/dist/details-table.d.ts +20 -0
  33. package/dist/details-table.js +13 -0
  34. package/dist/drawer-section.d.ts +27 -0
  35. package/dist/drawer-section.js +23 -0
  36. package/dist/floating-head.d.ts +2 -0
  37. package/dist/floating-head.js +57 -0
  38. package/dist/fold.d.ts +16 -0
  39. package/dist/fold.js +70 -0
  40. package/dist/form-issues.d.ts +32 -0
  41. package/dist/form-issues.js +60 -0
  42. package/dist/formula-input.d.ts +17 -0
  43. package/dist/formula-input.js +104 -0
  44. package/dist/frame-script.d.ts +7 -0
  45. package/dist/frame-script.js +103 -0
  46. package/dist/freshness.d.ts +24 -0
  47. package/dist/freshness.js +99 -0
  48. package/dist/group-button.d.ts +33 -0
  49. package/dist/group-button.js +67 -0
  50. package/dist/growth-chart.d.ts +77 -0
  51. package/dist/growth-chart.js +195 -0
  52. package/dist/hooks/use-keyboard-fit.d.ts +15 -0
  53. package/dist/hooks/use-keyboard-fit.js +29 -0
  54. package/dist/hooks/use-mobile.d.ts +1 -0
  55. package/dist/hooks/use-mobile.js +15 -0
  56. package/dist/icon-button.d.ts +21 -0
  57. package/dist/icon-button.js +22 -0
  58. package/dist/icon-picker.d.ts +14 -0
  59. package/dist/icon-picker.js +36 -0
  60. package/dist/index.d.ts +83 -0
  61. package/dist/index.js +90 -0
  62. package/dist/integrations.d.ts +57 -0
  63. package/dist/integrations.js +73 -0
  64. package/dist/joins.d.ts +128 -0
  65. package/dist/joins.js +767 -0
  66. package/dist/kit-config.d.ts +13 -0
  67. package/dist/kit-config.js +15 -0
  68. package/dist/lib/utils.d.ts +2 -0
  69. package/dist/lib/utils.js +5 -0
  70. package/dist/lumen-boundary.d.ts +7 -0
  71. package/dist/lumen-boundary.js +10 -0
  72. package/dist/lumen-code-editor-impl.d.ts +2 -0
  73. package/dist/lumen-code-editor-impl.js +16 -0
  74. package/dist/lumen-code-editor.d.ts +11 -0
  75. package/dist/lumen-code-editor.js +11 -0
  76. package/dist/lumen-code.d.ts +22 -0
  77. package/dist/lumen-code.js +75 -0
  78. package/dist/lumen-drawer.d.ts +30 -0
  79. package/dist/lumen-drawer.js +27 -0
  80. package/dist/lumen-echart.d.ts +58 -0
  81. package/dist/lumen-echart.js +129 -0
  82. package/dist/lumen-filter.d.ts +75 -0
  83. package/dist/lumen-filter.js +546 -0
  84. package/dist/lumen-finder.d.ts +62 -0
  85. package/dist/lumen-finder.js +275 -0
  86. package/dist/lumen-form.d.ts +34 -0
  87. package/dist/lumen-form.js +81 -0
  88. package/dist/lumen-geo-map.d.ts +9 -0
  89. package/dist/lumen-geo-map.js +135 -0
  90. package/dist/lumen-grid.d.ts +49 -0
  91. package/dist/lumen-grid.js +203 -0
  92. package/dist/lumen-layout.d.ts +150 -0
  93. package/dist/lumen-layout.js +658 -0
  94. package/dist/lumen-links.d.ts +96 -0
  95. package/dist/lumen-links.js +213 -0
  96. package/dist/lumen-page-state.d.ts +20 -0
  97. package/dist/lumen-page-state.js +15 -0
  98. package/dist/lumen-selector.d.ts +139 -0
  99. package/dist/lumen-selector.js +308 -0
  100. package/dist/lumen-source.d.ts +137 -0
  101. package/dist/lumen-source.js +327 -0
  102. package/dist/lumen-tab-drawer.d.ts +27 -0
  103. package/dist/lumen-tab-drawer.js +26 -0
  104. package/dist/lumen-tabs.d.ts +21 -0
  105. package/dist/lumen-tabs.js +6 -0
  106. package/dist/lumen-toast.d.ts +11 -0
  107. package/dist/lumen-toast.js +16 -0
  108. package/dist/lumen-trend.d.ts +18 -0
  109. package/dist/lumen-trend.js +157 -0
  110. package/dist/lumen-view-card.d.ts +35 -0
  111. package/dist/lumen-view-card.js +50 -0
  112. package/dist/lumen-view.d.ts +172 -0
  113. package/dist/lumen-view.js +645 -0
  114. package/dist/lumen-widget-charts.d.ts +57 -0
  115. package/dist/lumen-widget-charts.js +69 -0
  116. package/dist/lumen-widget.d.ts +614 -0
  117. package/dist/lumen-widget.js +594 -0
  118. package/dist/market/compare-chart.d.ts +34 -0
  119. package/dist/market/compare-chart.js +292 -0
  120. package/dist/market/indicator-picker.d.ts +29 -0
  121. package/dist/market/indicator-picker.js +125 -0
  122. package/dist/market/indicators.d.ts +63 -0
  123. package/dist/market/indicators.js +314 -0
  124. package/dist/market/live.d.ts +120 -0
  125. package/dist/market/live.js +444 -0
  126. package/dist/market/lumen-chart.d.ts +88 -0
  127. package/dist/market/lumen-chart.js +645 -0
  128. package/dist/market/market-widget.d.ts +45 -0
  129. package/dist/market/market-widget.js +346 -0
  130. package/dist/market/option-chain.d.ts +19 -0
  131. package/dist/market/option-chain.js +53 -0
  132. package/dist/market/price-chart.d.ts +128 -0
  133. package/dist/market/price-chart.js +619 -0
  134. package/dist/mcp-settings.d.ts +20 -0
  135. package/dist/mcp-settings.js +65 -0
  136. package/dist/nav.d.ts +77 -0
  137. package/dist/nav.js +73 -0
  138. package/dist/note-line.d.ts +13 -0
  139. package/dist/note-line.js +5 -0
  140. package/dist/notifications.d.ts +9 -0
  141. package/dist/notifications.js +166 -0
  142. package/dist/origin-legend.d.ts +14 -0
  143. package/dist/origin-legend.js +38 -0
  144. package/dist/overview.d.ts +84 -0
  145. package/dist/overview.js +105 -0
  146. package/dist/page-inputs.d.ts +14 -0
  147. package/dist/page-inputs.js +16 -0
  148. package/dist/page.d.ts +32 -0
  149. package/dist/page.js +45 -0
  150. package/dist/pager.d.ts +10 -0
  151. package/dist/pager.js +14 -0
  152. package/dist/prefs.d.ts +71 -0
  153. package/dist/prefs.js +126 -0
  154. package/dist/query-client.d.ts +7 -0
  155. package/dist/query-client.js +19 -0
  156. package/dist/refresh.d.ts +4 -0
  157. package/dist/refresh.js +19 -0
  158. package/dist/rename-field.d.ts +11 -0
  159. package/dist/rename-field.js +26 -0
  160. package/dist/reset-button.d.ts +9 -0
  161. package/dist/reset-button.js +10 -0
  162. package/dist/route-error.d.ts +42 -0
  163. package/dist/route-error.js +229 -0
  164. package/dist/schedules.d.ts +88 -0
  165. package/dist/schedules.js +221 -0
  166. package/dist/settings.d.ts +51 -0
  167. package/dist/settings.js +181 -0
  168. package/dist/shell.d.ts +99 -0
  169. package/dist/shell.js +137 -0
  170. package/dist/source-health.d.ts +12 -0
  171. package/dist/source-health.js +58 -0
  172. package/dist/spotlight.d.ts +23 -0
  173. package/dist/spotlight.js +138 -0
  174. package/dist/status-health.d.ts +16 -0
  175. package/dist/status-health.js +41 -0
  176. package/dist/status.d.ts +9 -0
  177. package/dist/status.js +12 -0
  178. package/dist/summary-cards.d.ts +56 -0
  179. package/dist/summary-cards.js +270 -0
  180. package/dist/symbol-sheet-context.d.ts +4 -0
  181. package/dist/symbol-sheet-context.js +5 -0
  182. package/dist/symbol-sheet.d.ts +9 -0
  183. package/dist/symbol-sheet.js +24 -0
  184. package/dist/tasks.d.ts +29 -0
  185. package/dist/tasks.js +278 -0
  186. package/dist/theme.css +606 -0
  187. package/dist/tile.d.ts +37 -0
  188. package/dist/tile.js +23 -0
  189. package/dist/ui/alert-dialog.d.ts +18 -0
  190. package/dist/ui/alert-dialog.js +41 -0
  191. package/dist/ui/alert.d.ts +10 -0
  192. package/dist/ui/alert.js +27 -0
  193. package/dist/ui/avatar.d.ts +11 -0
  194. package/dist/ui/avatar.js +22 -0
  195. package/dist/ui/badge.d.ts +9 -0
  196. package/dist/ui/badge.js +27 -0
  197. package/dist/ui/breadcrumb.d.ts +11 -0
  198. package/dist/ui/breadcrumb.js +27 -0
  199. package/dist/ui/button-group.d.ts +11 -0
  200. package/dist/ui/button-group.js +27 -0
  201. package/dist/ui/button.d.ts +10 -0
  202. package/dist/ui/button.js +38 -0
  203. package/dist/ui/card.d.ts +11 -0
  204. package/dist/ui/card.js +24 -0
  205. package/dist/ui/chart.d.ts +44 -0
  206. package/dist/ui/chart.js +117 -0
  207. package/dist/ui/checkbox.d.ts +4 -0
  208. package/dist/ui/checkbox.js +8 -0
  209. package/dist/ui/collapsible.d.ts +5 -0
  210. package/dist/ui/collapsible.js +12 -0
  211. package/dist/ui/combobox.d.ts +24 -0
  212. package/dist/ui/combobox.js +60 -0
  213. package/dist/ui/command.d.ts +18 -0
  214. package/dist/ui/command.js +34 -0
  215. package/dist/ui/dialog.d.ts +23 -0
  216. package/dist/ui/dialog.js +50 -0
  217. package/dist/ui/dropdown-menu.d.ts +29 -0
  218. package/dist/ui/dropdown-menu.js +50 -0
  219. package/dist/ui/emoji-picker.d.ts +6 -0
  220. package/dist/ui/emoji-picker.js +28 -0
  221. package/dist/ui/empty.d.ts +11 -0
  222. package/dist/ui/empty.js +33 -0
  223. package/dist/ui/field.d.ts +24 -0
  224. package/dist/ui/field.js +65 -0
  225. package/dist/ui/hover-card.d.ts +6 -0
  226. package/dist/ui/hover-card.js +13 -0
  227. package/dist/ui/input-group.d.ts +16 -0
  228. package/dist/ui/input-group.js +56 -0
  229. package/dist/ui/input.d.ts +3 -0
  230. package/dist/ui/input.js +6 -0
  231. package/dist/ui/item.d.ts +23 -0
  232. package/dist/ui/item.js +67 -0
  233. package/dist/ui/kbd.d.ts +3 -0
  234. package/dist/ui/kbd.js +9 -0
  235. package/dist/ui/label.d.ts +4 -0
  236. package/dist/ui/label.js +7 -0
  237. package/dist/ui/popover.d.ts +10 -0
  238. package/dist/ui/popover.js +25 -0
  239. package/dist/ui/progress.d.ts +4 -0
  240. package/dist/ui/progress.js +9 -0
  241. package/dist/ui/resizable.d.ts +7 -0
  242. package/dist/ui/resizable.js +13 -0
  243. package/dist/ui/scroll-area.d.ts +5 -0
  244. package/dist/ui/scroll-area.js +10 -0
  245. package/dist/ui/select.d.ts +15 -0
  246. package/dist/ui/select.js +36 -0
  247. package/dist/ui/separator.d.ts +4 -0
  248. package/dist/ui/separator.js +7 -0
  249. package/dist/ui/sheet.d.ts +14 -0
  250. package/dist/ui/sheet.js +54 -0
  251. package/dist/ui/sidebar.d.ts +69 -0
  252. package/dist/ui/sidebar.js +206 -0
  253. package/dist/ui/skeleton.d.ts +2 -0
  254. package/dist/ui/skeleton.js +6 -0
  255. package/dist/ui/slider.d.ts +4 -0
  256. package/dist/ui/slider.js +9 -0
  257. package/dist/ui/sonner.d.ts +3 -0
  258. package/dist/ui/sonner.js +24 -0
  259. package/dist/ui/spinner.d.ts +2 -0
  260. package/dist/ui/spinner.js +7 -0
  261. package/dist/ui/switch.d.ts +6 -0
  262. package/dist/ui/switch.js +7 -0
  263. package/dist/ui/table.d.ts +10 -0
  264. package/dist/ui/table.js +31 -0
  265. package/dist/ui/tabs.d.ts +7 -0
  266. package/dist/ui/tabs.js +22 -0
  267. package/dist/ui/textarea.d.ts +3 -0
  268. package/dist/ui/textarea.js +6 -0
  269. package/dist/ui/toggle-group.d.ts +10 -0
  270. package/dist/ui/toggle-group.js +22 -0
  271. package/dist/ui/toggle.d.ts +9 -0
  272. package/dist/ui/toggle.js +25 -0
  273. package/dist/ui/tooltip.d.ts +7 -0
  274. package/dist/ui/tooltip.js +16 -0
  275. package/dist/view-edit-cell.d.ts +49 -0
  276. package/dist/view-edit-cell.js +198 -0
  277. package/dist/view-editor.d.ts +52 -0
  278. package/dist/view-editor.js +940 -0
  279. package/dist/view-formulas.d.ts +61 -0
  280. package/dist/view-formulas.js +247 -0
  281. package/dist/view-icons.d.ts +20 -0
  282. package/dist/view-icons.js +70 -0
  283. package/dist/view-model.d.ts +390 -0
  284. package/dist/view-model.js +316 -0
  285. package/dist/whats-new.d.ts +3 -0
  286. package/dist/whats-new.js +53 -0
  287. package/dist/widget-controls.d.ts +9 -0
  288. package/dist/widget-controls.js +26 -0
  289. package/dist/widget-settings.d.ts +436 -0
  290. package/dist/widget-settings.js +290 -0
  291. package/dist/widget-thumbs.d.ts +3 -0
  292. package/dist/widget-thumbs.js +29 -0
  293. package/dist/workspace-keys.d.ts +12 -0
  294. package/dist/workspace-keys.js +13 -0
  295. package/dist/workspaces.d.ts +57 -0
  296. package/dist/workspaces.js +473 -0
  297. package/package.json +82 -4
@@ -0,0 +1,181 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Settings: one large dialog, opened from the profile menu or Ctrl/Cmd+, with
3
+ // its sections listed on the left. General holds Profile (display name, email,
4
+ // sign out), Appearance (theme, font, text size, glass) and Preferences (time zone, observability
5
+ // details), the same on every app; then All apps (Notifications, the assistant, MCP), then the app's own
6
+ // sections (an app: Repos). On phone the list becomes a picker.
7
+ // A search box tops the list and filters the rows on every section.
8
+ // `?settings=<section>` in the URL opens it at that section (GitHub's install page returns there).
9
+ import { LogOutIcon, MonitorIcon, MoonIcon, PaletteIcon, SearchIcon, SlidersHorizontalIcon, SunIcon, UserRoundIcon, XIcon, } from 'lucide-react';
10
+ import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, } from 'react';
11
+ import { createPortal } from 'react-dom';
12
+ import { toast } from 'sonner';
13
+ import { useAssistantMoves } from './assistant-screen.js';
14
+ import { LumenSelector } from './lumen-selector.js';
15
+ import { FONTS, FONT_SIZES, setPref, usePrefs, } from './prefs.js';
16
+ import { TimeZoneSelect } from './status.js';
17
+ import { Button } from './ui/button.js';
18
+ import { Dialog, DialogContent, DialogDescription, DialogTitle } from './ui/dialog.js';
19
+ import { Input } from './ui/input.js';
20
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './ui/input-group.js';
21
+ import { Label } from './ui/label.js';
22
+ import { Separator } from './ui/separator.js';
23
+ import { SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, } from './ui/sidebar.js';
24
+ import { Switch } from './ui/switch.js';
25
+ import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
26
+ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
27
+ const THEMES = [
28
+ { value: 'light', label: 'Light theme', short: 'Light', icon: SunIcon },
29
+ { value: 'dark', label: 'Dark theme', short: 'Dark', icon: MoonIcon },
30
+ { value: 'system', label: 'Follow the device', short: 'System', icon: MonitorIcon },
31
+ ];
32
+ /** The Settings search text; rows that do not match it hide. */
33
+ const SearchContext = createContext('');
34
+ const matches = (query, ...texts) => texts.some((t) => typeof t === 'string' && t.toLowerCase().includes(query.toLowerCase()));
35
+ /** The open section's header slot for its actions (Add and Resync sit by the
36
+ * title, not on a sub-header). Each section gets its own, so search results keep theirs apart. */
37
+ const ActionsSlot = createContext(null);
38
+ /** Renders its children in the section header, right of the title. */
39
+ export function SettingsActions({ children }) {
40
+ const el = useContext(ActionsSlot);
41
+ return el ? createPortal(children, el) : null;
42
+ }
43
+ /** The open section's footer slot, pinned under its body. */
44
+ const FooterSlot = createContext(null);
45
+ /** Renders its children in the open section's footer, from anywhere in the section's content. */
46
+ export function SettingsFooter({ children }) {
47
+ const el = useContext(FooterSlot);
48
+ return el ? createPortal(children, el) : null;
49
+ }
50
+ function ActionsScope({ children, }) {
51
+ const [el, setEl] = useState(null);
52
+ const [foot, setFoot] = useState(null);
53
+ return (_jsx(ActionsSlot.Provider, { value: el, children: _jsx(FooterSlot.Provider, { value: foot, children: children(setEl, setFoot) }) }));
54
+ }
55
+ /** One setting: title and description on the left, its control on the right. */
56
+ export function SettingRow({ label, description, htmlFor, children, }) {
57
+ const query = useContext(SearchContext);
58
+ if (query && !matches(query, label, description))
59
+ return null;
60
+ return (_jsxs("div", { "data-slot": "setting-row", className: "flex items-center gap-4 border-b py-3 last:border-b-0", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx(Label, { htmlFor: htmlFor, children: label }), description && _jsx("p", { className: "text-xs text-muted-foreground", children: description })] }), _jsx("div", { className: "shrink-0", children: children })] }));
61
+ }
62
+ /** Light, dark or system: in Settings with labels, in the profile menu as icons with tooltips. */
63
+ export function ThemeToggle({ compact = false }) {
64
+ const prefs = usePrefs();
65
+ return (_jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: prefs.theme, onValueChange: (v) => v && setPref('theme', v), "aria-label": "Theme", children: THEMES.map((t) => compact ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(ToggleGroupItem, { value: t.value, "aria-label": t.label, children: _jsx(t.icon, {}) }) }), _jsx(TooltipContent, { children: t.short })] }, t.value)) : (_jsxs(ToggleGroupItem, { value: t.value, "aria-label": t.label, children: [_jsx(t.icon, {}), t.short] }, t.value))) }));
66
+ }
67
+ function Appearance() {
68
+ const prefs = usePrefs();
69
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsx(SettingRow, { label: "Theme", description: "Light, dark, or follow the device.", children: _jsx(ThemeToggle, {}) }), _jsx(SettingRow, { label: "Font", description: "The typeface for text. Numbers keep their own.", children: _jsx(LumenSelector, { label: "Font", size: "sm", className: "w-40", options: FONTS.map((f) => ({ value: f.id, label: f.label })), value: prefs.font, onChange: (v) => setPref('font', v) }) }), _jsx(SettingRow, { label: "Text size", description: "Larger text makes everything a little roomier.", children: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: prefs.fontSize, onValueChange: (v) => v && setPref('fontSize', v), "aria-label": "Text size", children: FONT_SIZES.map((f) => (_jsx(ToggleGroupItem, { value: f.id, "aria-label": `${f.label} text`, children: f.label }, f.id))) }) }), _jsx(SettingRow, { label: "Glass surfaces", description: "Frosted cards and panels.", htmlFor: "settings-glass", children: _jsx(Switch, { id: "settings-glass", checked: prefs.glass, onCheckedChange: (v) => setPref('glass', v) }) })] }));
70
+ }
71
+ function Preferences() {
72
+ const prefs = usePrefs();
73
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsx(SettingRow, { label: "Time zone", description: "Every time on every desk shows in this zone.", children: _jsx(TimeZoneSelect, {}) }), _jsx(SettingRow, { label: "Observability details", description: "Show how long each card and view took to load, with its rows and when.", htmlFor: "settings-observability", children: _jsx(Switch, { id: "settings-observability", checked: prefs.observability, onCheckedChange: (v) => setPref('observability', v) }) })] }));
74
+ }
75
+ /** General: the person's own settings, the same on every app. */
76
+ const PROFILE = {
77
+ id: 'profile',
78
+ title: 'Profile',
79
+ icon: UserRoundIcon,
80
+ description: 'Your name and sign-in on every desk.',
81
+ };
82
+ const GENERAL = [
83
+ {
84
+ id: 'appearance',
85
+ title: 'Appearance',
86
+ icon: PaletteIcon,
87
+ description: 'How every desk looks, saved in this browser.',
88
+ content: _jsx(Appearance, {}),
89
+ },
90
+ {
91
+ id: 'preferences',
92
+ title: 'Preferences',
93
+ icon: SlidersHorizontalIcon,
94
+ description: 'Times and details on every desk, saved in this browser.',
95
+ content: _jsx(Preferences, {}),
96
+ },
97
+ ];
98
+ /** Every Settings section in the order the dialog lists them: General, the product's, the app's. */
99
+ export function allSettings(profile, shared = [], sections = []) {
100
+ const general = profile ? [{ ...PROFILE, content: profile }, ...GENERAL] : GENERAL;
101
+ return [...general, ...shared, ...sections];
102
+ }
103
+ export function LumenSettings({ open, section, onOpen, onClose, appLabel, profile, sections = [], shared = [], sharedLabel = 'All apps', }) {
104
+ const all = allSettings(profile, shared, sections);
105
+ const general = all.slice(0, all.length - shared.length - sections.length);
106
+ const current = all.find((s) => s.id === section) ?? all[0];
107
+ // The assistant opens Settings at a section (it never changes a setting); Undo puts it back as it was.
108
+ useAssistantMoves('settings', {
109
+ settings: onOpen,
110
+ sections: all.map((s) => ({ id: s.id, title: s.title })),
111
+ snapshot: () => (open ? section : null),
112
+ restore: (s) => (typeof s === 'string' ? onOpen(s) : onClose()),
113
+ });
114
+ // Ctrl/Cmd+, opens Settings, like most apps.
115
+ useEffect(() => {
116
+ const onKey = (e) => {
117
+ if (e.key === ',' && (e.metaKey || e.ctrlKey)) {
118
+ e.preventDefault();
119
+ onOpen(section);
120
+ }
121
+ };
122
+ window.addEventListener('keydown', onKey);
123
+ return () => window.removeEventListener('keydown', onKey);
124
+ }, [onOpen, section]);
125
+ // Search: every section's matching rows show at once; a section matching by
126
+ // title or description shows whole. Sections with nothing to show drop out of the nav.
127
+ const [query, setQuery] = useState('');
128
+ const q = query.trim();
129
+ const pane = useRef(null);
130
+ const [found, setFound] = useState([]);
131
+ useLayoutEffect(() => {
132
+ if (!q)
133
+ return;
134
+ const ids = [...(pane.current?.querySelectorAll('[data-settings-section]') ?? [])]
135
+ .filter((el) => el.dataset.whole === 'true' || el.querySelector('[data-slot=setting-row]'))
136
+ .map((el) => el.dataset.settingsSection ?? '');
137
+ setFound((f) => (f.join() === ids.join() ? f : ids));
138
+ });
139
+ const shown = (s) => !q || found.includes(s.id);
140
+ const go = (id) => {
141
+ setQuery('');
142
+ onOpen(id);
143
+ };
144
+ const groups = [
145
+ { label: 'General', items: general.filter(shown) },
146
+ { label: sharedLabel, items: shared.filter(shown) },
147
+ ...(sections.length ? [{ label: appLabel, items: sections.filter(shown) }] : []),
148
+ ].filter((g) => g.items.length);
149
+ const search = (_jsxs(InputGroup, { className: "bg-background", children: [_jsx(InputGroupAddon, { children: _jsx(SearchIcon, {}) }), _jsx(InputGroupInput, { type: "search", className: "[&::-webkit-search-cancel-button]:hidden", "aria-label": "Search settings", placeholder: "Search", value: query, onChange: (e) => setQuery(e.target.value) }), query && (_jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupButton, { size: "icon-xs", "aria-label": "Clear search", onClick: () => setQuery(''), children: _jsx(XIcon, {}) }) }))] }));
150
+ const header = (s, slot) => (_jsxs("div", { className: "flex items-end gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("h3", { id: `settings-${s.id}-title`, className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(s.icon, { className: "size-4 text-muted-foreground" }), s.title] }), s.description && (_jsx("p", { className: "truncate text-sm text-muted-foreground", title: typeof s.description === 'string' ? s.description : undefined, children: s.description }))] }), _jsx("div", { ref: slot, className: "flex shrink-0 items-center gap-2 empty:hidden" })] }));
151
+ return (_jsx(Dialog, { open: open, onOpenChange: (o) => {
152
+ if (o)
153
+ onOpen(section);
154
+ else {
155
+ setQuery('');
156
+ onClose();
157
+ }
158
+ }, children: _jsxs(DialogContent, { "data-slot": "settings",
159
+ // Esc clears the search first, then closes.
160
+ onEscapeKeyDown: (e) => {
161
+ if (query) {
162
+ e.preventDefault();
163
+ setQuery('');
164
+ }
165
+ }, className: "flex h-[min(720px,calc(100svh-2rem))] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl md:flex-row", children: [_jsx(DialogTitle, { className: "sr-only", children: "Settings" }), _jsx(DialogDescription, { className: "sr-only", children: "Your preferences and this desk's settings." }), _jsxs("nav", { "aria-label": "Settings sections", className: "hidden w-56 shrink-0 flex-col border-r bg-sidebar py-2 md:flex", children: [_jsx("div", { className: "px-2 pt-1 pb-1", children: search }), groups.map((g) => (_jsxs(SidebarGroup, { className: "py-1", children: [_jsx(SidebarGroupLabel, { children: g.label }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: g.items.map((s) => (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: !q && s.id === current.id, className: s.sub ? 'pl-7' : undefined, onClick: () => go(s.id), children: [_jsx(s.icon, {}), _jsx("span", { children: s.title })] }) }, s.id))) }) })] }, g.label)))] }), _jsxs("div", { className: "flex flex-col gap-2 border-b p-3 pr-12 md:hidden", children: [search, !q && (_jsx(LumenSelector, { label: "Settings section", className: "w-full", groups: groups.map((g) => ({
166
+ label: g.label,
167
+ options: g.items.map((s) => ({
168
+ value: s.id,
169
+ label: s.title,
170
+ icon: s.icon,
171
+ indent: !!s.sub,
172
+ })),
173
+ })), value: current.id, onChange: go }))] }), q ? (_jsxs("section", { ref: pane, "aria-label": "Search results",
174
+ // The top band on desktop keeps results clear of the dialog's X.
175
+ className: "flex min-w-0 flex-1 flex-col gap-6 overflow-y-auto px-4 pt-4 pb-8 md:mt-12 md:border-t md:px-6 md:pt-6", children: [all.map((s) => {
176
+ const whole = matches(q, s.title, s.description);
177
+ return (_jsx(ActionsScope, { children: (slot) => (_jsxs("div", { "data-settings-section": s.id, "data-whole": whole, "aria-labelledby": `settings-${s.id}-title`, className: whole
178
+ ? 'flex flex-col gap-3'
179
+ : 'hidden flex-col gap-3 has-[[data-slot=setting-row]]:flex', children: [header(s, slot), _jsx(Separator, {}), _jsx(SearchContext.Provider, { value: whole ? '' : q, children: s.content })] })) }, s.id));
180
+ }), !found.length && (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["No settings match \u201C", q, "\u201D."] }))] })) : (_jsx(ActionsScope, { children: (slot, footer) => (_jsxs("section", { "aria-labelledby": `settings-${current.id}-title`, className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex shrink-0 flex-col gap-3 px-4 pt-4 md:px-6 md:pt-6", children: [header(current, slot), _jsx(Separator, {})] }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-4 pt-3 pb-4 *:shrink-0 md:px-6", children: current.content }), _jsx("div", { ref: footer, "data-slot": "settings-footer", className: "flex min-h-12 shrink-0 items-center justify-end gap-2 border-t px-4 py-2 md:px-6", children: current.footer })] })) }, current.id))] }) }));
181
+ }
@@ -0,0 +1,99 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type LumenFinderConfig } from './lumen-finder.js';
3
+ import { type SettingsSectionDef } from './settings.js';
4
+ interface ShellContext {
5
+ /** The app this page renders in (`data-app` on <html>). */
6
+ app: string;
7
+ /** Its name, where Settings lists its own sections. */
8
+ appLabel: string;
9
+ /** Where a page's status items render (the status bar's page slot). */
10
+ statusSlot: HTMLElement | null;
11
+ /** Opens Settings at a section (`preferences` or one of the app's own). */
12
+ openSettings: (section?: string) => void;
13
+ /** Closes Settings (a section that sends the person to a page). */
14
+ closeSettings: () => void;
15
+ /** The header's Refresh: reloads the page's data, skipping the server cache. Null without `onRefresh`. */
16
+ refresh: (() => Promise<void>) | null;
17
+ refreshing: boolean;
18
+ /** The app shows Hide amounts in its header (`hideAmounts` on `LumenShell`). */
19
+ hideAmounts: boolean;
20
+ /** The product's buttons on every page header, after Refresh (`headerActions`). */
21
+ headerActions: ReactNode;
22
+ }
23
+ export declare function useShell(): ShellContext;
24
+ /** The app this page renders in, or null outside a shell (a preview). */
25
+ export declare function useApp(): {
26
+ id: string;
27
+ label: string;
28
+ } | null;
29
+ /** True while Hide amounts is on in an app that offers it: holding amounts render as `•••••`. */
30
+ export declare function useAmountsHidden(): boolean;
31
+ /** `+$152,372` → `+$•••••`: every number in the text masked, signs and units kept. */
32
+ export declare const maskAmount: (text: string) => string;
33
+ /** Who is signed in, for the profile button; null while loading or where nobody is. */
34
+ export interface ShellUser {
35
+ email: string;
36
+ /** The name shown, when there is one. */
37
+ name: string | null;
38
+ }
39
+ /** `AL` from "Ada Lovelace", else the first two letters of the email. */
40
+ export declare function initials(who: ShellUser | null): string;
41
+ /** What the profile button shows: the first name only, else the email's name. */
42
+ export declare function firstName(who: ShellUser | null): string;
43
+ export interface LumenShellProps {
44
+ /** The app's id: `data-app` on <html>, which picks its accent. */
45
+ app: string;
46
+ /** The app's name, heading its own Settings sections. */
47
+ appLabel: string;
48
+ /** The sidebar's top: the app's name, or a switcher between a product's apps. */
49
+ brand: ReactNode;
50
+ /** The app's sidebar content: `NavGroup`s and `SidebarSection`s. */
51
+ nav: ReactNode;
52
+ /** App-wide status bar items (health, freshness); pages add their own through `Page`. */
53
+ status?: ReactNode;
54
+ /** The services the app checks, as one `StatusHealth` chip at the status bar's end. */
55
+ health?: ReactNode;
56
+ /** Status bar items after `health` and What's new, before the time zone. */
57
+ statusEnd?: ReactNode;
58
+ /** Turns on What's new: the app's releases API (`GET ?limit=&before=` answering `{releases}`).
59
+ * The status bar shows the newest version, which opens each release's Key takeaways. */
60
+ releases?: string;
61
+ /** The app's own Settings sections, listed after General and the product's. */
62
+ settings?: SettingsSectionDef[];
63
+ /** Sections every app of the product shows, listed after General under `sharedLabel`. */
64
+ shared?: SettingsSectionDef[];
65
+ sharedLabel?: string;
66
+ /** Who is signed in (the profile button); null while it loads. */
67
+ user?: ShellUser | null;
68
+ /** Where Sign out goes; no Sign out without it. */
69
+ signOutUrl?: string;
70
+ /** Settings ▸ General ▸ Profile's content; no Profile section without it. */
71
+ profile?: ReactNode;
72
+ /** Reloads the page's data and drops every cached read (TanStack Query `invalidateQueries`);
73
+ * shows Refresh in the header, which then mounts the page again. */
74
+ onRefresh?: () => Promise<unknown>;
75
+ /** Shows Hide amounts (eye) in the header: masks the fields and values the app marks `amount`. */
76
+ hideAmounts?: boolean;
77
+ /** Opens the app's Logs page; shows Logs in the profile menu. */
78
+ onLogs?: () => void;
79
+ /** The product's buttons on every page header, after Refresh (the product's Tasks and the assistant). */
80
+ headerActions?: ReactNode;
81
+ /** A panel docked right of the page (the product's assistant). */
82
+ panel?: ReactNode;
83
+ /** How ⌘K Find reads in this app: its chips and how the nav's pages group. */
84
+ finder?: LumenFinderConfig;
85
+ /** Wraps the frame in the product's providers, inside the shell so they can call `useShell`. */
86
+ wrap?: (frame: ReactNode) => ReactNode;
87
+ /** The routed page (usually `<Outlet />`). */
88
+ children: ReactNode;
89
+ }
90
+ export declare function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, settings, shared, sharedLabel, user, signOutUrl, profile, onRefresh, hideAmounts, onLogs, headerActions, panel, finder, wrap, children, }: LumenShellProps): import("react").JSX.Element;
91
+ /** A status bar entry: an optional health dot, a label and a mono value. */
92
+ export declare function StatusItem({ label, value, ok, className, }: {
93
+ label: ReactNode;
94
+ value?: ReactNode;
95
+ /** Green when true, red when false, grey while unknown; no dot when left out. */
96
+ ok?: boolean | null;
97
+ className?: string;
98
+ }): import("react").JSX.Element;
99
+ export {};
package/dist/shell.js ADDED
@@ -0,0 +1,137 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The frame every app renders in, from the shadcn sidebar-07 block: the app's
3
+ // brand (or an app switcher) top-left, its own nav, the profile bottom-left; collapses to icons on
4
+ // desktop and becomes a drawer on phone. An app passes its id, nav and status items; pages render in
5
+ // `Page`. What a product adds to every app (the product's app switcher, the assistant, Tasks) comes in through
6
+ // the slots below (`brand`, `wrap`, `panel`, `headerActions`, `shared`), never from here.
7
+ import { useRouterState } from '@tanstack/react-router';
8
+ import { ChevronsUpDownIcon, LogOutIcon, ScrollTextIcon, SettingsIcon, SunMoonIcon, } from 'lucide-react';
9
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
10
+ import { cn } from './lib/utils.js';
11
+ import { LumenFinder } from './lumen-finder.js';
12
+ import { usePrefs } from './prefs.js';
13
+ import { withRefreshToken } from './refresh.js';
14
+ import { LumenSettings, ThemeToggle, allSettings } from './settings.js';
15
+ import { SourceHealthProvider } from './source-health.js';
16
+ import { StatusTimeZone } from './status.js';
17
+ import { Avatar, AvatarFallback } from './ui/avatar.js';
18
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
19
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarRail, useSidebar, } from './ui/sidebar.js';
20
+ import { Toaster } from './ui/sonner.js';
21
+ import { TooltipProvider } from './ui/tooltip.js';
22
+ import { WhatsNew } from './whats-new.js';
23
+ const Ctx = createContext(null);
24
+ export function useShell() {
25
+ const c = useContext(Ctx);
26
+ if (!c)
27
+ throw new Error('useShell must be used inside <LumenShell>.');
28
+ return c;
29
+ }
30
+ /** The app this page renders in, or null outside a shell (a preview). */
31
+ export function useApp() {
32
+ const c = useContext(Ctx);
33
+ return c ? { id: c.app, label: c.appLabel } : null;
34
+ }
35
+ /** True while Hide amounts is on in an app that offers it: holding amounts render as `•••••`. */
36
+ export function useAmountsHidden() {
37
+ const offered = useContext(Ctx)?.hideAmounts ?? false;
38
+ return usePrefs().hideAmounts && offered;
39
+ }
40
+ /** `+$152,372` → `+$•••••`: every number in the text masked, signs and units kept. */
41
+ export const maskAmount = (text) => text.replace(/\d[\d.,]*/g, '•••••');
42
+ /** The sidebar provider writes this cookie on toggle; reading it back keeps the choice per app. */
43
+ const sidebarOpen = () => typeof document === 'undefined' || !document.cookie.split('; ').includes('sidebar_state=false');
44
+ /** The phone drawer closes once a link in it opens a page. */
45
+ function CloseDrawerOnNavigate() {
46
+ const path = useRouterState({ select: (s) => s.location.pathname });
47
+ const { setOpenMobile } = useSidebar();
48
+ // biome-ignore lint/correctness/useExhaustiveDependencies: runs on each new path.
49
+ useEffect(() => setOpenMobile(false), [path, setOpenMobile]);
50
+ return null;
51
+ }
52
+ const SETTINGS_KEYS = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘,' : 'Ctrl+,';
53
+ /** The profile menu's theme line: not a menu item, so a pick keeps it open. */
54
+ function ThemeItem() {
55
+ return (_jsxs("div", { "data-slot": "theme-item", className: "flex items-center gap-2 px-2 py-1 text-sm", children: [_jsx(SunMoonIcon, { className: "size-4 text-muted-foreground" }), _jsx("span", { className: "flex-1", children: "Theme" }), _jsx(ThemeToggle, { compact: true })] }));
56
+ }
57
+ /** `AL` from "Ada Lovelace", else the first two letters of the email. */
58
+ export function initials(who) {
59
+ if (!who)
60
+ return '··';
61
+ const parts = (who.name ?? '').trim().split(/\s+/).filter(Boolean);
62
+ if (parts.length >= 2)
63
+ return `${parts[0]?.[0] ?? ''}${parts.at(-1)?.[0] ?? ''}`.toUpperCase();
64
+ return who.email.slice(0, 2).toUpperCase();
65
+ }
66
+ /** What the profile button shows: the first name only, else the email's name. */
67
+ export function firstName(who) {
68
+ if (!who)
69
+ return 'Signed in';
70
+ return (who.name ?? '').trim().split(/\s+/)[0] || (who.email.split('@')[0] ?? who.email);
71
+ }
72
+ function NavUser({ who, signOutUrl, onSettings, onLogs, }) {
73
+ const { isMobile } = useSidebar();
74
+ const person = (_jsxs(_Fragment, { children: [_jsx(Avatar, { className: "size-8 rounded-lg", children: _jsx(AvatarFallback, { className: "rounded-lg text-xs", children: initials(who) }) }), _jsx("span", { className: "flex-1 truncate text-left text-sm font-medium", children: firstName(who) })] }));
75
+ return (_jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", tooltip: "Profile", "aria-label": "Profile", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [person, _jsx(ChevronsUpDownIcon, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "min-w-56 rounded-lg", side: isMobile ? 'bottom' : 'right', align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "truncate font-normal text-muted-foreground", children: who?.email ?? 'Signed in' }), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onSelect: onSettings, children: [_jsx(SettingsIcon, {}), "Settings", _jsx(DropdownMenuShortcut, { children: SETTINGS_KEYS })] }), onLogs ? (_jsxs(DropdownMenuItem, { onSelect: onLogs, children: [_jsx(ScrollTextIcon, {}), "Logs"] })) : null, _jsx(ThemeItem, {})] }), signOutUrl && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, children: _jsxs("a", { href: signOutUrl, "data-sign-out": true, children: [_jsx(LogOutIcon, {}), "Sign out"] }) })] }))] })] }) }) }));
76
+ }
77
+ // `?settings=<section>` opens Settings there (GitHub's install page returns to it). It is read when
78
+ // the page loads, before an app's router can redirect it away, and dropped from the URL so a reload
79
+ // does not reopen it.
80
+ const settingsFromUrl = (() => {
81
+ if (typeof window === 'undefined')
82
+ return null;
83
+ const url = new URL(window.location.href);
84
+ const at = url.searchParams.get('settings');
85
+ if (at === null)
86
+ return null;
87
+ url.searchParams.delete('settings');
88
+ window.history.replaceState(window.history.state, '', url);
89
+ return at || 'profile';
90
+ })();
91
+ export function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, settings, shared, sharedLabel, user = null, signOutUrl, profile, onRefresh, hideAmounts = false, onLogs, headerActions = null, panel, finder, wrap = (frame) => frame, children, }) {
92
+ const [statusSlot, setStatusSlot] = useState(null);
93
+ const [refreshing, setRefreshing] = useState(false);
94
+ // Refresh reloads the layout too: once the data is fresh the page mounts
95
+ // again, so every card, tab and saved layout starts from what the server holds now.
96
+ const [epoch, setEpoch] = useState(0);
97
+ const refresh = useCallback(async () => {
98
+ if (!onRefresh)
99
+ return;
100
+ setRefreshing(true);
101
+ try {
102
+ await withRefreshToken(onRefresh);
103
+ }
104
+ finally {
105
+ setRefreshing(false);
106
+ setEpoch((e) => e + 1);
107
+ }
108
+ }, [onRefresh]);
109
+ const [settingsAt, setSettingsAt] = useState(settingsFromUrl);
110
+ const [lastSection, setLastSection] = useState(settingsAt ?? 'profile');
111
+ const openSettings = useCallback((section) => {
112
+ const at = section ?? lastSection;
113
+ setLastSection(at);
114
+ setSettingsAt(at);
115
+ }, [lastSection]);
116
+ useEffect(() => {
117
+ document.documentElement.dataset.app = app;
118
+ }, [app]);
119
+ // Find lists every Settings section by name.
120
+ const sectionNames = JSON.stringify(allSettings(profile, shared, settings).map((s) => ({ id: s.id, title: s.title })));
121
+ const findSettings = useMemo(() => JSON.parse(sectionNames), [sectionNames]);
122
+ return (_jsx(Ctx.Provider, { value: {
123
+ app,
124
+ appLabel,
125
+ statusSlot,
126
+ openSettings,
127
+ closeSettings: () => setSettingsAt(null),
128
+ refresh: onRefresh ? refresh : null,
129
+ refreshing,
130
+ hideAmounts,
131
+ headerActions,
132
+ }, children: _jsx(SourceHealthProvider, { children: wrap(_jsx(TooltipProvider, { delayDuration: 200, children: _jsx(SidebarProvider, { defaultOpen: sidebarOpen(), children: _jsxs(LumenFinder, { app: app, settings: findSettings, openSettings: openSettings, config: finder, children: [_jsx(CloseDrawerOnNavigate, {}), _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: brand }), _jsx(SidebarContent, { children: nav }), _jsx(SidebarFooter, { children: _jsx(NavUser, { who: user, signOutUrl: signOutUrl, onSettings: () => openSettings(), onLogs: onLogs }) }), _jsx(SidebarRail, {})] }), _jsxs(SidebarInset, { className: "flex min-h-svh min-w-0 flex-col", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: children }, epoch), _jsxs("footer", { "data-slot": "shell-status", "aria-label": "Status", className: "sticky bottom-0 z-10 flex h-(--shell-status-height) shrink-0 items-center gap-4 overflow-x-auto overflow-y-hidden border-t bg-background px-(--shell-gutter) text-xs whitespace-nowrap text-muted-foreground", children: [status, _jsx("div", { ref: setStatusSlot, className: "flex shrink-0 items-center gap-4" }), _jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: [health, releases && _jsx(WhatsNew, { api: releases }), statusEnd, _jsx(StatusTimeZone, {})] })] })] }), panel, _jsx(LumenSettings, { open: settingsAt !== null, section: settingsAt ?? lastSection, onOpen: openSettings, onClose: () => setSettingsAt(null), appLabel: appLabel, profile: profile, sections: settings, shared: shared, sharedLabel: sharedLabel }), _jsx(Toaster, { position: "bottom-right" })] }) }) })) }) }));
133
+ }
134
+ /** A status bar entry: an optional health dot, a label and a mono value. */
135
+ export function StatusItem({ label, value, ok, className, }) {
136
+ return (_jsxs("span", { className: cn('flex items-center gap-1.5', className), children: [ok !== undefined && (_jsx("span", { "aria-hidden": true, className: cn('size-2 rounded-full', ok === null ? 'bg-muted-foreground/40' : ok ? 'bg-gain' : 'bg-destructive') })), label, value !== undefined && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "num text-foreground/80", children: value })] }))] }));
137
+ }
@@ -0,0 +1,12 @@
1
+ import { type SourceState } from '@columenlabs/lumen-ui-types/domains';
2
+ import { type ReactNode } from 'react';
3
+ export declare function SourceHealthProvider({ children }: {
4
+ children: ReactNode;
5
+ }): import("react").JSX.Element;
6
+ export declare function useReportSources(sources: SourceState[]): void;
7
+ /** Every source the cards on screen read, one entry per source (the first card's word). */
8
+ export declare function useSourceHealth(): SourceState[];
9
+ /** One line on the sources behind the page: `a broker live · an app Oct 15 · Feed off`. */
10
+ export declare function SourcesLine({ className }: {
11
+ className?: string;
12
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Source health (views from data domains): the sources every card on screen
3
+ // reads through its joins, one report per shell, so the assistant panel can say in one line how each
4
+ // fared (the same report that marks a column's header and the status bar).
5
+ import { sourceLine } from '@columenlabs/lumen-ui-types/domains';
6
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useState, } from 'react';
7
+ import { detailOf } from './joins.js';
8
+ const HealthCtx = createContext(null);
9
+ export function SourceHealthProvider({ children }) {
10
+ const [reports, setReports] = useState(() => new Map());
11
+ // Stable, so a card's report effect runs once per change of its sources, not once per report.
12
+ const set = useCallback((id, sources) => setReports((was) => {
13
+ const next = new Map(was);
14
+ if (sources?.length)
15
+ next.set(id, sources);
16
+ else
17
+ next.delete(id);
18
+ return next;
19
+ }), []);
20
+ const value = useMemo(() => ({ reports, set }), [reports, set]);
21
+ return _jsx(HealthCtx.Provider, { value: value, children: children });
22
+ }
23
+ export function useReportSources(sources) {
24
+ const ctx = useContext(HealthCtx);
25
+ const id = useId();
26
+ const set = ctx?.set;
27
+ const key = JSON.stringify(sources);
28
+ // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the sources' content.
29
+ useEffect(() => {
30
+ if (!set)
31
+ return;
32
+ set(id, sources);
33
+ return () => set(id, null);
34
+ }, [set, id, key]);
35
+ }
36
+ /** Every source the cards on screen read, one entry per source (the first card's word). */
37
+ export function useSourceHealth() {
38
+ const ctx = useContext(HealthCtx);
39
+ return useMemo(() => {
40
+ const out = new Map();
41
+ for (const list of ctx?.reports.values() ?? [])
42
+ for (const s of list)
43
+ if (!out.has(s.id))
44
+ out.set(s.id, s);
45
+ return [...out.values()];
46
+ }, [ctx?.reports]);
47
+ }
48
+ /** One line on the sources behind the page: `a broker live · an app Oct 15 · Feed off`. */
49
+ export function SourcesLine({ className }) {
50
+ const sources = useSourceHealth();
51
+ if (!sources.length)
52
+ return null;
53
+ return (_jsx("div", { className: className, title: sources.map(sourceLine).join(' · '), children: sources.map((s, i) => (_jsxs("span", { className: "inline-flex items-center gap-1", children: [i > 0 && _jsx("span", { className: "mx-1 opacity-50", children: "\u00B7" }), _jsx("span", { "aria-hidden": true, className: s.state === 'ok'
54
+ ? 'size-1.5 rounded-full bg-gain'
55
+ : s.state === 'failed'
56
+ ? 'size-1.5 rounded-full bg-destructive'
57
+ : 'size-1.5 rounded-full bg-muted-foreground/40' }), _jsx("span", { children: s.name }), _jsx("span", { className: "text-muted-foreground", children: detailOf(s, '') })] }, s.id))) }));
58
+ }
@@ -0,0 +1,23 @@
1
+ export interface CardOnScreen {
2
+ id: string;
3
+ title: string;
4
+ /** `view` for a table card, else the widget's kind (chart, tiles, ...). */
5
+ kind: string;
6
+ }
7
+ export declare const cardOf: (el: HTMLElement) => CardOnScreen;
8
+ /** The cards on the page now, top to bottom. */
9
+ export declare const cardsOnScreen: () => CardOnScreen[];
10
+ /** Where a target is on screen now: a card on the page, a sidebar page, or neither. */
11
+ export declare function onScreen(target: string | null | undefined): 'card' | 'nav' | null;
12
+ /**
13
+ * Lights one target (`card:<id>` or an app path) and dims the page around it; null clears. The
14
+ * target scrolls into view only after a short hover.
15
+ */
16
+ export declare function spotlight(target: string | null | undefined, name?: string): void;
17
+ /**
18
+ * Point at the page: the cards dim and the one under the cursor (or finger) lights up with its
19
+ * name; a click picks it, Esc or a click off any card cancels.
20
+ */
21
+ export declare function pointAtCard(): Promise<CardOnScreen | null>;
22
+ /** A card's style (`kind:style`) as a person says it: a table, a donut, a page's own card. */
23
+ export declare function kindName(styleId: string): string;