@columenlabs/lumen-ui 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) 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 +661 -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 +720 -0
  117. package/dist/lumen-widget.js +609 -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/sourced-card.d.ts +10 -0
  173. package/dist/sourced-card.js +69 -0
  174. package/dist/spotlight.d.ts +23 -0
  175. package/dist/spotlight.js +138 -0
  176. package/dist/status-health.d.ts +16 -0
  177. package/dist/status-health.js +41 -0
  178. package/dist/status.d.ts +9 -0
  179. package/dist/status.js +12 -0
  180. package/dist/summary-cards.d.ts +56 -0
  181. package/dist/summary-cards.js +270 -0
  182. package/dist/symbol-sheet-context.d.ts +4 -0
  183. package/dist/symbol-sheet-context.js +5 -0
  184. package/dist/symbol-sheet.d.ts +9 -0
  185. package/dist/symbol-sheet.js +24 -0
  186. package/dist/tasks.d.ts +29 -0
  187. package/dist/tasks.js +278 -0
  188. package/dist/theme.css +606 -0
  189. package/dist/tile.d.ts +37 -0
  190. package/dist/tile.js +23 -0
  191. package/dist/ui/alert-dialog.d.ts +18 -0
  192. package/dist/ui/alert-dialog.js +41 -0
  193. package/dist/ui/alert.d.ts +10 -0
  194. package/dist/ui/alert.js +27 -0
  195. package/dist/ui/avatar.d.ts +11 -0
  196. package/dist/ui/avatar.js +22 -0
  197. package/dist/ui/badge.d.ts +9 -0
  198. package/dist/ui/badge.js +27 -0
  199. package/dist/ui/breadcrumb.d.ts +11 -0
  200. package/dist/ui/breadcrumb.js +27 -0
  201. package/dist/ui/button-group.d.ts +11 -0
  202. package/dist/ui/button-group.js +27 -0
  203. package/dist/ui/button.d.ts +10 -0
  204. package/dist/ui/button.js +38 -0
  205. package/dist/ui/card.d.ts +11 -0
  206. package/dist/ui/card.js +24 -0
  207. package/dist/ui/chart.d.ts +44 -0
  208. package/dist/ui/chart.js +117 -0
  209. package/dist/ui/checkbox.d.ts +4 -0
  210. package/dist/ui/checkbox.js +8 -0
  211. package/dist/ui/collapsible.d.ts +5 -0
  212. package/dist/ui/collapsible.js +12 -0
  213. package/dist/ui/combobox.d.ts +24 -0
  214. package/dist/ui/combobox.js +60 -0
  215. package/dist/ui/command.d.ts +18 -0
  216. package/dist/ui/command.js +34 -0
  217. package/dist/ui/dialog.d.ts +23 -0
  218. package/dist/ui/dialog.js +50 -0
  219. package/dist/ui/dropdown-menu.d.ts +29 -0
  220. package/dist/ui/dropdown-menu.js +50 -0
  221. package/dist/ui/emoji-picker.d.ts +6 -0
  222. package/dist/ui/emoji-picker.js +28 -0
  223. package/dist/ui/empty.d.ts +11 -0
  224. package/dist/ui/empty.js +33 -0
  225. package/dist/ui/field.d.ts +24 -0
  226. package/dist/ui/field.js +65 -0
  227. package/dist/ui/hover-card.d.ts +6 -0
  228. package/dist/ui/hover-card.js +13 -0
  229. package/dist/ui/input-group.d.ts +16 -0
  230. package/dist/ui/input-group.js +56 -0
  231. package/dist/ui/input.d.ts +3 -0
  232. package/dist/ui/input.js +6 -0
  233. package/dist/ui/item.d.ts +23 -0
  234. package/dist/ui/item.js +67 -0
  235. package/dist/ui/kbd.d.ts +3 -0
  236. package/dist/ui/kbd.js +9 -0
  237. package/dist/ui/label.d.ts +4 -0
  238. package/dist/ui/label.js +7 -0
  239. package/dist/ui/popover.d.ts +10 -0
  240. package/dist/ui/popover.js +25 -0
  241. package/dist/ui/progress.d.ts +4 -0
  242. package/dist/ui/progress.js +9 -0
  243. package/dist/ui/resizable.d.ts +7 -0
  244. package/dist/ui/resizable.js +13 -0
  245. package/dist/ui/scroll-area.d.ts +5 -0
  246. package/dist/ui/scroll-area.js +10 -0
  247. package/dist/ui/select.d.ts +15 -0
  248. package/dist/ui/select.js +36 -0
  249. package/dist/ui/separator.d.ts +4 -0
  250. package/dist/ui/separator.js +7 -0
  251. package/dist/ui/sheet.d.ts +14 -0
  252. package/dist/ui/sheet.js +54 -0
  253. package/dist/ui/sidebar.d.ts +69 -0
  254. package/dist/ui/sidebar.js +206 -0
  255. package/dist/ui/skeleton.d.ts +2 -0
  256. package/dist/ui/skeleton.js +6 -0
  257. package/dist/ui/slider.d.ts +4 -0
  258. package/dist/ui/slider.js +9 -0
  259. package/dist/ui/sonner.d.ts +3 -0
  260. package/dist/ui/sonner.js +24 -0
  261. package/dist/ui/spinner.d.ts +2 -0
  262. package/dist/ui/spinner.js +7 -0
  263. package/dist/ui/switch.d.ts +6 -0
  264. package/dist/ui/switch.js +7 -0
  265. package/dist/ui/table.d.ts +10 -0
  266. package/dist/ui/table.js +31 -0
  267. package/dist/ui/tabs.d.ts +7 -0
  268. package/dist/ui/tabs.js +22 -0
  269. package/dist/ui/textarea.d.ts +3 -0
  270. package/dist/ui/textarea.js +6 -0
  271. package/dist/ui/toggle-group.d.ts +10 -0
  272. package/dist/ui/toggle-group.js +22 -0
  273. package/dist/ui/toggle.d.ts +9 -0
  274. package/dist/ui/toggle.js +25 -0
  275. package/dist/ui/tooltip.d.ts +7 -0
  276. package/dist/ui/tooltip.js +16 -0
  277. package/dist/view-edit-cell.d.ts +49 -0
  278. package/dist/view-edit-cell.js +198 -0
  279. package/dist/view-editor.d.ts +52 -0
  280. package/dist/view-editor.js +1081 -0
  281. package/dist/view-formulas.d.ts +61 -0
  282. package/dist/view-formulas.js +247 -0
  283. package/dist/view-icons.d.ts +20 -0
  284. package/dist/view-icons.js +70 -0
  285. package/dist/view-model.d.ts +390 -0
  286. package/dist/view-model.js +316 -0
  287. package/dist/whats-new.d.ts +3 -0
  288. package/dist/whats-new.js +53 -0
  289. package/dist/widget-controls.d.ts +9 -0
  290. package/dist/widget-controls.js +26 -0
  291. package/dist/widget-data.d.ts +4 -0
  292. package/dist/widget-data.js +32 -0
  293. package/dist/widget-settings.d.ts +516 -0
  294. package/dist/widget-settings.js +290 -0
  295. package/dist/widget-thumbs.d.ts +3 -0
  296. package/dist/widget-thumbs.js +29 -0
  297. package/dist/workspace-keys.d.ts +12 -0
  298. package/dist/workspace-keys.js +13 -0
  299. package/dist/workspaces.d.ts +57 -0
  300. package/dist/workspaces.js +473 -0
  301. package/package.json +82 -4
