@columenlabs/lumen-ui 0.0.0-stage → 0.1.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 +434 -0
  32. package/dist/details-table.d.ts +20 -0
  33. package/dist/details-table.js +17 -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 +127 -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 +79 -0
  78. package/dist/lumen-drawer.d.ts +30 -0
  79. package/dist/lumen-drawer.js +29 -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 +554 -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 +121 -0
  90. package/dist/lumen-grid.d.ts +49 -0
  91. package/dist/lumen-grid.js +205 -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 +329 -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 +647 -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 +598 -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 +168 -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 +183 -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 +274 -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 +25 -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 +120 -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 +201 -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,275 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // LumenFinder (any app): one box that finds a page, a setting or anything an app adds, opened with ⌘K (Ctrl K)
3
+ // or the header's search button. A dialog on desktop, a bottom sheet on phone, around
4
+ // LumenSelectorList. The shell lists what it knows itself (the nav's pages grouped by the app's
5
+ // `pageTypes`, Settings, Recent, and Ask where an assistant is docked); an app adds its own rows from
6
+ // anywhere below the shell with `useLumenFinderRows`, reading what was typed with `useLumenFinder`
7
+ // (one app's symbols, another's accounts). Its chips are the app's
8
+ // (`LumenFinderConfig`, LumenShell's `finder` prop).
9
+ import { useRouter } from '@tanstack/react-router';
10
+ import { ClockIcon, FileTextIcon, LayersIcon, SearchIcon, SettingsIcon, SparklesIcon, XIcon, } from 'lucide-react';
11
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
12
+ import { useAssistant } from './assistant-context.js';
13
+ import { useAssistantScreen } from './assistant-screen.js';
14
+ import { useIsMobile } from './hooks/use-mobile.js';
15
+ import { IconButton } from './icon-button.js';
16
+ import { cn } from './lib/utils.js';
17
+ import { LumenSelectorList, fuzzyScore, } from './lumen-selector.js';
18
+ import { Button } from './ui/button.js';
19
+ import { Dialog, DialogContent, DialogTitle } from './ui/dialog.js';
20
+ import { Kbd } from './ui/kbd.js';
21
+ import { Sheet, SheetContent, SheetTitle } from './ui/sheet.js';
22
+ /** A row shown under every chip (the assistant's Ask row). */
23
+ const EVERY_SCOPE = '*';
24
+ const DEFAULT_SCOPES = ['Pages', 'Settings'];
25
+ const PAGE = { group: 'Pages', scope: 'Pages', icon: FileTextIcon };
26
+ const WORKSPACE = { group: 'Workspaces', scope: 'Pages', icon: LayersIcon };
27
+ const StateCtx = createContext(null);
28
+ const ApiCtx = createContext(null);
29
+ /** Whether Find is open and what is typed in it; null outside the shell. */
30
+ export const useLumenFinder = () => useContext(StateCtx);
31
+ /** Opens Find; null outside the shell. */
32
+ export const useOpenLumenFinder = () => useContext(ApiCtx)?.setOpen ?? null;
33
+ /** Adds rows to Find while the caller is mounted. Pass a memoized list (or null for none). */
34
+ export function useLumenFinderRows(rows) {
35
+ const register = useContext(ApiCtx)?.register;
36
+ const key = useId();
37
+ useEffect(() => {
38
+ if (!register)
39
+ return;
40
+ register(key, rows);
41
+ return () => register(key, null);
42
+ }, [register, key, rows]);
43
+ }
44
+ const NO_TYPES = {};
45
+ /** The groups with nothing typed: Recent, Pages, Workspaces, the page types' groups, then the rest. */
46
+ function groupOrder(pageTypes) {
47
+ return [
48
+ ...new Set(['Recent', 'Pages', 'Workspaces', ...Object.values(pageTypes).map((t) => t.group)]),
49
+ ];
50
+ }
51
+ const recentKey = (app) => `lumen-finder-recent:${app}`;
52
+ function readRecent(app) {
53
+ try {
54
+ const v = JSON.parse(localStorage.getItem(recentKey(app)) ?? '[]');
55
+ return Array.isArray(v) ? v : [];
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
61
+ function remember(app, r) {
62
+ if (!r.href)
63
+ return;
64
+ const next = [
65
+ { label: r.label, detail: r.detail, scope: r.scope, href: r.href },
66
+ ...readRecent(app).filter((x) => x.href !== r.href),
67
+ ].slice(0, 5);
68
+ try {
69
+ localStorage.setItem(recentKey(app), JSON.stringify(next));
70
+ }
71
+ catch {
72
+ // A private window refuses storage; Recent stays empty.
73
+ }
74
+ }
75
+ const scoreOf = (r, q) => [r.label, r.detail ?? '', ...(r.keywords ?? [])].reduce((best, s) => {
76
+ const v = fuzzyScore(q, s);
77
+ return v === null ? best : best === null ? v : Math.max(best, v);
78
+ }, null);
79
+ /** What shows: rows in the scope, matched and ranked against the query, Settings then Ask last. */
80
+ function arrange(rows, q, scope, order, lastGroup) {
81
+ const query = q.trim().toLowerCase();
82
+ const groups = new Map();
83
+ for (const r of rows) {
84
+ if (scope !== 'All' && r.scope !== scope && r.scope !== EVERY_SCOPE)
85
+ continue;
86
+ if (query && r.group === 'Recent')
87
+ continue;
88
+ const v = r.pinned ? 2000 : query ? scoreOf(r, query) : 0;
89
+ if (v === null)
90
+ continue;
91
+ const g = groups.get(r.group) ?? { rows: [], best: Number.NEGATIVE_INFINITY };
92
+ g.rows.push({ r, v });
93
+ g.best = Math.max(g.best, v);
94
+ groups.set(r.group, g);
95
+ }
96
+ const last = (label) => label === lastGroup ? 2 : label === 'Settings' && !query ? 1 : 0;
97
+ const rank = (label) => {
98
+ const i = order.indexOf(label);
99
+ return i < 0 ? order.length : i;
100
+ };
101
+ return [...groups]
102
+ .sort(([a, ga], [b, gb]) => last(a) - last(b) || (query ? gb.best - ga.best : rank(a) - rank(b)))
103
+ .map(([label, g]) => ({
104
+ label,
105
+ options: (query ? g.rows.sort((x, y) => y.v - x.v) : g.rows).map((x) => x.r),
106
+ }));
107
+ }
108
+ /** The shell's own rows: Recent, the nav's pages, Settings sections and Ask the assistant. */
109
+ function useShellRows(open, query, settings, openSettings, app, pageTypes, settingsScope) {
110
+ const screen = useAssistantScreen();
111
+ const assistant = useAssistant();
112
+ // The nav registers its pages with the screen map; Find reads it, and Recent, each time it opens.
113
+ const pages = useMemo(() => (open ? (screen?.map().pages ?? []) : []), [open, screen]);
114
+ const recent = useMemo(() => (open ? readRecent(app) : []), [open, app]);
115
+ const base = useMemo(() => {
116
+ const rows = recent.map((r) => ({
117
+ ...r,
118
+ value: `recent:${r.href}`,
119
+ group: 'Recent',
120
+ icon: ClockIcon,
121
+ }));
122
+ for (const p of pages) {
123
+ // A workspace page is its own kind, whatever the nav calls it.
124
+ const type = p.path.startsWith('/workspaces/') ? 'workspace' : p.type;
125
+ const t = (type && pageTypes[type]) || (type === 'workspace' ? WORKSPACE : PAGE);
126
+ const where = p.parent ?? p.section;
127
+ rows.push({
128
+ value: `page:${p.path}`,
129
+ label: p.title,
130
+ group: t.group,
131
+ scope: t.scope,
132
+ icon: t.icon,
133
+ detail: where === t.group ? undefined : where,
134
+ href: p.path,
135
+ });
136
+ }
137
+ for (const s of settings)
138
+ rows.push({
139
+ value: `setting:${s.id}`,
140
+ label: `Settings ▸ ${s.title}`,
141
+ group: 'Settings',
142
+ scope: settingsScope,
143
+ icon: SettingsIcon,
144
+ keywords: [s.title],
145
+ run: () => openSettings(s.id),
146
+ });
147
+ return rows;
148
+ }, [pages, recent, settings, openSettings, pageTypes, settingsScope]);
149
+ const q = query.trim();
150
+ return useMemo(() => assistant?.ready && q
151
+ ? [
152
+ ...base,
153
+ {
154
+ value: 'assistant',
155
+ label: `Ask ${assistant.name} “${q}”`,
156
+ group: assistant.name,
157
+ scope: EVERY_SCOPE,
158
+ icon: SparklesIcon,
159
+ pinned: true,
160
+ run: () => assistant.draft(q, []),
161
+ },
162
+ ]
163
+ : base, [base, assistant, q]);
164
+ }
165
+ /** Find for the shell: ⌘K, the rows apps add and the dialog. LumenShell mounts it. */
166
+ export function LumenFinder({ app, settings, openSettings, config, children, }) {
167
+ const pageTypes = config?.pageTypes ?? NO_TYPES;
168
+ const settingsScope = config?.settingsScope ?? 'Settings';
169
+ const scopes = config?.scopes ?? DEFAULT_SCOPES;
170
+ const order = useMemo(() => groupOrder(pageTypes), [pageTypes]);
171
+ const lastGroup = useAssistant()?.name ?? null;
172
+ const [open, setOpenState] = useState(false);
173
+ const [query, setQuery] = useState('');
174
+ const [scope, setScope] = useState('All');
175
+ const [sources, setSources] = useState(new Map());
176
+ const setOpen = useCallback((o) => {
177
+ if (o) {
178
+ setQuery('');
179
+ setScope('All');
180
+ }
181
+ setOpenState(o);
182
+ }, []);
183
+ const register = useCallback((key, rows) => {
184
+ setSources((m) => {
185
+ if (m.get(key) === rows || (!rows && !m.has(key)))
186
+ return m;
187
+ const next = new Map(m);
188
+ if (rows)
189
+ next.set(key, rows);
190
+ else
191
+ next.delete(key);
192
+ return next;
193
+ });
194
+ }, []);
195
+ useEffect(() => {
196
+ const onKey = (e) => {
197
+ if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key.toLowerCase() === 'k') {
198
+ e.preventDefault();
199
+ setOpenState((o) => {
200
+ if (!o) {
201
+ setQuery('');
202
+ setScope('All');
203
+ }
204
+ return !o;
205
+ });
206
+ }
207
+ };
208
+ window.addEventListener('keydown', onKey);
209
+ return () => window.removeEventListener('keydown', onKey);
210
+ }, []);
211
+ const api = useMemo(() => ({ setOpen, register }), [setOpen, register]);
212
+ const state = useMemo(() => ({ open, query }), [open, query]);
213
+ const own = useShellRows(open, query, settings, openSettings, app, pageTypes, settingsScope);
214
+ const groups = useMemo(() => open
215
+ ? arrange([...own, ...[...sources.values()].flat()], query, scope, order, lastGroup)
216
+ : [], [open, own, sources, query, scope, order, lastGroup]);
217
+ return (_jsx(ApiCtx.Provider, { value: api, children: _jsxs(StateCtx.Provider, { value: state, children: [children, _jsx(FinderDialog, { app: app, open: open, onOpenChange: setOpen, query: query, onQuery: setQuery, scope: scope, onScope: setScope, scopes: scopes, placeholder: config?.placeholder ?? 'Find a page or setting', groups: groups })] }) }));
218
+ }
219
+ function FinderDialog({ app, open, onOpenChange, query, onQuery, scope, onScope, scopes, placeholder, groups, }) {
220
+ const mobile = useIsMobile();
221
+ const router = useRouter();
222
+ const list = useRef(null);
223
+ const input = useRef(null);
224
+ // Focus without scrolling: on a phone iOS pans the page to a focused field as the keyboard opens,
225
+ // which carried Find's input off the top of the screen.
226
+ useEffect(() => {
227
+ if (open)
228
+ input.current?.focus({ preventScroll: true });
229
+ }, [open]);
230
+ // ⌘↵ (Ctrl ↵) or a ⌘-click opens the pick in a new tab.
231
+ const newTab = useRef(false);
232
+ const pick = (o) => {
233
+ const r = o;
234
+ const tab = newTab.current;
235
+ newTab.current = false;
236
+ remember(app, r);
237
+ onOpenChange(false);
238
+ if (r.run)
239
+ return r.run();
240
+ if (!r.href)
241
+ return;
242
+ const outside = /^https?:/.test(r.href);
243
+ if (tab)
244
+ window.open(outside ? r.href : router.history.createHref(r.href), '_blank');
245
+ else if (outside)
246
+ window.location.assign(r.href);
247
+ else
248
+ void router.navigate({ href: r.href });
249
+ };
250
+ const body = (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", onMouseDownCapture: (e) => {
251
+ newTab.current = e.metaKey || e.ctrlKey;
252
+ },
253
+ // A swipe through the list on a phone drops the keyboard, so the rows it hid show.
254
+ onTouchMove: (e) => {
255
+ const typing = document.activeElement;
256
+ if (typing instanceof HTMLInputElement && !typing.contains(e.target))
257
+ typing.blur();
258
+ }, children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 max-md:px-4", children: [_jsx(SearchIcon, { className: "size-4 shrink-0 text-muted-foreground" }), _jsx("input", { ref: input, value: query, onChange: (e) => onQuery(e.target.value), onKeyDown: (e) => {
259
+ if (e.key === 'Escape')
260
+ return;
261
+ newTab.current = e.key === 'Enter' && (e.metaKey || e.ctrlKey);
262
+ list.current?.key(e);
263
+ }, "aria-label": "Find", placeholder: placeholder, className: "h-11 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground max-md:text-base" }), mobile ? (_jsx(IconButton, { icon: XIcon, label: "Close", onClick: () => onOpenChange(false) })) : (_jsx(Kbd, { children: "esc" }))] }), _jsx("div", { className: "flex gap-1 overflow-x-auto border-b px-2 py-1.5 max-md:px-3", children: ['All', ...scopes].map((s) => (_jsx(Button, { size: "xs", variant: "ghost", "aria-pressed": s === scope, className: cn('text-muted-foreground', s === scope && 'bg-muted text-foreground'), onClick: () => onScope(s), children: s }, s))) }), _jsx(LumenSelectorList, { groups: groups, query: "", onPick: pick, handle: list, label: "Find", empty: query.trim() ? 'Nothing here by that name' : 'Nothing to find yet', className: "max-h-none min-h-0 flex-1 rounded-none bg-transparent shadow-none ring-0" }), !mobile && (_jsxs("div", { className: "flex items-center gap-3 border-t px-3 py-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Kbd, { children: "\u2191" }), _jsx(Kbd, { children: "\u2193" }), " move"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Kbd, { children: "\u21B5" }), " open"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Kbd, { children: "\u2318\u21B5" }), " new tab"] })] }))] }));
264
+ if (mobile)
265
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "top", showCloseButton: false, className: "flex h-dvh flex-col gap-0 p-0 pt-[env(safe-area-inset-top)]", children: [_jsx(SheetTitle, { className: "sr-only", children: "Find" }), open && body] }) }));
266
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { showCloseButton: false, className: "top-[12vh] flex h-[76vh] max-h-140 translate-y-0 flex-col gap-0 overflow-hidden p-0 sm:max-w-2xl", children: [_jsx(DialogTitle, { className: "sr-only", children: "Find" }), open && body] }) }));
267
+ }
268
+ const SHORTCUT = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘K' : 'Ctrl K';
269
+ /** The header's Find button. */
270
+ export function LumenFinderButton() {
271
+ const open = useOpenLumenFinder();
272
+ if (!open)
273
+ return null;
274
+ return _jsx(IconButton, { icon: SearchIcon, label: `Find (${SHORTCUT})`, onClick: () => open(true) });
275
+ }
@@ -0,0 +1,34 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type LumenOption } from './lumen-selector.js';
3
+ /** A text field's value is a string; a `multi` field's is a list. */
4
+ export type LumenFormValue = string | string[];
5
+ export type LumenFormValues = Record<string, LumenFormValue>;
6
+ export interface LumenFormField {
7
+ id: string;
8
+ label: string;
9
+ /** `text` by default. `secret` hides what is typed; `select` and `multi` are LumenSelector. */
10
+ kind?: 'text' | 'secret' | 'number' | 'date' | 'textarea' | 'select' | 'multi';
11
+ required?: boolean;
12
+ /** A line under the field. */
13
+ hint?: ReactNode;
14
+ placeholder?: string;
15
+ /** `select` and `multi`. */
16
+ options?: readonly LumenOption[];
17
+ /** A message when the value is wrong, else nothing: `v => v.startsWith('https://') || 'Must start with https://'`. */
18
+ check?: (value: LumenFormValue, values: LumenFormValues) => string | true | undefined | null;
19
+ /** `secret`: the saved value, masked (`••••3f9a`). An empty box keeps it. */
20
+ saved?: string;
21
+ }
22
+ /** What stands in the way of a save, one message per field. */
23
+ export declare function lumenFormIssues(fields: readonly LumenFormField[], values: LumenFormValues): Record<string, string>;
24
+ export declare function LumenForm({ fields, initial, onSave, onCancel, saveLabel, busy, footer, }: {
25
+ fields: readonly LumenFormField[];
26
+ initial?: LumenFormValues;
27
+ /** Runs only when every check passes. A secret left empty is left out. */
28
+ onSave: (values: LumenFormValues) => void;
29
+ onCancel?: () => void;
30
+ saveLabel?: string;
31
+ busy?: boolean;
32
+ /** Beside Save, on the left (a Test button, a note). */
33
+ footer?: ReactNode;
34
+ }): import("react").JSX.Element;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // LumenForm:
3
+ // a list of fields in, a laid-out form out. Label, input, hint and error sit the same way in every
4
+ // app; each field's own check runs as the person types, and Save is LumenShell's SaveButton, which
5
+ // lists what stands in the way and takes the person to it. A secret already saved shows masked and
6
+ // is kept while its box stays empty. LumenForm only puts the kit's parts together.
7
+ import { useId, useState } from 'react';
8
+ import { SaveButton } from './form-issues.js';
9
+ import { LumenSelector } from './lumen-selector.js';
10
+ import { Button } from './ui/button.js';
11
+ import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from './ui/field.js';
12
+ import { Input } from './ui/input.js';
13
+ import { Textarea } from './ui/textarea.js';
14
+ const isEmpty = (v) => v === undefined || (Array.isArray(v) ? v.length === 0 : v.trim() === '');
15
+ /** What stands in the way of a save, one message per field. */
16
+ export function lumenFormIssues(fields, values) {
17
+ const out = {};
18
+ for (const f of fields) {
19
+ const v = values[f.id] ?? (f.kind === 'multi' ? [] : '');
20
+ if (isEmpty(v)) {
21
+ if (f.required && !(f.kind === 'secret' && f.saved))
22
+ out[f.id] = `${f.label} is needed`;
23
+ continue;
24
+ }
25
+ const r = f.check?.(v, values);
26
+ if (typeof r === 'string')
27
+ out[f.id] = r;
28
+ }
29
+ return out;
30
+ }
31
+ export function LumenForm({ fields, initial, onSave, onCancel, saveLabel = 'Save', busy = false, footer, }) {
32
+ const base = useId();
33
+ const [values, setValues] = useState(initial ?? {});
34
+ // A field shows its error once the person has left it, or tried to save.
35
+ const [touched, setTouched] = useState(new Set());
36
+ const errors = lumenFormIssues(fields, values);
37
+ const domId = (id) => `${base}-${id}`;
38
+ const issues = fields
39
+ .filter((f) => errors[f.id])
40
+ .map((f) => ({ where: f.label, message: errors[f.id] ?? '', target: domId(f.id) }));
41
+ const set = (id, v) => setValues((s) => ({ ...s, [id]: v }));
42
+ const touchAll = () => setTouched(new Set(fields.map((f) => f.id)));
43
+ const touch = (id) => setTouched((s) => (s.has(id) ? s : new Set(s).add(id)));
44
+ const save = () => onSave(Object.fromEntries(Object.entries(values).filter(([k, v]) => !(fields.find((f) => f.id === k)?.kind === 'secret' && isEmpty(v)))));
45
+ const input = (f) => {
46
+ const id = domId(f.id);
47
+ const invalid = touched.has(f.id) && !!errors[f.id];
48
+ const text = values[f.id] ?? '';
49
+ switch (f.kind) {
50
+ case 'select':
51
+ return (_jsx(LumenSelector, { id: id, label: f.label, options: f.options ?? [], placeholder: f.placeholder, value: text, onChange: (v) => {
52
+ set(f.id, v);
53
+ touch(f.id);
54
+ } }));
55
+ case 'multi':
56
+ return (_jsx(LumenSelector, { id: id, label: f.label, multiple: true, options: f.options ?? [], placeholder: f.placeholder, value: values[f.id] ?? [], onChange: (v) => {
57
+ set(f.id, v);
58
+ touch(f.id);
59
+ } }));
60
+ case 'textarea':
61
+ return (_jsx(Textarea, { id: id, value: text, placeholder: f.placeholder, "aria-invalid": invalid || undefined, onChange: (e) => set(f.id, e.target.value), onBlur: () => touch(f.id) }));
62
+ default:
63
+ return (_jsx(Input, { id: id, type: f.kind === 'secret'
64
+ ? 'password'
65
+ : f.kind === 'number'
66
+ ? 'number'
67
+ : f.kind === 'date'
68
+ ? 'date'
69
+ : 'text', value: text, placeholder: f.kind === 'secret' && f.saved ? f.saved : f.placeholder, autoComplete: f.kind === 'secret' ? 'off' : undefined, className: f.kind === 'date' ? 'w-44' : undefined, "aria-invalid": invalid || undefined, onChange: (e) => set(f.id, e.target.value), onBlur: () => touch(f.id) }));
70
+ }
71
+ };
72
+ return (_jsxs("form", { className: "flex flex-col gap-4", onSubmit: (e) => {
73
+ e.preventDefault();
74
+ touchAll();
75
+ if (!issues.length)
76
+ save();
77
+ }, children: [_jsx(FieldGroup, { children: fields.map((f) => {
78
+ const invalid = touched.has(f.id) && !!errors[f.id];
79
+ return (_jsxs(Field, { "data-invalid": invalid || undefined, children: [_jsxs(FieldLabel, { htmlFor: domId(f.id), children: [f.label, f.required && _jsx("span", { className: "font-normal text-muted-foreground", children: "required" })] }), input(f), f.hint && !invalid && _jsx(FieldDescription, { children: f.hint }), invalid && _jsx(FieldError, { children: errors[f.id] })] }, f.id));
80
+ }) }), _jsxs("div", { className: "flex items-center justify-end gap-2 border-t pt-3", children: [footer, _jsx("div", { className: "flex-1" }), onCancel && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onCancel, children: "Cancel" })), _jsx("span", { className: "contents", onClickCapture: touchAll, children: _jsx(SaveButton, { issues: issues, busy: busy, label: saveLabel, onSave: save }) })] })] }));
81
+ }
@@ -0,0 +1,9 @@
1
+ export declare function LumenGeoMap({ values, label, unit, height, }: {
2
+ /** The number for each country, by ISO alpha-2 code; countries left out draw empty. */
3
+ values: Record<string, number>;
4
+ /** What the map shows, for screen readers. */
5
+ label: string;
6
+ /** What the number counts ("visits"), shown in the tooltip after it. */
7
+ unit?: string;
8
+ height?: number;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // LumenGeoMap: a world map shaded by a number per country (visits, people, revenue), drawn by
3
+ // ECharts' map chart over Natural Earth's 110m countries (world-atlas). Countries are keyed by
4
+ // ISO 3166-1 alpha-2 code ("US", "GB"), the code most sources report. The map and its outlines
5
+ // load in their own chunk the first time a page draws one.
6
+ import { useEffect, useMemo, useState } from 'react';
7
+ import { LumenEChart, baseOption, useChartTokens } from './lumen-echart.js';
8
+ const ANTARCTICA = '010';
9
+ /** Russia and Fiji, by numeric code. */
10
+ const ACROSS_180 = new Set(['643', '242']);
11
+ /** Points this far west belong to the shape's eastern end. */
12
+ const FAR_WEST = -150;
13
+ const FULL_TURN = 360;
14
+ /** The map's height when the caller gives none, as LumenEChart's. */
15
+ const HEIGHT = 260;
16
+ /** Country borders, thin enough to read small countries at 110m. */
17
+ const BORDER = 0.5;
18
+ /** Moves every far-west longitude in nested GeoJSON coordinates one turn east, in place. */
19
+ function unwrap(c) {
20
+ if (!Array.isArray(c))
21
+ return;
22
+ if (typeof c[0] === 'number') {
23
+ if (c[0] < FAR_WEST)
24
+ c[0] += FULL_TURN;
25
+ return;
26
+ }
27
+ for (const inner of c)
28
+ unwrap(inner);
29
+ }
30
+ /** Registers the map chart and the world outlines with ECharts once. */
31
+ let loading = null;
32
+ function loadWorld() {
33
+ loading ??= Promise.all([
34
+ import('echarts/core'),
35
+ import('echarts/charts'),
36
+ import('topojson-client'),
37
+ import('world-atlas/countries-110m.json'),
38
+ import('i18n-iso-countries'),
39
+ ]).then(([core, charts, topo, atlas, iso]) => {
40
+ core.use([charts.MapChart]);
41
+ const world = atlas.default;
42
+ const fc = topo.feature(world, world.objects.countries);
43
+ // Antarctica takes a third of the height and holds no one; Russia and Fiji cross the date
44
+ // line, so their far-east points move past 180° to keep each shape in one piece.
45
+ fc.features = fc.features.filter((f) => f.id !== ANTARCTICA);
46
+ for (const f of fc.features)
47
+ if (f.id && ACROSS_180.has(f.id))
48
+ unwrap(f.geometry.coordinates);
49
+ // ECharts matches data to a feature by a property, and world-atlas keeps the numeric code
50
+ // in the feature's own id, so copy it into the properties.
51
+ const names = new Map();
52
+ for (const f of fc.features) {
53
+ f.properties.key = f.id ?? f.properties.name;
54
+ names.set(f.properties.key, f.properties.name);
55
+ }
56
+ core.registerMap('world', fc);
57
+ return {
58
+ key: (alpha2) => iso.default.alpha2ToNumeric(alpha2),
59
+ name: (key) => names.get(key) ?? key,
60
+ };
61
+ });
62
+ return loading;
63
+ }
64
+ export function LumenGeoMap({ values, label, unit, height = HEIGHT, }) {
65
+ const t = useChartTokens();
66
+ const [world, setWorld] = useState(null);
67
+ useEffect(() => {
68
+ let alive = true;
69
+ loadWorld().then((w) => alive && setWorld(w));
70
+ return () => {
71
+ alive = false;
72
+ };
73
+ }, []);
74
+ const option = useMemo(() => {
75
+ if (!world)
76
+ return null;
77
+ const data = Object.entries(values).flatMap(([code, value]) => {
78
+ const id = world.key(code.toUpperCase());
79
+ return id ? [{ name: id, value }] : [];
80
+ });
81
+ const max = Math.max(1, ...data.map((d) => d.value));
82
+ const base = baseOption(t);
83
+ return {
84
+ ...base,
85
+ tooltip: {
86
+ ...base.tooltip,
87
+ trigger: 'item',
88
+ formatter: (p) => {
89
+ const value = Number.isFinite(p.value) ? p.value : 0;
90
+ return `${world.name(p.name)}: ${value.toLocaleString('en-US')}${unit ? ` ${unit}` : ''}`;
91
+ },
92
+ },
93
+ visualMap: {
94
+ show: false,
95
+ min: 0,
96
+ max,
97
+ inRange: { color: [t.border, t.primary] },
98
+ },
99
+ series: [
100
+ {
101
+ type: 'map',
102
+ map: 'world',
103
+ nameProperty: 'key',
104
+ roam: false,
105
+ left: 0,
106
+ right: 0,
107
+ top: 0,
108
+ bottom: 0,
109
+ data,
110
+ itemStyle: { areaColor: t.border, borderColor: t.card, borderWidth: BORDER },
111
+ emphasis: { label: { show: false }, itemStyle: { areaColor: t.series[5] ?? t.primary } },
112
+ select: { disabled: true },
113
+ },
114
+ ],
115
+ };
116
+ }, [world, values, unit, t]);
117
+ if (!option) {
118
+ return (_jsx("div", { role: "img", "aria-label": label, className: "w-full animate-pulse rounded-md bg-muted/40", style: { height } }));
119
+ }
120
+ return _jsx(LumenEChart, { label: label, option: option, height: height });
121
+ }
@@ -0,0 +1,49 @@
1
+ import { type ViewCardPlace } from '@columenlabs/lumen-ui-types/views';
2
+ import { type ReactNode } from 'react';
3
+ import { type LayoutItem } from 'react-grid-layout';
4
+ /** The gap between cards, the shell's section gap. */
5
+ export declare const GRID_GAP = 12;
6
+ /** One card on the grid. */
7
+ export interface GridCard {
8
+ id: string;
9
+ /** Columns of 6 it spans on a desktop. */
10
+ w: number;
11
+ /** Rows of 8px; left out, as tall as it draws. */
12
+ h?: number;
13
+ x?: number;
14
+ y?: number;
15
+ /** Its own place on a phone. */
16
+ phone?: ViewCardPlace;
17
+ /** A full-width table: outside Edit layout it sits between the grid's runs, scrolling sideways. */
18
+ wide?: boolean;
19
+ /** A card the page builds in code: it is laid out but not moved or sized. */
20
+ locked?: boolean;
21
+ node: ReactNode;
22
+ }
23
+ /** Where a card was put while editing: its columns, row, width and, once resized, its height. */
24
+ export interface GridPlace {
25
+ id: string;
26
+ x: number;
27
+ y: number;
28
+ w: number;
29
+ h?: number;
30
+ }
31
+ export declare const useGridCell: () => {
32
+ fixed: boolean;
33
+ } | null;
34
+ /**
35
+ * The grid's layout: the page's lead cards, then the saved ones where they were placed, then the
36
+ * page's other cards, compacted upward; cards of measured height starting on the same row take the
37
+ * tallest's height.
38
+ */
39
+ export declare function gridLayout(groups: GridCard[][], heights: Record<string, number>, phone: boolean): LayoutItem[];
40
+ /** The page's cards on the grid; `onPlace` makes them movable and sizable (Edit layout). */
41
+ export declare function LumenGrid({ lead, cards, tail, editing, onPlace, }: {
42
+ /** The page's own cards before the saved ones, and after them. */
43
+ lead?: GridCard[];
44
+ cards: GridCard[];
45
+ tail?: GridCard[];
46
+ editing: boolean;
47
+ /** Told every card's place after a move or resize, and whether it was on the phone's grid. */
48
+ onPlace?: (places: GridPlace[], phone: boolean) => void;
49
+ }): import("react").JSX.Element;