@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,57 @@
1
+ // A table that scrolls sideways in its own box (a card narrower than the page, or a view outside
2
+ // a LumenLayout) can't pin its header with sticky: CSS makes any sideways scroller a scroller up and
3
+ // down too, so sticky pins to that box, which never scrolls up and down. This slides the header
4
+ // down with the page's scroller instead, under the page's sticky tabs (--pin-top), until the
5
+ // table's bottom ("table headers are not sticky like they were").
6
+ import { useEffect } from 'react';
7
+ /** The nearest ancestor that scrolls up and down; null for the window. A sideways scroller around
8
+ * the table (overflow-x makes overflow-y auto too) whose rows all fit is passed over: taking it
9
+ * slid the header over the first row (the Tune card, 2026-10-15). */
10
+ function scrollParentOf(el) {
11
+ for (let e = el.parentElement; e; e = e.parentElement) {
12
+ const y = getComputedStyle(e).overflowY;
13
+ if ((y === 'auto' || y === 'scroll') && e.scrollHeight > e.clientHeight)
14
+ return e;
15
+ }
16
+ return null;
17
+ }
18
+ export function useFloatingHead(box, on) {
19
+ useEffect(() => {
20
+ const el = box.current;
21
+ if (!on || !el)
22
+ return;
23
+ const scroller = scrollParentOf(el);
24
+ const target = scroller ?? window;
25
+ let frame = 0;
26
+ const place = () => {
27
+ frame = 0;
28
+ const head = el.querySelector(':scope table > thead');
29
+ const table = head?.parentElement;
30
+ if (!head || !table)
31
+ return;
32
+ const top = scroller ? scroller.getBoundingClientRect().top : 0;
33
+ // The scroller's own --pin-top: the table's box sets its own to 0 for its sticky cells.
34
+ const pin = Number.parseFloat(getComputedStyle(scroller ?? document.documentElement).getPropertyValue('--pin-top')) || 0;
35
+ const rect = table.getBoundingClientRect();
36
+ const y = Math.max(0, Math.min(top + pin - rect.top, rect.height - head.offsetHeight));
37
+ head.style.position = 'relative';
38
+ head.style.zIndex = '20';
39
+ head.style.transform = y > 0 ? `translateY(${y}px)` : '';
40
+ };
41
+ const queue = () => {
42
+ if (!frame)
43
+ frame = requestAnimationFrame(place);
44
+ };
45
+ place();
46
+ target.addEventListener('scroll', queue, { passive: true });
47
+ const seen = new ResizeObserver(queue);
48
+ seen.observe(el);
49
+ if (scroller)
50
+ seen.observe(scroller);
51
+ return () => {
52
+ target.removeEventListener('scroll', queue);
53
+ seen.disconnect();
54
+ cancelAnimationFrame(frame);
55
+ };
56
+ }, [box, on]);
57
+ }
package/dist/fold.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /** Whether the header `key` is folded, and a setter that remembers it. */
2
+ export declare function useFolded(key: string): [boolean, (folded: boolean) => void];
3
+ /** Every folded key at once, for headers drawn in a loop (a map's groups), and the setter. */
4
+ export declare function useFolds(): [Set<string>, (key: string, folded: boolean) => void];
5
+ /** The page's route (`/market/$symbol`), so a fold on one page holds for all its params. */
6
+ export declare function usePageKey(): string;
7
+ /**
8
+ * The chevron after a foldable header's name, always shown: it alone folds the header
9
+ *, so a click elsewhere on a header never folds it.
10
+ */
11
+ export declare function FoldChevron({ folded, label, onToggle, className, }: {
12
+ folded: boolean;
13
+ label: string;
14
+ onToggle: () => void;
15
+ className?: string;
16
+ }): import("react").JSX.Element;
package/dist/fold.js ADDED
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Folded section headers (option B): a header someone folds stays folded in
3
+ // their browser. Sidebar sections fold across the app; cards and the groups inside them fold per
4
+ // page (the route, so every symbol page shares one choice). One localStorage list per app host;
5
+ // nothing is stored server-side, and a browser that refuses storage simply starts everything open.
6
+ import { useRouterState } from '@tanstack/react-router';
7
+ import { ChevronRightIcon } from 'lucide-react';
8
+ import { useCallback, useSyncExternalStore } from 'react';
9
+ import { cn } from './lib/utils.js';
10
+ import { Button } from './ui/button.js';
11
+ const KEY = 'edgeshell:folded';
12
+ const listeners = new Set();
13
+ let cache = null;
14
+ function read() {
15
+ if (cache)
16
+ return cache;
17
+ try {
18
+ const raw = JSON.parse(localStorage.getItem(KEY) ?? '[]');
19
+ cache = new Set(Array.isArray(raw) ? raw.filter((k) => typeof k === 'string') : []);
20
+ }
21
+ catch {
22
+ cache = new Set();
23
+ }
24
+ return cache;
25
+ }
26
+ function write(key, folded) {
27
+ const next = new Set(read());
28
+ if (folded)
29
+ next.add(key);
30
+ else
31
+ next.delete(key);
32
+ cache = next;
33
+ try {
34
+ localStorage.setItem(KEY, JSON.stringify([...next]));
35
+ }
36
+ catch {
37
+ // Storage refused (private window, blocked site data): the fold still holds until reload.
38
+ }
39
+ for (const l of listeners)
40
+ l();
41
+ }
42
+ const subscribe = (l) => {
43
+ listeners.add(l);
44
+ return () => listeners.delete(l);
45
+ };
46
+ /** Whether the header `key` is folded, and a setter that remembers it. */
47
+ export function useFolded(key) {
48
+ const folded = useSyncExternalStore(subscribe, () => read().has(key), () => false);
49
+ const set = useCallback((f) => write(key, f), [key]);
50
+ return [folded, set];
51
+ }
52
+ /** Every folded key at once, for headers drawn in a loop (a map's groups), and the setter. */
53
+ export function useFolds() {
54
+ const folds = useSyncExternalStore(subscribe, read, () => EMPTY);
55
+ return [folds, write];
56
+ }
57
+ const EMPTY = new Set();
58
+ /** The page's route (`/market/$symbol`), so a fold on one page holds for all its params. */
59
+ export function usePageKey() {
60
+ return useRouterState({
61
+ select: (s) => String(s.matches.at(-1)?.routeId ?? s.location.pathname),
62
+ });
63
+ }
64
+ /**
65
+ * The chevron after a foldable header's name, always shown: it alone folds the header
66
+ *, so a click elsewhere on a header never folds it.
67
+ */
68
+ export function FoldChevron({ folded, label, onToggle, className, }) {
69
+ return (_jsx(Button, { variant: "ghost", size: "icon-xs", "aria-expanded": !folded, "aria-label": `${folded ? 'Expand' : 'Collapse'} ${label}`, onClick: onToggle, className: cn('shrink-0 text-muted-foreground', className), children: _jsx(ChevronRightIcon, { className: cn('transition-transform', !folded && 'rotate-90') }) }));
70
+ }
@@ -0,0 +1,32 @@
1
+ import { type ComponentType } from 'react';
2
+ export interface FormIssue {
3
+ /** Where it is, in the form's words: `Formula value`, `Name`. */
4
+ where: string;
5
+ /** What to fix: `Unknown field shares_nonret`. */
6
+ message: string;
7
+ /** The DOM id of the field to take the person to. */
8
+ target?: string;
9
+ /** Opens what hides the field (a section, a tab, a sub-editor) before the jump. */
10
+ reveal?: () => void;
11
+ }
12
+ /** Opens what hides an issue, then scrolls to its field, focuses it and flashes it once. */
13
+ export declare function goToIssue(issue: FormIssue): void;
14
+ /**
15
+ * Save, and before it, while anything needs fixing, a chip with the count that lists each issue.
16
+ * Save stays tappable then and takes the person to the first issue instead of saving.
17
+ */
18
+ export declare function SaveButton({ issues, onSave, label, icon: Icon, busy, disabled, type, className, }: {
19
+ issues: FormIssue[];
20
+ onSave?: () => void;
21
+ label?: string;
22
+ icon?: ComponentType<{
23
+ className?: string;
24
+ }> | null;
25
+ /** Saving now: Save waits. */
26
+ busy?: boolean;
27
+ /** Nothing to save (no change): Save is greyed and needs no reason. */
28
+ disabled?: boolean;
29
+ /** `submit` inside a form: the form's own submit runs only when there are no issues. */
30
+ type?: 'button' | 'submit';
31
+ className?: string;
32
+ }): import("react").JSX.Element;
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Save with issues: a Save that cannot go yet says why. Beside it a chip
3
+ // counts what needs fixing and lists it; Save, and each line of the list, takes the person to the
4
+ // issue: the editor opens its section or tab (`reveal`), then the field scrolls into view, takes
5
+ // the cursor and flashes red once. Nothing saves while an issue stands. Every editor and form in
6
+ // the suite uses this in place of a silent disabled Save.
7
+ import { CheckIcon, TriangleAlertIcon } from 'lucide-react';
8
+ import { useState } from 'react';
9
+ import { cn } from './lib/utils.js';
10
+ import { Button } from './ui/button.js';
11
+ import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
12
+ /** Opens what hides an issue, then scrolls to its field, focuses it and flashes it once. */
13
+ export function goToIssue(issue) {
14
+ issue.reveal?.();
15
+ if (!issue.target)
16
+ return;
17
+ const id = issue.target;
18
+ // The field may mount only once its section opens: look for it for up to a second.
19
+ let tries = 0;
20
+ const find = () => {
21
+ const el = document.getElementById(id);
22
+ if (!el) {
23
+ if (++tries < 20)
24
+ setTimeout(find, 50);
25
+ return;
26
+ }
27
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
28
+ el.focus({ preventScroll: true });
29
+ delete el.dataset.issueFlash;
30
+ // A frame between removing and setting restarts the animation on a second jump.
31
+ requestAnimationFrame(() => {
32
+ el.dataset.issueFlash = '';
33
+ setTimeout(() => delete el.dataset.issueFlash, 1200);
34
+ });
35
+ };
36
+ requestAnimationFrame(find);
37
+ }
38
+ /**
39
+ * Save, and before it, while anything needs fixing, a chip with the count that lists each issue.
40
+ * Save stays tappable then and takes the person to the first issue instead of saving.
41
+ */
42
+ export function SaveButton({ issues, onSave, label = 'Save', icon: Icon = CheckIcon, busy = false, disabled = false, type = 'button', className, }) {
43
+ const [open, setOpen] = useState(false);
44
+ const blocked = issues.length > 0;
45
+ const first = issues[0];
46
+ const go = (issue) => {
47
+ setOpen(false);
48
+ goToIssue(issue);
49
+ };
50
+ return (_jsxs(_Fragment, { children: [blocked && (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", children: [_jsx(TriangleAlertIcon, {}), issues.length, " to fix"] }) }), _jsxs(PopoverContent, { align: "end", className: "w-80 gap-1 p-1", children: [_jsxs("p", { className: "px-2 pt-1 text-xs text-muted-foreground", children: ["Fix ", issues.length === 1 ? 'this' : 'these', " to save. Tap one to go to it."] }), issues.map((issue, i) => (_jsxs(Button
51
+ // biome-ignore lint/suspicious/noArrayIndexKey: two issues can share a place and message.
52
+ , { type: "button", variant: "ghost", size: "sm", className: "h-auto w-full flex-col items-start gap-0 px-2 py-1.5 text-left font-normal whitespace-normal", onClick: () => go(issue), children: [_jsx("span", { className: "text-xs font-medium", children: issue.where }), _jsx("span", { className: "text-xs text-destructive", children: issue.message })] }, i)))] })] })), _jsxs(Button, { type: blocked ? 'button' : type, size: "sm", disabled: disabled || busy, "aria-disabled": blocked || undefined, className: cn(blocked && 'opacity-50', className), onClick: (e) => {
53
+ if (first) {
54
+ e.preventDefault();
55
+ go(first);
56
+ }
57
+ else if (type === 'button')
58
+ onSave?.();
59
+ }, children: [Icon && _jsx(Icon, {}), label] })] }));
60
+ }
@@ -0,0 +1,17 @@
1
+ import type { LumenOption } from './lumen-selector.js';
2
+ export declare function FormulaInput({ id, value, onChange, fields, invalid, placeholder, }: {
3
+ id?: string;
4
+ value: string;
5
+ onChange: (expr: string) => void;
6
+ /** What it can read: `value` the name, `label` what people call it, `group` its source. */
7
+ fields: LumenOption[];
8
+ invalid?: boolean;
9
+ placeholder?: string;
10
+ }): import("react").JSX.Element;
11
+ /**
12
+ * For an `unknown field X` issue: the field X most likely means, by label first (Price is
13
+ * price), then by the closest key or label.
14
+ */
15
+ export declare function guessField(name: string, fields: LumenOption[]): LumenOption | null;
16
+ /** `expr` with every whole name `from` swapped for `to` (never part of a longer name). */
17
+ export declare const swapWord: (expr: string, from: string, to: string) => string;
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { renameFormula } from '@columenlabs/lumen-ui-types/views';
3
+ // Formula box with autocomplete: a formula reads fields by name, where they
4
+ // come from and their key (`symbol.price`, `frame.earnings_next_week.eps`, a formula's bare key),
5
+ // but people know fields by their labels (Price). Typing a word opens a LumenSelectorList under
6
+ // the box that matches labels and names fuzzily (`prc` finds Price, `symbol.` lists the Symbol
7
+ // fields); Tab or Enter puts the name in. It replaces the wall of key chips under the box.
8
+ import { useLayoutEffect, useMemo, useRef, useState } from 'react';
9
+ import { LumenSelectorList, fuzzyScore } from './lumen-selector.js';
10
+ import { Textarea } from './ui/textarea.js';
11
+ /** The roll-ups a formula can call, offered beside the fields. */
12
+ const FUNCTIONS = [
13
+ {
14
+ value: 'group_sum("")',
15
+ label: 'group_sum',
16
+ detail: 'a field summed over its group',
17
+ group: 'Functions',
18
+ },
19
+ {
20
+ value: 'total_sum("")',
21
+ label: 'total_sum',
22
+ detail: 'a field summed over every row',
23
+ group: 'Functions',
24
+ },
25
+ ];
26
+ /** The name the caret is in (dots included), from its start to the caret. */
27
+ function wordAt(text, caret) {
28
+ const before = text.slice(0, caret);
29
+ const m = /[A-Za-z_][A-Za-z0-9_.]*$/.exec(before);
30
+ return m ? { start: caret - m[0].length, word: m[0] } : { start: caret, word: '' };
31
+ }
32
+ /** Does the name at `start` run on past the caret? Its whole length, for replacing it. */
33
+ function wordEnd(text, caret) {
34
+ const m = /^[A-Za-z0-9_.]*/.exec(text.slice(caret));
35
+ return caret + (m?.[0].length ?? 0);
36
+ }
37
+ export function FormulaInput({ id, value, onChange, fields, invalid, placeholder, }) {
38
+ const box = useRef(null);
39
+ const list = useRef(null);
40
+ const [caret, setCaret] = useState(null);
41
+ const [closedAt, setClosedAt] = useState(null);
42
+ const moveTo = useRef(null);
43
+ const options = useMemo(() => [...fields.map((f) => ({ ...f, detail: f.value })), ...FUNCTIONS], [fields]);
44
+ const { start, word } = caret === null ? { start: 0, word: '' } : wordAt(value, caret);
45
+ const known = options.some((o) => o.value === word);
46
+ const open = caret !== null && word.length > 0 && !known && closedAt !== `${start}:${word}`;
47
+ useLayoutEffect(() => {
48
+ if (moveTo.current === null || !box.current)
49
+ return;
50
+ box.current.setSelectionRange(moveTo.current, moveTo.current);
51
+ setCaret(moveTo.current);
52
+ moveTo.current = null;
53
+ });
54
+ const pick = (o) => {
55
+ if (caret === null)
56
+ return;
57
+ const end = wordEnd(value, caret);
58
+ const fn = o.value.endsWith('("")');
59
+ const next = value.slice(0, start) + o.value + value.slice(end);
60
+ // A function's caret goes between its quotes, ready for the field it sums.
61
+ moveTo.current = start + (fn ? o.value.length - 2 : o.value.length);
62
+ onChange(next);
63
+ };
64
+ const track = () => setCaret(box.current?.selectionStart ?? null);
65
+ return (_jsxs("div", { className: "relative", children: [_jsx(Textarea, { ref: box, id: id, rows: 2, "aria-invalid": invalid || undefined, "aria-autocomplete": "list", className: "min-h-16 font-mono text-xs", placeholder: placeholder, value: value, spellCheck: false, onChange: (e) => {
66
+ onChange(e.target.value);
67
+ setCaret(e.target.selectionStart);
68
+ }, onKeyDown: (e) => {
69
+ if (open && list.current?.key(e))
70
+ return;
71
+ }, onKeyUp: (e) => {
72
+ if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(e.key))
73
+ track();
74
+ }, onClick: track, onFocus: track, onBlur: () => setCaret(null) }), open && (_jsx(LumenSelectorList, { fuzzy: true, options: options, query: word, label: "Fields", handle: list, onPick: pick, onClose: () => setClosedAt(`${start}:${word}`), empty: `No field matches ${word}`, className: "absolute top-full z-30 mt-1 w-full max-w-md" }))] }));
75
+ }
76
+ /**
77
+ * For an `unknown field X` issue: the field X most likely means, by label first (Price is
78
+ * price), then by the closest key or label.
79
+ */
80
+ export function guessField(name, fields) {
81
+ const n = name.toLowerCase();
82
+ // A name's field part: `price` (a formula from before names) or `holding.price` is `holdings.price`.
83
+ const part = n.slice(n.lastIndexOf('.') + 1);
84
+ const tail = (v) => v.toLowerCase().slice(v.lastIndexOf('.') + 1);
85
+ return (fields.find((f) => f.label.toLowerCase() === n) ??
86
+ fields.find((f) => f.value.toLowerCase() === n) ??
87
+ fields.find((f) => tail(f.value) === part) ??
88
+ fields.find((f) => f.label.toLowerCase().replace(/[^a-z0-9]+/g, '_') === part) ??
89
+ closest(part, fields));
90
+ }
91
+ function closest(n, fields) {
92
+ let best = null;
93
+ let top = 0;
94
+ for (const f of fields) {
95
+ const key = f.value.slice(f.value.lastIndexOf('.') + 1);
96
+ const v = Math.max(fuzzyScore(n, f.label) ?? 0, fuzzyScore(n, key) ?? 0);
97
+ if (v > top)
98
+ [best, top] = [f, v];
99
+ }
100
+ // Only a match on most of its letters in order, never a stray one.
101
+ return top >= n.length * 2 ? best : null;
102
+ }
103
+ /** `expr` with every whole name `from` swapped for `to` (never part of a longer name). */
104
+ export const swapWord = (expr, from, to) => renameFormula(expr, (n) => (n === from ? to : n));
@@ -0,0 +1,7 @@
1
+ export declare function FrameScriptDialog({ api, frame, name, open, onOpenChange, }: {
2
+ api: string;
3
+ frame: string;
4
+ name: string;
5
+ open: boolean;
6
+ onOpenChange: (open: boolean) => void;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,103 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
3
+ import { PlayIcon, RotateCcwIcon, WrapTextIcon } from 'lucide-react';
4
+ import { useEffect, useMemo, useState } from 'react';
5
+ import { apiJson } from './api-fetch.js';
6
+ import { useAssistantName } from './assistant-context.js';
7
+ // A frame's Script editor: the one script that makes an assistant's table (a call as
8
+ // JSON, SQL, or JavaScript), as code the person reads and edits, in a dialog off the card's signal
9
+ // dot or ⋯ menu. Run and save runs the edited script once as the person, with the read tools the
10
+ // assistant may use; it becomes the frame's script only when it works, and a failure shows here.
11
+ import { LumenCodeEditor } from './lumen-code-editor.js';
12
+ import { lumenToast } from './lumen-toast.js';
13
+ import { IconButton } from './page.js';
14
+ import { Alert, AlertDescription, AlertTitle } from './ui/alert.js';
15
+ import { Badge } from './ui/badge.js';
16
+ import { Button } from './ui/button.js';
17
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from './ui/dialog.js';
18
+ import { Skeleton } from './ui/skeleton.js';
19
+ import { Spinner } from './ui/spinner.js';
20
+ import { WORKSPACES_KEY, frameKey } from './workspace-keys.js';
21
+ const json = (url, init) => apiJson(url, init, 'workspaces');
22
+ /** The script as the text the editor shows, and its language. */
23
+ function scriptText(s) {
24
+ if (s.kind === 'script')
25
+ return { text: s.code, lang: 'js' };
26
+ if (s.kind === 'sql')
27
+ return { text: s.query, lang: 'sql' };
28
+ return { text: JSON.stringify({ fn: s.fn, args: s.args }, null, 2), lang: 'json' };
29
+ }
30
+ /** The edited text back as a script; a call that is not JSON says where. */
31
+ function parseScript(was, text) {
32
+ if (was.kind === 'script')
33
+ return { kind: 'script', code: text };
34
+ if (was.kind === 'sql')
35
+ return { kind: 'sql', query: text };
36
+ let v;
37
+ try {
38
+ v = JSON.parse(text);
39
+ }
40
+ catch (e) {
41
+ throw new Error(`The call is not valid JSON: ${e.message}`);
42
+ }
43
+ const o = (v ?? {});
44
+ return {
45
+ kind: 'call',
46
+ fn: String(o.fn ?? ''),
47
+ args: (o.args ?? {}),
48
+ };
49
+ }
50
+ export function FrameScriptDialog({ api, frame, name, open, onOpenChange, }) {
51
+ const qc = useQueryClient();
52
+ const assistant = useAssistantName();
53
+ const read = useQuery({
54
+ queryKey: [...frameKey(api, frame), 'script'],
55
+ queryFn: () => json(`${api}/frames/${frame}/script`),
56
+ enabled: open,
57
+ staleTime: 0,
58
+ });
59
+ const script = read.data?.script ?? null;
60
+ const [text, setText] = useState('');
61
+ // Unwrapped by default: long lines scroll sideways like a code editor.
62
+ const [wrap, setWrap] = useState(false);
63
+ const [error, setError] = useState(null);
64
+ const original = useMemo(() => (script ? scriptText(script).text : ''), [script]);
65
+ useEffect(() => {
66
+ if (!open)
67
+ return;
68
+ setText(original);
69
+ setError(null);
70
+ }, [open, original]);
71
+ const save = useMutation({
72
+ mutationFn: (next) => json(`${api}/frames/${frame}/script`, {
73
+ method: 'PUT',
74
+ headers: { 'content-type': 'application/json' },
75
+ body: JSON.stringify({ script: next }),
76
+ }),
77
+ onSuccess: (data) => {
78
+ qc.setQueryData(frameKey(api, frame), data);
79
+ void qc.invalidateQueries({ queryKey: [...frameKey(api, frame), 'script'] });
80
+ void qc.invalidateQueries({ queryKey: [...WORKSPACES_KEY, api, 'ws'] });
81
+ lumenToast.success(`Ran your script: ${data.meta.rows.toLocaleString('en-US')} rows, saved`);
82
+ onOpenChange(false);
83
+ },
84
+ onError: (e) => setError(e.message),
85
+ });
86
+ const lang = script ? scriptText(script).lang : undefined;
87
+ const changed = text !== original;
88
+ const run = () => {
89
+ if (!script)
90
+ return;
91
+ setError(null);
92
+ try {
93
+ save.mutate(parseScript(script, text));
94
+ }
95
+ catch (e) {
96
+ setError(e.message);
97
+ }
98
+ };
99
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[min(44rem,calc(100dvh-2rem))] flex-col sm:max-w-4xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Script \u00B7 ", name] }), _jsxs(DialogDescription, { children: ["What ", assistant, " runs to make these rows. Saved only if it runs."] })] }), read.isPending ? (_jsx(Skeleton, { className: "min-h-0 flex-1" })) : read.error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTitle, { children: "Couldn't read the script" }), _jsx(AlertDescription, { children: read.error.message })] })) : !script ? (_jsxs(Alert, { children: [_jsx(AlertTitle, { children: "No script" }), _jsxs(AlertDescription, { children: [assistant, " wrote these rows once: nothing runs to make them."] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-end gap-1", children: [lang && (_jsx(Badge, { variant: "secondary", className: "font-mono", children: lang })), _jsx(IconButton, { icon: WrapTextIcon, label: wrap ? 'Stop wrapping lines' : 'Wrap long lines', pressed: wrap, onClick: () => setWrap(!wrap) })] }), _jsx(LumenCodeEditor, { className: "flex-1", value: text, lang: lang, wrap: wrap, onChange: setText }), error && (_jsxs(Alert, { variant: "destructive", className: "max-h-32 overflow-y-auto", children: [_jsx(AlertTitle, { children: "Didn't save" }), _jsx(AlertDescription, { className: "break-words font-mono text-xs", children: error })] }))] })), _jsxs(DialogFooter, { children: [_jsx(IconButton, { icon: RotateCcwIcon, label: "Undo my changes", disabled: !changed || save.isPending, onClick: () => {
100
+ setText(original);
101
+ setError(null);
102
+ } }), _jsxs(Button, { disabled: !script || save.isPending, onClick: run, children: [save.isPending ? _jsx(Spinner, {}) : _jsx(PlayIcon, {}), " Run and save"] })] })] }) }));
103
+ }
@@ -0,0 +1,24 @@
1
+ import { type ViewDomain, type ViewField } from './view-model.js';
2
+ export type Freshness = 'live' | 'calc' | 'asof';
3
+ export declare const FRESHNESS: Record<Freshness, {
4
+ label: string;
5
+ about: string;
6
+ }>;
7
+ /** How a field updates: live, live calc, as of, or null for fixed. */
8
+ export declare function freshnessOf<T>(domain: ViewDomain<T>, f: ViewField<T>, seen?: Set<string>): Freshness | null;
9
+ /** The live fields a Live calc field reads, by label, for its tooltip (`from Price`). */
10
+ export declare function liveInputs<T>(domain: ViewDomain<T>, f: ViewField<T>): string[];
11
+ /** The tooltip line under a header's path: `Live: moves with the market while it trades`. */
12
+ export declare function freshnessLine<T>(domain: ViewDomain<T>, f: ViewField<T>): string | null;
13
+ /** The mark itself, drawn: a green dot for Live, a calculator for Live calc, a clock for As of. */
14
+ export declare function FreshnessIcon({ kind, className }: {
15
+ kind: Freshness;
16
+ className?: string;
17
+ }): import("react").JSX.Element;
18
+ /** The mark beside a header's name; nothing for a fixed field. */
19
+ export declare function FreshnessMark<T>({ domain, field, }: {
20
+ domain: ViewDomain<T>;
21
+ field: ViewField<T>;
22
+ }): import("react").JSX.Element | null;
23
+ /** The marks the shown columns carry, in order live, live calc, as of. */
24
+ export declare function marksOf<T>(domain: ViewDomain<T>, columns: ViewField<T>[]): Freshness[];
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Freshness marks:
3
+ // a column's colour says where it comes from, and a small mark beside its name says how it
4
+ // updates: a green dot Live (moves with the market: a quote), a calculator Live calc (worked out
5
+ // from a live field: Value, Unrealized, a formula over Price; , #861), ◷ As of (a daily or snapshot value: a close, a fact,
6
+ // the assistant frame's column) and nothing for a fixed value (Qty, Cost). Declared once on each field
7
+ // (`updates`, `from`); a formula's mark comes from the fields its expression reads.
8
+ import { formulaReads } from '@columenlabs/lumen-ui-types/views';
9
+ import { CalculatorIcon } from 'lucide-react';
10
+ import { FORMULAS_GROUP, groupOf, nameIndex } from './view-model.js';
11
+ export const FRESHNESS = {
12
+ live: { label: 'Live', about: 'moves with the market while it trades' },
13
+ calc: { label: 'Live calc', about: 'worked out from a live field' },
14
+ asof: { label: 'As of', about: 'a daily or snapshot value' },
15
+ };
16
+ /** The field keys a field is made from: its `from`, a share's field, the names a formula reads. */
17
+ function inputsOf(domain, f) {
18
+ const out = new Set(f.from ?? []);
19
+ if (f.share)
20
+ out.add(f.share.of);
21
+ if (groupOf(domain, f) === FORMULAS_GROUP && f.formula) {
22
+ const byName = nameIndex(domain);
23
+ for (const n of formulaReads(f.formula)) {
24
+ const k = byName.get(n)?.key;
25
+ if (k && k !== f.key)
26
+ out.add(k);
27
+ }
28
+ }
29
+ return [...out];
30
+ }
31
+ /** How a field updates: live, live calc, as of, or null for fixed. */
32
+ export function freshnessOf(domain, f, seen = new Set()) {
33
+ if (f.updates === 'live')
34
+ return 'live';
35
+ if (f.updates === 'calc')
36
+ return 'calc';
37
+ seen.add(f.key);
38
+ let asof = f.updates === 'asof';
39
+ for (const k of inputsOf(domain, f)) {
40
+ if (seen.has(k))
41
+ continue;
42
+ const g = domain.fields.find((x) => x.key === k);
43
+ if (!g)
44
+ continue;
45
+ const r = freshnessOf(domain, g, seen);
46
+ if (r === 'live' || r === 'calc')
47
+ return 'calc';
48
+ if (r === 'asof')
49
+ asof = true;
50
+ }
51
+ return asof ? 'asof' : null;
52
+ }
53
+ /** The live fields a Live calc field reads, by label, for its tooltip (`from Price`). */
54
+ export function liveInputs(domain, f) {
55
+ return inputsOf(domain, f)
56
+ .map((k) => domain.fields.find((x) => x.key === k))
57
+ .filter((g) => {
58
+ if (!g)
59
+ return false;
60
+ const r = freshnessOf(domain, g, new Set([f.key]));
61
+ return r === 'live' || r === 'calc';
62
+ })
63
+ .map((g) => g.label);
64
+ }
65
+ /** The tooltip line under a header's path: `Live: moves with the market while it trades`. */
66
+ export function freshnessLine(domain, f) {
67
+ const r = freshnessOf(domain, f);
68
+ if (!r)
69
+ return null;
70
+ const m = FRESHNESS[r];
71
+ if (r === 'calc') {
72
+ const from = liveInputs(domain, f);
73
+ return `${m.label}: worked out from ${from.length ? from.join(', ') : 'a live field'}`;
74
+ }
75
+ return `${m.label}: ${m.about}`;
76
+ }
77
+ /** The mark itself, drawn: a green dot for Live, a calculator for Live calc, a clock for As of. */
78
+ export function FreshnessIcon({ kind, className }) {
79
+ const base = `inline-block shrink-0 align-[-0.05em] ${className ?? ''}`;
80
+ const cls = `${base} size-[0.62rem]`;
81
+ const label = FRESHNESS[kind].label;
82
+ if (kind === 'calc')
83
+ return (_jsx(CalculatorIcon, { className: `${base} size-3`, strokeWidth: 2.4, role: "img", "aria-label": label }));
84
+ if (kind === 'live')
85
+ return (_jsx("svg", { viewBox: "0 0 12 12", className: `${cls} text-gain`, role: "img", "aria-label": label, children: _jsx("circle", { cx: "6", cy: "6", r: "3.6", fill: "currentColor" }) }));
86
+ return (_jsxs("svg", { viewBox: "0 0 12 12", className: cls, role: "img", "aria-label": label, children: [_jsx("circle", { cx: "6", cy: "6", r: "4.6", fill: "none", stroke: "currentColor", strokeWidth: "1.4" }), _jsx("path", { d: "M6 3.4V6l1.8 1.2", fill: "none", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round" })] }));
87
+ }
88
+ /** The mark beside a header's name; nothing for a fixed field. */
89
+ export function FreshnessMark({ domain, field, }) {
90
+ const r = freshnessOf(domain, field);
91
+ if (!r)
92
+ return null;
93
+ return (_jsx("span", { className: "mr-1 inline-flex opacity-80", children: _jsx(FreshnessIcon, { kind: r }) }));
94
+ }
95
+ /** The marks the shown columns carry, in order live, live calc, as of. */
96
+ export function marksOf(domain, columns) {
97
+ const got = new Set(columns.map((c) => freshnessOf(domain, c)));
98
+ return ['live', 'calc', 'asof'].filter((r) => got.has(r));
99
+ }
@@ -0,0 +1,33 @@
1
+ import type { CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
2
+ import { type ViewDomain, type ViewField, type ViewSubgroup } from './view-model.js';
3
+ export declare const GROUP_NONE = "_none";
4
+ export declare const GROUP_PIVOT = "_pivot";
5
+ /** A view's grouping for the visit: by a field, as a pivot, and what each row opens. */
6
+ export declare function useGroup<T>(domain: ViewDomain<T>, config: CustomViewConfig): {
7
+ groupField: ViewField<T> | null;
8
+ pivot: boolean;
9
+ sub: string | null;
10
+ button: import("react").JSX.Element | null;
11
+ held: {
12
+ group: string | null;
13
+ pivot: boolean;
14
+ subgroup: string | null;
15
+ };
16
+ reset: () => void;
17
+ };
18
+ /**
19
+ * The Group button: says what is on ("Account › Tax lots") and opens two
20
+ * pickers, Group rows by (a field, or Pivot) and Each row opens (the domain's sub-groups), each
21
+ * with None.
22
+ */
23
+ export declare function GroupButton<T>({ groupable, pivot, mode, onMode, subgroups, subgroup, onSubgroup, opensLabel, }: {
24
+ groupable: ViewField<T>[];
25
+ pivot: boolean;
26
+ mode: string;
27
+ onMode: (mode: string) => void;
28
+ subgroups: ViewSubgroup<T>[];
29
+ subgroup: string | null;
30
+ onSubgroup: (key: string | null) => void;
31
+ /** The sub-group picker's name (`subgroupLabel`). */
32
+ opensLabel?: string;
33
+ }): import("react").JSX.Element;