@@ -0,0 +1,327 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The shared loading contract.
3
+ // Every card and view loads its own data and says so the same way: a skeleton on first load, a thin
4
+ // bar while a view's query runs, the old data on screen (dimmed) while a refresh someone asked for
5
+ // runs (a poll, refetch on focus or other background refresh is silent: the data just updates in
6
+ // place), its own ↻ that
7
+ // reloads just its source past the server cache, a failure kept inside it with Retry, and, with
8
+ // Settings ▸ Preferences ▸ Observability details on, how long it took. TanStack Query is the engine;
9
+ // `useLumenSource` runs one query, `useLumenLoad` reads queries a page already runs, and `LumenStates`
10
+ // draws the states. No component draws its own.
11
+ import { useQuery, } from '@tanstack/react-query';
12
+ import { AlertTriangleIcon, CodeIcon, RefreshCwIcon, RotateCcwIcon } from 'lucide-react';
13
+ import { useCallback, useEffect, useRef, useState } from 'react';
14
+ import { useInView } from 'react-intersection-observer';
15
+ import { IconButton } from './icon-button.js';
16
+ import { cn } from './lib/utils.js';
17
+ import { timeZoneLabel, usePrefs } from './prefs.js';
18
+ import { withRefreshToken } from './refresh.js';
19
+ import { Alert, AlertAction, AlertDescription, AlertTitle } from './ui/alert.js';
20
+ import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
21
+ import { Progress } from './ui/progress.js';
22
+ import { Skeleton } from './ui/skeleton.js';
23
+ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
24
+ const asError = (e) => (e instanceof Error ? e : new Error(String(e)));
25
+ /** Times each load from the moment a request is in flight to the render that shows its data. */
26
+ function useLoadTime(fetching, updatedAt) {
27
+ const start = useRef(null);
28
+ const seen = useRef(0);
29
+ const [time, setTime] = useState({
30
+ tookMs: null,
31
+ cached: false,
32
+ });
33
+ useEffect(() => {
34
+ if (fetching) {
35
+ start.current ??= performance.now();
36
+ return;
37
+ }
38
+ if (updatedAt && updatedAt !== seen.current) {
39
+ seen.current = updatedAt;
40
+ const t0 = start.current;
41
+ setTime(t0 === null
42
+ ? { tookMs: 0, cached: true }
43
+ : { tookMs: performance.now() - t0, cached: false });
44
+ }
45
+ start.current = null;
46
+ }, [fetching, updatedAt]);
47
+ return time;
48
+ }
49
+ /** The loading state of queries a page already runs (rows built from several reads). */
50
+ export function useLumenLoad(...queries) {
51
+ const latest = useRef(queries);
52
+ latest.current = queries;
53
+ const has = queries.every((q) => q.data !== undefined);
54
+ const failed = queries.find((q) => q.isError);
55
+ const fetching = queries.some((q) => q.isFetching);
56
+ const updatedAt = Math.max(0, ...queries.map((q) => q.dataUpdatedAt));
57
+ const time = useLoadTime(fetching, has ? updatedAt : 0);
58
+ // A ↻ (or the page's Refresh) is the only refresh that shows; a poll or refetch on focus over data
59
+ // already on screen updates it in place with no bar, dimming or spin. New inputs showing the old
60
+ // rows as placeholder still show the bar.
61
+ const [asked, setAsked] = useState(false);
62
+ const refresh = useCallback(async () => {
63
+ setAsked(true);
64
+ try {
65
+ await withRefreshToken(() => Promise.all(latest.current.map((q) => q.refetch())));
66
+ }
67
+ finally {
68
+ setAsked(false);
69
+ }
70
+ }, []);
71
+ const poll = useCallback(() => {
72
+ for (const q of latest.current)
73
+ void q.refetch();
74
+ }, []);
75
+ const quiet = has && !asked && !queries.some((q) => q.isPlaceholderData);
76
+ const shown = fetching && !quiet;
77
+ return {
78
+ loading: !has && !failed,
79
+ fetching: shown,
80
+ refreshing: has && shown,
81
+ background: fetching && quiet,
82
+ error: !has && failed ? asError(failed.error) : null,
83
+ stale: has && failed ? asError(failed.error) : null,
84
+ refresh,
85
+ poll,
86
+ tookMs: time.tookMs,
87
+ cached: time.cached,
88
+ at: has && updatedAt ? updatedAt : null,
89
+ };
90
+ }
91
+ /**
92
+ * Cards that draw one shared source (a view and the widgets over its rows). A card's ↻ still reloads
93
+ * that source, since every card draws the same rows, but only the card that asked spins, dims and
94
+ * shows the new load time; the others keep still with their own last time and take the new rows
95
+ * quietly. A reload no card asked for (the page's Refresh, a refetch) shows on every card. Returns a
96
+ * function giving each card its own load.
97
+ */
98
+ export function useCardLoads() {
99
+ const [owner, setOwner] = useState(null);
100
+ // When each card's last ↻ ran (ms since epoch): data that landed then is that card's alone.
101
+ const asked = useRef(new Map());
102
+ // Each card's own load time, kept while another card's refresh is on screen.
103
+ const times = useRef(new Map());
104
+ return useCallback((id, load) => {
105
+ if (!load)
106
+ return load;
107
+ const shown = owner === null || owner === id;
108
+ const at = load.at;
109
+ const byOther = !shown ||
110
+ (at !== null &&
111
+ [...asked.current].some(([card, r]) => card !== id && at >= r.from && at <= r.to));
112
+ const kept = times.current.get(id);
113
+ const time = byOther && kept ? kept : { tookMs: load.tookMs, cached: load.cached, at };
114
+ times.current.set(id, time);
115
+ return {
116
+ ...load,
117
+ ...time,
118
+ fetching: load.fetching && shown,
119
+ refreshing: load.refreshing && shown,
120
+ background: load.background && shown,
121
+ refresh: async () => {
122
+ const from = Date.now();
123
+ setOwner(id);
124
+ try {
125
+ await load.refresh();
126
+ }
127
+ finally {
128
+ asked.current.set(id, { from, to: Date.now() });
129
+ setOwner(null);
130
+ }
131
+ },
132
+ };
133
+ }, [owner]);
134
+ }
135
+ /** Runs one source. `lazy` holds the query until the card (`ref`) scrolls into view. */
136
+ export function useLumenSource(options, { lazy = false } = {}) {
137
+ const { ref, inView } = useInView({ triggerOnce: true, rootMargin: '200px', skip: !lazy });
138
+ const query = useQuery({ ...options, enabled: lazy && !inView ? false : options.enabled });
139
+ const load = useLumenLoad(query);
140
+ return { data: query.data, load, ref };
141
+ }
142
+ /** Mounts `children` once they scroll into view: cards whose data is their own (live market). */
143
+ export function LumenLazy({ children, fallback = _jsx(Skeleton, { className: "h-40 w-full" }), }) {
144
+ const { ref, inView } = useInView({ triggerOnce: true, rootMargin: '200px' });
145
+ return _jsx("div", { ref: ref, children: inView ? children : fallback });
146
+ }
147
+ /** The thin indeterminate bar along the top of a view while its query runs. */
148
+ export function LumenProgress({ load }) {
149
+ if (!load.fetching)
150
+ return null;
151
+ return (_jsx(Progress, { "aria-label": "Loading", className: "pointer-events-none absolute inset-x-0 top-0 z-30 h-0.5 rounded-none bg-transparent" }));
152
+ }
153
+ /** ↻ for one card or view: reloads its source past the server cache, spinning while it runs. */
154
+ export function LumenRefresh({ load, label = 'Refresh' }) {
155
+ return (_jsx(IconButton, { icon: RefreshCwIcon, busy: load.fetching, label: load.refreshLabel ?? `${label} (skip cache)`, disabled: load.fetching, onClick: () => void load.refresh() }));
156
+ }
157
+ const SLOW_MS = 2000;
158
+ /** The load time line (`1.8 s · 412 rows · 12:41:08 Pacific`), only with Observability details on. */
159
+ export function LumenLoadTime({ load, rows, className, }) {
160
+ const { observability, timeZone } = usePrefs();
161
+ if (!observability || load.tookMs === null)
162
+ return null;
163
+ const when = load.at === null
164
+ ? ''
165
+ : new Intl.DateTimeFormat('en-US', {
166
+ timeZone,
167
+ hour: 'numeric',
168
+ minute: '2-digit',
169
+ second: '2-digit',
170
+ }).format(load.at);
171
+ // Whole ms under a second, seconds with one decimal from there.
172
+ const took = load.tookMs < 1000 ? `${Math.round(load.tookMs)} ms` : `${(load.tookMs / 1000).toFixed(1)} s`;
173
+ const count = rows === undefined ? null : `${rows.toLocaleString('en-US')} rows`;
174
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("span", { "data-slot": "lumen-load-time", className: cn('num flex min-w-0 items-center gap-1.5 whitespace-nowrap text-xs text-muted-foreground', className), children: [_jsx("span", { className: cn(load.tookMs > SLOW_MS ? 'text-loss' : 'text-gain'), children: load.cached ? 'cache' : took }), count && _jsxs("span", { children: ["\u00B7 ", count] }), load.refreshing && _jsx("span", { children: "\u00B7 refreshing\u2026" })] }) }), _jsxs(TooltipContent, { children: [load.cached
175
+ ? "From this browser's cache, no request"
176
+ : `Loaded in ${took}${load.tookMs > SLOW_MS ? ', slow (over 2 s)' : ''}`, when && ` at ${when} ${timeZoneLabel(timeZone)}`, count && ` · ${count}`] })] }));
177
+ }
178
+ /** A failed source, inside its own card, with Retry. */
179
+ export function LumenError({ error, onRetry }) {
180
+ return (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTriangleIcon, {}), _jsx(AlertTitle, { children: "Could not load" }), _jsx(AlertDescription, { className: "num text-xs break-words", children: error.message }), onRetry && (_jsx(AlertAction, { children: _jsx(IconButton, { icon: RotateCcwIcon, label: "Retry", variant: "outline", onClick: onRetry }) }))] }));
181
+ }
182
+ /** Rows of skeleton the height of a table, so nothing jumps when the data lands. */
183
+ export function LumenSkeleton({ rows = 6, className }) {
184
+ return (_jsx("div", { "aria-busy": "true", "aria-label": "Loading", className: cn('flex flex-col gap-2 p-3', className), children: Array.from({ length: rows }, (_, i) => (_jsx(Skeleton, { className: "h-7 w-full" }, i))) }));
185
+ }
186
+ /** One component's states from its load: bar (views), skeleton, error with Retry, empty, then the
187
+ * content, dimmed while it refreshes. Place it at the top of a `relative` card body. */
188
+ export function LumenStates({ load, kind = 'table', skeleton, empty, children, }) {
189
+ return (_jsxs(_Fragment, { children: [kind === 'table' && _jsx(LumenProgress, { load: load }), load.error ? (_jsx("div", { className: "p-3", children: _jsx(LumenError, { error: load.error, onRetry: () => void load.refresh() }) })) : load.loading ? ((skeleton ?? (kind === 'table' ? _jsx(LumenSkeleton, {}) : _jsx(Skeleton, { className: "h-40 w-full" })))) : empty ? (empty) : (_jsx("div", { className: cn('contents [&>*]:transition-opacity', load.refreshing && '[&>*]:opacity-70'), children: children }))] }));
190
+ }
191
+ /** A card's footer line with its load time, rows and clock time, only with Observability details on. */
192
+ export function LumenLoadFooter({ load, rows, note, }) {
193
+ const { observability, timeZone } = usePrefs();
194
+ if (!observability || load.tookMs === null)
195
+ return null;
196
+ return (_jsx("div", { "data-slot": "lumen-load-footer", className: "shrink-0 border-t border-dashed", children: _jsx(LumenLoadTimeLine, { load: load, rows: rows, note: note, timeZone: timeZone }) }));
197
+ }
198
+ const FRESH_LIVE_MS = 5 * 60_000;
199
+ /** How often a view showing live columns reads its source again while the market trades. */
200
+ export const LIVE_POLL_MS = 60_000;
201
+ /**
202
+ * When US stocks trade at all: pre-market, the session, after-hours and the
203
+ * overnight session run from 8:00 PM New York on Sunday to 8:00 PM on Friday (holidays not known here).
204
+ */
205
+ export function marketTrades(at = Date.now()) {
206
+ const p = Object.fromEntries(new Intl.DateTimeFormat('en-US', {
207
+ timeZone: 'America/New_York',
208
+ weekday: 'short',
209
+ hour: 'numeric',
210
+ minute: 'numeric',
211
+ hourCycle: 'h23',
212
+ })
213
+ .formatToParts(at)
214
+ .map((x) => [x.type, x.value]));
215
+ const evening = Number(p.hour) >= 20;
216
+ if (p.weekday === 'Sat')
217
+ return false;
218
+ if (p.weekday === 'Sun')
219
+ return evening;
220
+ return p.weekday !== 'Fri' || !evening;
221
+ }
222
+ /**
223
+ * A view's load while it shows live columns (#861): while the market trades and
224
+ * the tab shows, it reads its source again every minute, quietly, and the card's header carries the
225
+ * green signal dot (Live, with when it last updated). Extended hours and overnight poll too (the
226
+ * app's feed decides whether prices move then); over the weekend nothing polls.
227
+ */
228
+ export function useLiveLoad(load, live) {
229
+ const [open, setOpen] = useState(() => marketTrades());
230
+ const poll = load?.poll;
231
+ const on = live && open && !!poll;
232
+ useEffect(() => {
233
+ if (!live)
234
+ return;
235
+ const t = setInterval(() => setOpen(marketTrades()), 30_000);
236
+ return () => clearInterval(t);
237
+ }, [live]);
238
+ useEffect(() => {
239
+ if (!on || !poll)
240
+ return;
241
+ const t = setInterval(() => {
242
+ if (document.visibilityState === 'visible')
243
+ poll();
244
+ }, LIVE_POLL_MS);
245
+ return () => clearInterval(t);
246
+ }, [on, poll]);
247
+ if (!load || !on || load.updated || load.at === null)
248
+ return load;
249
+ return {
250
+ ...load,
251
+ updated: {
252
+ at: load.at,
253
+ kind: 'live',
254
+ about: 'Live columns update every minute, extended hours included',
255
+ },
256
+ };
257
+ }
258
+ /** How long ago, in words: `just now`, `3 min ago`, `2 h ago`, `4 d ago`. */
259
+ function ago(ms) {
260
+ const m = Math.floor(ms / 60_000);
261
+ if (m < 1)
262
+ return 'just now';
263
+ if (m < 60)
264
+ return `${m} min ago`;
265
+ const h = Math.floor(m / 60);
266
+ return h < 24 ? `${h} h ago` : `${Math.floor(h / 24)} d ago`;
267
+ }
268
+ /**
269
+ * The card header's signal dot for a source with its own update time (`load.updated`): green while
270
+ * fresh, amber once stale (worth a ↻), red when its last update failed, grey for data that never
271
+ * updates; hover (a tap on phone) says when, what and why.
272
+ */
273
+ export function LumenFreshness({ load }) {
274
+ const { timeZone } = usePrefs();
275
+ const [now, setNow] = useState(() => Date.now());
276
+ const [open, setOpen] = useState(false);
277
+ useEffect(() => {
278
+ const t = setInterval(() => setNow(Date.now()), 60_000);
279
+ return () => clearInterval(t);
280
+ }, []);
281
+ const u = load.updated;
282
+ if (!u)
283
+ return null;
284
+ const kind = u.kind ?? 'asof';
285
+ const age = now - u.at;
286
+ const sameDay = (a, b) => new Intl.DateTimeFormat('en-US', { timeZone, dateStyle: 'short' }).format(a) ===
287
+ new Intl.DateTimeFormat('en-US', { timeZone, dateStyle: 'short' }).format(b);
288
+ const state = u.error
289
+ ? 'failed'
290
+ : kind === 'fixed'
291
+ ? 'fixed'
292
+ : (kind === 'live' ? age < FRESH_LIVE_MS : sameDay(u.at, now))
293
+ ? 'fresh'
294
+ : 'stale';
295
+ const dot = {
296
+ fresh: 'bg-gain',
297
+ stale: 'bg-amber-500',
298
+ failed: 'bg-loss',
299
+ fixed: 'bg-muted-foreground/50',
300
+ }[state];
301
+ const when = new Intl.DateTimeFormat('en-US', {
302
+ timeZone,
303
+ month: 'short',
304
+ day: 'numeric',
305
+ hour: 'numeric',
306
+ minute: '2-digit',
307
+ }).format(u.at);
308
+ const label = {
309
+ fresh: 'Up to date',
310
+ stale: 'Not updated recently',
311
+ failed: 'Last update failed',
312
+ fixed: 'Does not update',
313
+ }[state];
314
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-slot": "lumen-freshness", "aria-label": `${label}: updated ${when}`, className: "flex size-6 shrink-0 items-center justify-center rounded-md hover:bg-accent", onMouseEnter: () => setOpen(true), onMouseLeave: () => setOpen(false), children: _jsx("span", { className: cn('size-2 rounded-full', dot) }) }) }), _jsxs(PopoverContent, { align: "end", className: "w-72 space-y-1 p-3 text-xs", onOpenAutoFocus: (e) => e.preventDefault(), onMouseEnter: () => setOpen(true), onMouseLeave: () => setOpen(false), children: [_jsxs("div", { className: "flex items-center gap-1.5 font-medium text-foreground", children: [_jsx("span", { className: cn('size-2 rounded-full', dot) }), label] }), _jsxs("div", { className: "num text-muted-foreground", children: ["Updated ", when, " (", ago(age), ")"] }), u.about && _jsx("div", { className: "text-muted-foreground", children: u.about }), u.error && _jsx("div", { className: "break-words text-loss", children: u.error }), kind !== 'fixed' && load.refreshLabel && (_jsxs("div", { className: "text-muted-foreground", children: ["\u21BB ", load.refreshLabel] })), load.editScript && (_jsx(IconButton, { icon: CodeIcon, label: "See and edit the script", variant: "outline", onClick: () => {
315
+ setOpen(false);
316
+ load.editScript?.();
317
+ } }))] })] }));
318
+ }
319
+ /** The Observability line of the footer: load time, rows or a note, and the clock time. */
320
+ function LumenLoadTimeLine({ load, rows, note, timeZone, }) {
321
+ return (_jsxs("div", { className: "flex h-7 items-center gap-2 overflow-hidden px-3", children: [_jsx(LumenLoadTime, { load: load, rows: note ? undefined : rows }), note && _jsxs("span", { className: "truncate text-xs text-muted-foreground", children: ["\u00B7 ", note] }), load.at !== null && (_jsx("span", { className: "num ml-auto shrink-0 whitespace-nowrap text-xs text-muted-foreground", children: new Intl.DateTimeFormat('en-US', {
322
+ timeZone,
323
+ hour: 'numeric',
324
+ minute: '2-digit',
325
+ second: '2-digit',
326
+ }).format(load.at) }))] }));
327
+ }
@@ -0,0 +1,27 @@
1
+ import type { ViewIcon } from '@columenlabs/lumen-ui-types/views';
2
+ import type { NavIcon } from './nav.js';
3
+ /** A tab's name, at most this long unless the app keeps longer ones. */
4
+ export declare const TAB_NAME_MAX = 30;
5
+ export declare function LumenTabDrawer({ open, onClose, name, icon, fallback, description, onSave, busy, error, remove, maxName, }: {
6
+ open: boolean;
7
+ onClose: () => void;
8
+ name: string;
9
+ icon: ViewIcon | null | undefined;
10
+ /** The icon shown with none picked. */
11
+ fallback: NavIcon;
12
+ description?: string;
13
+ onSave: (next: {
14
+ name: string;
15
+ icon: ViewIcon | null;
16
+ }) => void;
17
+ busy?: boolean;
18
+ /** A failed save, under the fields. */
19
+ error?: string | null;
20
+ /** The footer's Remove: what goes and what stays. Left out, the tab cannot be removed here. */
21
+ remove?: {
22
+ description: string;
23
+ onRemove: () => void;
24
+ };
25
+ /** The longest name this app keeps. */
26
+ maxName?: number;
27
+ }): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Trash2Icon } from 'lucide-react';
3
+ import { useEffect, useState } from 'react';
4
+ import { LumenConfirm } from './confirm.js';
5
+ import { SaveButton } from './form-issues.js';
6
+ import { IconButton } from './icon-button.js';
7
+ import { IconPicker } from './icon-picker.js';
8
+ import { LumenDrawer } from './lumen-drawer.js';
9
+ import { Button } from './ui/button.js';
10
+ import { Input } from './ui/input.js';
11
+ /** A tab's name, at most this long unless the app keeps longer ones. */
12
+ export const TAB_NAME_MAX = 30;
13
+ export function LumenTabDrawer({ open, onClose, name, icon, fallback, description = 'Its filters and columns save as you change them.', onSave, busy, error, remove, maxName = TAB_NAME_MAX, }) {
14
+ const [typed, setTyped] = useState(name);
15
+ const [picked, setPicked] = useState(icon ?? null);
16
+ // A drawer opened again starts from the tab as it is now.
17
+ useEffect(() => {
18
+ if (open) {
19
+ setTyped(name);
20
+ setPicked(icon ?? null);
21
+ }
22
+ }, [open, name, icon]);
23
+ const next = typed.trim();
24
+ const changed = next !== name || picked !== (icon ?? null);
25
+ return (_jsxs(LumenDrawer, { open: open, onClose: onClose, title: "Tab", description: description, footer: _jsxs(_Fragment, { children: [remove && (_jsx(LumenConfirm, { title: `Remove tab ${name}?`, description: remove.description, action: "Remove", onConfirm: remove.onRemove, children: _jsx(IconButton, { icon: Trash2Icon, label: "Remove tab", variant: "destructive" }) })), _jsx(Button, { variant: "outline", size: "sm", onClick: onClose, children: "Cancel" }), _jsx(SaveButton, { issues: next ? [] : [{ where: 'Name', message: 'Give it a name', target: 'lumen-tab-name' }], disabled: !changed, busy: busy, onSave: () => onSave({ name: next, icon: picked }) })] }), children: [_jsxs("div", { className: "grid grid-cols-[7rem_minmax(0,1fr)] items-center gap-2 pt-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Name" }), _jsx(Input, { id: "lumen-tab-name", className: "h-8", value: typed, maxLength: maxName, onChange: (e) => setTyped(e.target.value) }), _jsx("span", { className: "text-muted-foreground", children: "Icon" }), _jsx("div", { children: _jsx(IconPicker, { value: picked, onChange: setPicked, fallback: fallback, label: "Tab icon", variant: "outline" }) })] }), error && _jsx("p", { className: "pt-2 text-sm text-destructive", children: error })] }));
26
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { NavIcon } from './nav.js';
3
+ /**
4
+ * One tab: its icon, its name and a count or mark after it. Every tab has an icon; a tab the person edits takes theirs through `iconOf` and LumenTabDrawer.
5
+ */
6
+ export interface LumenTab<V extends string = string> {
7
+ value: V;
8
+ label: ReactNode;
9
+ icon: NavIcon;
10
+ /** After the name: a count, a dot. */
11
+ after?: ReactNode;
12
+ /** Left out of the row (a tab this page does not have). */
13
+ hidden?: boolean;
14
+ }
15
+ export declare function LumenTabs<V extends string>({ tabs, value, onChange, actions, }: {
16
+ tabs: LumenTab<V>[];
17
+ value: V | undefined;
18
+ onChange: (value: V) => void;
19
+ /** What only the open tab uses, at the row's right end. */
20
+ actions?: ReactNode;
21
+ }): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Tabs, TabsList, TabsTrigger } from './ui/tabs.js';
3
+ export function LumenTabs({ tabs, value, onChange, actions, }) {
4
+ const shown = tabs.filter((t) => !t.hidden);
5
+ return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("div", { className: "min-w-48 max-w-full flex-1", children: shown.length ? (_jsx(Tabs, { value: value, onValueChange: (v) => onChange(v), children: _jsx(TabsList, { className: "max-w-full overflow-x-auto", children: shown.map(({ value: v, label, icon: Icon, after }) => (_jsxs(TabsTrigger, { value: v, children: [_jsx(Icon, {}), label, after] }, v))) }) })) : null }), actions ? _jsx("div", { className: "ml-auto flex shrink-0 items-center gap-0.5", children: actions }) : null] }));
6
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type ExternalToast } from 'sonner';
3
+ export declare const lumenToast: ((message: ReactNode, options?: ExternalToast) => string | number) & {
4
+ success: (message: ((() => React.ReactNode) | ReactNode) | React.ReactNode, data?: ExternalToast) => string | number;
5
+ error: (message: ((() => React.ReactNode) | ReactNode) | React.ReactNode, data?: ExternalToast) => string | number;
6
+ warning: (message: ((() => React.ReactNode) | ReactNode) | React.ReactNode, data?: ExternalToast) => string | number;
7
+ info: (message: ((() => React.ReactNode) | ReactNode) | React.ReactNode, data?: ExternalToast) => string | number;
8
+ dismiss: (id?: number | string) => string | number;
9
+ /** A change done, with Undo that puts it back. */
10
+ undo: (message: ReactNode, onUndo: () => void, options?: ExternalToast) => string | number;
11
+ };
@@ -0,0 +1,16 @@
1
+ import { toast } from 'sonner';
2
+ /** How long a toast with Undo stays up, so there is time to press it. */
3
+ const UNDO_MS = 6000;
4
+ export const lumenToast = Object.assign((message, options) => toast(message, options), {
5
+ success: toast.success,
6
+ error: toast.error,
7
+ warning: toast.warning,
8
+ info: toast.info,
9
+ dismiss: toast.dismiss,
10
+ /** A change done, with Undo that puts it back. */
11
+ undo: (message, onUndo, options) => toast(message, {
12
+ duration: UNDO_MS,
13
+ ...options,
14
+ action: { label: 'Undo', onClick: onUndo },
15
+ }),
16
+ });
@@ -0,0 +1,18 @@
1
+ import { type TrendBucket, type TrendStyle } from '@columenlabs/lumen-ui-types/views';
2
+ import type { ViewCellValue, ViewField } from './view-model.js';
3
+ /** A row's date as a bucket key (`2026-10-16`, `2026-10-12` the week's Monday, `2026-10`, `2026`). */
4
+ export declare function bucketOf(v: ViewCellValue, bucket: TrendBucket): string | null;
5
+ type Agg = 'sum' | 'mean' | 'last' | 'count';
6
+ /** The rows rolled up per bucket, oldest first: `{at, v0, v1, ...}`. */
7
+ export declare function trendPoints<T>(rows: T[], date: ViewField<T>, values: ViewField<T>[], bucket: TrendBucket, agg: Agg, read: (f: ViewField<T>, r: T) => ViewCellValue): Record<string, string | number>[];
8
+ export declare function TrendBody<T>({ style, date, values, bucket, agg, rows, read, legend, }: {
9
+ style: TrendStyle;
10
+ date: ViewField<T>;
11
+ values: ViewField<T>[];
12
+ bucket: TrendBucket;
13
+ agg: Agg;
14
+ rows: T[];
15
+ read: (f: ViewField<T>, r: T) => ViewCellValue;
16
+ legend?: boolean;
17
+ }): import("react").JSX.Element;
18
+ export {};
@@ -0,0 +1,157 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Trend widgets: up to 4 number fields over a date field of the view's rows, rolled up per day, week,
3
+ // month or year, drawn as a line, area, stacked area, bars over time, combo, a spark or a tracker.
4
+ // With the allocation, donut, bars and card styles they cover every Tremor visualization, drawn on
5
+ // our own Recharts in ChartContainer. Worked out in the browser from the rows; nothing is fetched.
6
+ import { isDollarFormat, } from '@columenlabs/lumen-ui-types/views';
7
+ import { useMemo } from 'react';
8
+ import { Area, AreaChart, Bar, BarChart, CartesianGrid, ComposedChart, Line, LineChart, XAxis, YAxis, } from 'recharts';
9
+ import { cn } from './lib/utils.js';
10
+ import { Headline } from './lumen-widget-charts.js';
11
+ import { maskAmount, useAmountsHidden } from './shell.js';
12
+ import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from './ui/chart.js';
13
+ const MINUS = '−';
14
+ const COMPACT = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 });
15
+ // Chart 2 is skipped: it is green, and an app whose accent (chart 1) is green drew its first two
16
+ // series alike (an app's Income and Spend).
17
+ const COLORS = [1, 3, 4, 5].map((i) => `var(--chart-${i})`);
18
+ /** Blocks a tracker shows, the newest last. */
19
+ const TRACKER_MAX = 60;
20
+ function short(v, format, hidden) {
21
+ if (hidden)
22
+ return maskAmount('0');
23
+ const sign = v < 0 ? MINUS : '';
24
+ const abs = Math.abs(v);
25
+ if (format === 'percent' || format === 'weight')
26
+ return `${sign}${abs.toFixed(1)}%`;
27
+ if (isDollarFormat(format))
28
+ return `${sign}$${COMPACT.format(abs)}`;
29
+ return `${sign}${COMPACT.format(abs)}`;
30
+ }
31
+ /** A row's date as a bucket key (`2026-10-16`, `2026-10-12` the week's Monday, `2026-10`, `2026`). */
32
+ export function bucketOf(v, bucket) {
33
+ if (v === null || v === '')
34
+ return null;
35
+ const ms = typeof v === 'number' ? v : Date.parse(String(v).length === 10 ? `${v}T12:00:00Z` : String(v));
36
+ if (!Number.isFinite(ms))
37
+ return null;
38
+ const d = new Date(ms);
39
+ const day = d.toISOString().slice(0, 10);
40
+ if (bucket === 'day')
41
+ return day;
42
+ if (bucket === 'month')
43
+ return day.slice(0, 7);
44
+ if (bucket === 'year')
45
+ return day.slice(0, 4);
46
+ const back = (d.getUTCDay() + 6) % 7;
47
+ return new Date(ms - back * 86_400_000).toISOString().slice(0, 10);
48
+ }
49
+ /** The rows rolled up per bucket, oldest first: `{at, v0, v1, ...}`. */
50
+ export function trendPoints(rows, date, values, bucket, agg, read) {
51
+ const m = new Map();
52
+ // Rows in date order, so `last` is the latest in each bucket.
53
+ const dated = rows
54
+ .map((r) => ({ r, at: bucketOf(read(date, r), bucket), raw: String(read(date, r) ?? '') }))
55
+ .filter((x) => x.at !== null)
56
+ .sort((a, b) => a.raw.localeCompare(b.raw));
57
+ for (const { r, at } of dated) {
58
+ const b = m.get(at) ?? {
59
+ sum: values.map(() => 0),
60
+ n: values.map(() => 0),
61
+ last: values.map(() => 0),
62
+ };
63
+ values.forEach((f, i) => {
64
+ const v = read(f, r);
65
+ if (agg === 'count') {
66
+ b.n[i] = (b.n[i] ?? 0) + 1;
67
+ return;
68
+ }
69
+ if (typeof v !== 'number' || !Number.isFinite(v))
70
+ return;
71
+ b.sum[i] = (b.sum[i] ?? 0) + v;
72
+ b.n[i] = (b.n[i] ?? 0) + 1;
73
+ b.last[i] = v;
74
+ });
75
+ m.set(at, b);
76
+ }
77
+ return [...m]
78
+ .sort(([a], [b]) => a.localeCompare(b))
79
+ .map(([at, b]) => {
80
+ const out = { at };
81
+ values.forEach((_, i) => {
82
+ const n = b.n[i] ?? 0;
83
+ out[`v${i}`] =
84
+ agg === 'count'
85
+ ? n
86
+ : agg === 'mean'
87
+ ? n
88
+ ? (b.sum[i] ?? 0) / n
89
+ : 0
90
+ : agg === 'last'
91
+ ? (b.last[i] ?? 0)
92
+ : (b.sum[i] ?? 0);
93
+ });
94
+ return out;
95
+ });
96
+ }
97
+ /** A bucket key as an axis label: `Oct 16`, `Oct 2026`, `2026`. */
98
+ function tick(at, bucket) {
99
+ if (bucket === 'year')
100
+ return at;
101
+ if (bucket === 'month')
102
+ return new Date(`${at}-15T12:00:00Z`).toLocaleDateString('en-US', {
103
+ month: 'short',
104
+ year: '2-digit',
105
+ timeZone: 'UTC',
106
+ });
107
+ return new Date(`${at}T12:00:00Z`).toLocaleDateString('en-US', {
108
+ month: 'short',
109
+ day: 'numeric',
110
+ timeZone: 'UTC',
111
+ });
112
+ }
113
+ export function TrendBody({ style, date, values, bucket, agg, rows, read, legend = true, }) {
114
+ const hidden = useAmountsHidden();
115
+ const points = useMemo(() => trendPoints(rows, date, values, bucket, agg, read), [rows, date, values, bucket, agg, read]);
116
+ const first = values[0];
117
+ if (!points.length || !first)
118
+ return _jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No dated rows to draw." });
119
+ const masked = (f) => hidden && !!f.amount && agg !== 'count';
120
+ const fmt = (v, f = first) => short(v, agg === 'count' ? 'rounded' : f.format, masked(f));
121
+ const config = Object.fromEntries(values.map((f, i) => [
122
+ `v${i}`,
123
+ { label: agg === 'count' ? `${f.label} (count)` : f.label, color: COLORS[i] },
124
+ ]));
125
+ const last = Number(points[points.length - 1]?.v0 ?? 0);
126
+ const start = Number(points[0]?.v0 ?? 0);
127
+ if (style === 'tracker') {
128
+ // One block per bucket, the newest last: green where the first value rose or is positive.
129
+ const shown = points.slice(-TRACKER_MAX);
130
+ const up = shown.filter((p) => Number(p.v0) > 0).length;
131
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Headline, { value: fmt(last), caption: `${first.label} · ${up} of ${shown.length} ${bucket}s up`, tone: last > 0 ? 'gain' : last < 0 ? 'loss' : undefined }), _jsx("div", { className: "flex h-8 w-full gap-0.5", role: "img", "aria-label": `${first.label} by ${bucket}`, children: shown.map((p) => {
132
+ const v = Number(p.v0);
133
+ return (_jsx("span", { title: `${tick(String(p.at), bucket)} · ${fmt(v)}`, className: cn('h-full min-w-0 flex-1 rounded-[2px]', v > 0 ? 'bg-gain' : v < 0 ? 'bg-loss' : 'bg-muted') }, String(p.at)));
134
+ }) })] }));
135
+ }
136
+ if (style === 'spark') {
137
+ const change = last - start;
138
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Headline, { value: fmt(last), caption: `${change >= 0 ? '+' : ''}${fmt(change)} since ${tick(String(points[0]?.at), bucket)}`, tone: change > 0 ? 'gain' : change < 0 ? 'loss' : undefined }), _jsx(ChartContainer, { config: config, className: "aspect-auto h-12 w-full", children: _jsx(AreaChart, { data: points, margin: { left: 0, right: 0, top: 2, bottom: 2 }, children: _jsx(Area, { dataKey: "v0", type: "monotone", stroke: "var(--color-v0)", fill: "var(--color-v0)", fillOpacity: 0.15, strokeWidth: 1.5, isAnimationActive: false }) }) })] }));
139
+ }
140
+ const axes = [
141
+ _jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }, "g"),
142
+ _jsx(XAxis, { dataKey: "at", tickLine: false, axisLine: false, minTickGap: 24, tickFormatter: (v) => tick(String(v), bucket), fontSize: 11 }, "x"),
143
+ _jsx(YAxis, { tickLine: false, axisLine: false, width: 48, tickFormatter: (v) => fmt(Number(v)), fontSize: 11 }, "y"),
144
+ _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { labelFormatter: (v) => tick(String(v), bucket), formatter: (v, name) => {
145
+ const i = Number(String(name).slice(1));
146
+ const f = values[i] ?? first;
147
+ return (_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: COLORS[i] } }), _jsx("span", { className: "truncate", children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v), f) })] }));
148
+ } }) }, "t"),
149
+ ...(legend && values.length > 1
150
+ ? [_jsx(ChartLegend, { content: _jsx(ChartLegendContent, {}) }, "l")]
151
+ : []),
152
+ ];
153
+ const lead = (_jsx(Headline, { value: fmt(last), caption: `${config.v0?.label} · ${tick(String(points[points.length - 1]?.at), bucket)}` }));
154
+ const box = 'aspect-auto h-56 w-full';
155
+ const keys = values.map((_, i) => `v${i}`);
156
+ return (_jsxs("div", { className: "flex flex-col", children: [lead, _jsx(ChartContainer, { config: config, className: box, children: style === 'bars' ? (_jsxs(BarChart, { data: points, margin: { left: 0, right: 4 }, children: [axes, keys.map((k) => (_jsx(Bar, { dataKey: k, fill: `var(--color-${k})`, radius: 2, isAnimationActive: false }, k)))] })) : style === 'combo' ? (_jsxs(ComposedChart, { data: points, margin: { left: 0, right: 4 }, children: [axes, keys.map((k, i) => i === 0 ? (_jsx(Bar, { dataKey: k, fill: `var(--color-${k})`, radius: 2, isAnimationActive: false }, k)) : (_jsx(Line, { dataKey: k, type: "monotone", stroke: `var(--color-${k})`, strokeWidth: 1.5, dot: false, isAnimationActive: false }, k)))] })) : style === 'area' || style === 'stacked' ? (_jsxs(AreaChart, { data: points, margin: { left: 0, right: 4 }, children: [axes, keys.map((k) => (_jsx(Area, { dataKey: k, type: "monotone", stackId: style === 'stacked' ? 'a' : undefined, stroke: `var(--color-${k})`, fill: `var(--color-${k})`, fillOpacity: 0.15, strokeWidth: 1.5, isAnimationActive: false }, k)))] })) : (_jsxs(LineChart, { data: points, margin: { left: 0, right: 4 }, children: [axes, keys.map((k) => (_jsx(Line, { dataKey: k, type: "monotone", stroke: `var(--color-${k})`, strokeWidth: 1.5, dot: false, isAnimationActive: false }, k)))] })) })] }));
157
+ }