@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,34 @@
1
+ import { type ChartBar, type TimeSpan } from './price-chart.js';
2
+ /** One symbol's line: its bars (closes are drawn) and its colour token (`var(--chart-1)`). */
3
+ export interface CompareLine {
4
+ symbol: string;
5
+ color: string;
6
+ bars: ChartBar[];
7
+ }
8
+ /** An indicator line of one symbol: values per bar time, in its symbol's colour. */
9
+ export interface CompareExtra {
10
+ key: string;
11
+ label: string;
12
+ symbol: string;
13
+ color: string;
14
+ values?: Map<string | number, number>;
15
+ }
16
+ export interface ComparePane {
17
+ id: string;
18
+ label: string;
19
+ domain?: [number, number];
20
+ refs?: number[];
21
+ lines: CompareExtra[];
22
+ }
23
+ export declare function CompareChart({ lines, mode, overlays, panes, timeZone, timeline, range, onRange, onOutside, }: {
24
+ lines: CompareLine[];
25
+ mode: 'pct' | 'price';
26
+ overlays: CompareExtra[];
27
+ panes: ComparePane[];
28
+ timeZone?: string;
29
+ /** The navigator's bars: the first symbol's whole history. */
30
+ timeline: ChartBar[];
31
+ range?: TimeSpan;
32
+ onRange?: (span: TimeSpan, byPerson: boolean) => void;
33
+ onOutside?: (span: TimeSpan) => void;
34
+ }): import("react").JSX.Element;
@@ -0,0 +1,292 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The compare chart: two to eight symbols on
4
+ * one time axis. In % each line is its change from the left edge of what is visible, so panning,
5
+ * zooming or dragging the timeline moves the baseline and every % re-reads from there; in Price each
6
+ * symbol sits on its own scale. Overlays (SMA 50 on NVDA) follow their symbol's colour, dashed and,
7
+ * in %, measured from the same baseline as their symbol; panes draw one line per symbol.
8
+ */
9
+ import { ColorType, CrosshairMode, LineSeries, createChart, } from 'lightweight-charts';
10
+ import { useEffect, useMemo, useRef, useState } from 'react';
11
+ import { AXIS, Navigator, OSC_PANE, PRICE_PANE, price, secOf, showSpan, timeText, useTheme, } from './price-chart.js';
12
+ const MINUS = '−';
13
+ const pct = (n) => `${n >= 0 ? '+' : MINUS}${Math.abs(n).toFixed(2)}%`;
14
+ /** First index of `secs` at or after `t`; the last when none is. */
15
+ function from(secs, t) {
16
+ let lo = 0;
17
+ let hi = secs.length;
18
+ while (lo < hi) {
19
+ const mid = (lo + hi) >> 1;
20
+ if (secs[mid] < t)
21
+ lo = mid + 1;
22
+ else
23
+ hi = mid;
24
+ }
25
+ return Math.min(lo, secs.length - 1);
26
+ }
27
+ /** Last index of `secs` at or before `t`; -1 when none is. */
28
+ function upTo(secs, t) {
29
+ let lo = 0;
30
+ let hi = secs.length;
31
+ while (lo < hi) {
32
+ const mid = (lo + hi) >> 1;
33
+ if (secs[mid] <= t)
34
+ lo = mid + 1;
35
+ else
36
+ hi = mid;
37
+ }
38
+ return lo - 1;
39
+ }
40
+ export function CompareChart({ lines, mode, overlays, panes, timeZone = 'America/Los_Angeles', timeline, range, onRange, onOutside, }) {
41
+ const host = useRef(null);
42
+ const theme = useTheme();
43
+ // One time axis: every time any symbol has a bar.
44
+ const axis = useMemo(() => {
45
+ const seen = new Map();
46
+ for (const l of lines)
47
+ for (const b of l.bars)
48
+ seen.set(secOf(b.time), b.time);
49
+ return [...seen.entries()].sort((a, b) => a[0] - b[0]);
50
+ }, [lines]);
51
+ const secs = useMemo(() => axis.map(([s]) => s), [axis]);
52
+ const axisBars = useMemo(() => axis.map(([, t]) => ({ time: t, open: 0, high: 0, low: 0, close: 0 })), [axis]);
53
+ const own = useMemo(() => lines.map((l) => l.bars.map((b) => secOf(b.time))), [lines]);
54
+ const intraday = typeof axis[0]?.[1] === 'number';
55
+ const multiDay = intraday && secs.length > 1 && (secs.at(-1) ?? 0) - (secs[0] ?? 0) > 86_400;
56
+ const target = useRef(null);
57
+ const applied = useRef(undefined);
58
+ const touch = useRef({ down: false, at: 0 });
59
+ const report = useRef(onRange);
60
+ report.current = onRange;
61
+ const [api, setApi] = useState(null);
62
+ const live = useRef(null);
63
+ const [legend, setLegend] = useState({ date: '', rows: [] });
64
+ const height = PRICE_PANE + panes.length * OSC_PANE + AXIS;
65
+ // biome-ignore lint/correctness/useExhaustiveDependencies: range is applied by the effect below
66
+ useEffect(() => {
67
+ const el = host.current;
68
+ if (!el || !secs.length)
69
+ return;
70
+ const css = (c) => theme.css(c.replace(/^var\((.*)\)$/, '$1'));
71
+ const chart = createChart(el, {
72
+ autoSize: true,
73
+ layout: {
74
+ background: { type: ColorType.Solid, color: 'transparent' },
75
+ textColor: theme.text,
76
+ fontFamily: theme.css('--font-mono'),
77
+ fontSize: 11,
78
+ panes: { separatorColor: theme.border, enableResize: false },
79
+ },
80
+ grid: { vertLines: { visible: false }, horzLines: { color: theme.grid } },
81
+ rightPriceScale: { borderVisible: false },
82
+ timeScale: {
83
+ borderVisible: false,
84
+ rightOffset: 2,
85
+ minBarSpacing: 0.001,
86
+ timeVisible: intraday,
87
+ tickMarkFormatter: intraday ? (t) => timeText(t, timeZone, false) : undefined,
88
+ },
89
+ localization: { timeFormatter: (t) => timeText(t, timeZone, true) },
90
+ crosshair: { mode: CrosshairMode.Normal },
91
+ handleScale: { mouseWheel: true, pinch: true },
92
+ handleScroll: { mouseWheel: false },
93
+ });
94
+ const percent = mode === 'pct';
95
+ const format = percent
96
+ ? { type: 'custom', formatter: (v) => pct(v) }
97
+ : { type: 'price', precision: 2, minMove: 0.01 };
98
+ const scaleOf = (i) => (percent || i === 0 ? 'right' : `p${i}`);
99
+ // The price pane: each symbol, then its overlays, which take its baseline in %.
100
+ const priced = lines.map((l, i) => {
101
+ const s = chart.addSeries(LineSeries, {
102
+ color: css(l.color),
103
+ lineWidth: 2,
104
+ priceLineVisible: false,
105
+ lastValueVisible: true,
106
+ priceScaleId: scaleOf(i),
107
+ priceFormat: format,
108
+ }, 0);
109
+ if (scaleOf(i) !== 'right')
110
+ chart.priceScale(scaleOf(i)).applyOptions({ visible: false, borderVisible: false });
111
+ return s;
112
+ });
113
+ const over = overlays.map((o) => {
114
+ const i = lines.findIndex((l) => l.symbol === o.symbol);
115
+ return {
116
+ o,
117
+ i,
118
+ s: chart.addSeries(LineSeries, {
119
+ color: css(o.color),
120
+ lineWidth: 1,
121
+ lineStyle: 2,
122
+ priceLineVisible: false,
123
+ lastValueVisible: false,
124
+ crosshairMarkerVisible: false,
125
+ priceScaleId: scaleOf(Math.max(0, i)),
126
+ priceFormat: format,
127
+ }, 0),
128
+ };
129
+ });
130
+ if (percent)
131
+ priced[0]?.createPriceLine({
132
+ price: 0,
133
+ color: theme.text,
134
+ lineWidth: 1,
135
+ lineStyle: 2,
136
+ axisLabelVisible: false,
137
+ });
138
+ // Each symbol's baseline: its close at or after the axis bar `a`.
139
+ let base = -1;
140
+ const draw = (a) => {
141
+ if (a === base)
142
+ return;
143
+ base = a;
144
+ const t0 = secs[a] ?? 0;
145
+ const bases = lines.map((l, i) => {
146
+ const k = l.bars.length ? from(own[i] ?? [], t0) : -1;
147
+ return l.bars[k]?.close ?? 1;
148
+ });
149
+ const val = (v, i) => (percent ? (v / (bases[i] ?? 1) - 1) * 100 : v);
150
+ lines.forEach((l, i) => priced[i]?.setData(l.bars.map((b) => ({ time: b.time, value: val(b.close, i) }))));
151
+ for (const { o, i, s } of over) {
152
+ const bars = lines[i]?.bars ?? [];
153
+ s.setData(bars.flatMap((b) => {
154
+ const v = o.values?.get(b.time);
155
+ return v === undefined ? [] : [{ time: b.time, value: val(v, i) }];
156
+ }));
157
+ }
158
+ };
159
+ draw(0);
160
+ panes.forEach((p, n) => {
161
+ let anchor = null;
162
+ for (const x of p.lines) {
163
+ const bars = lines.find((l) => l.symbol === x.symbol)?.bars ?? [];
164
+ const s = chart.addSeries(LineSeries, { color: css(x.color), lineWidth: 2, priceLineVisible: false, lastValueVisible: false }, n + 1);
165
+ s.setData(bars.flatMap((b) => {
166
+ const v = x.values?.get(b.time);
167
+ return v === undefined ? [] : [{ time: b.time, value: v }];
168
+ }));
169
+ const domain = p.domain;
170
+ if (domain)
171
+ s.applyOptions({
172
+ autoscaleInfoProvider: () => ({
173
+ priceRange: { minValue: domain[0], maxValue: domain[1] },
174
+ }),
175
+ });
176
+ anchor ??= s;
177
+ }
178
+ for (const r of p.refs ?? [])
179
+ anchor?.createPriceLine({
180
+ price: r,
181
+ color: theme.border,
182
+ lineWidth: 1,
183
+ lineStyle: 2,
184
+ axisLabelVisible: false,
185
+ });
186
+ });
187
+ chart.panes()[0]?.setStretchFactor(PRICE_PANE);
188
+ panes.forEach((_, n) => chart.panes()[n + 1]?.setStretchFactor(OSC_PANE));
189
+ if (range && range !== applied.current) {
190
+ applied.current = range;
191
+ target.current = range;
192
+ }
193
+ target.current ??= range ?? { from: secs[0] ?? 0, to: Number.POSITIVE_INFINITY };
194
+ const show = () => target.current && showSpan(chart, secs, target.current);
195
+ show();
196
+ let frame = 0;
197
+ const sized = new ResizeObserver(() => {
198
+ cancelAnimationFrame(frame);
199
+ frame = requestAnimationFrame(show);
200
+ });
201
+ sized.observe(el);
202
+ const press = () => {
203
+ touch.current.down = true;
204
+ };
205
+ const release = () => {
206
+ if (touch.current.down)
207
+ touch.current = { down: false, at: performance.now() };
208
+ };
209
+ const wheel = () => {
210
+ touch.current.at = performance.now();
211
+ };
212
+ el.addEventListener('pointerdown', press);
213
+ el.addEventListener('wheel', wheel, { passive: true });
214
+ window.addEventListener('pointerup', release);
215
+ window.addEventListener('pointercancel', release);
216
+ // The values at the axis bar `i`: each symbol's last close at or before it.
217
+ let shownEnd = secs.length - 1;
218
+ const legendAt = (i) => {
219
+ const t = secs[i] ?? 0;
220
+ const rows = lines.map((l, k) => {
221
+ const j = upTo(own[k] ?? [], t);
222
+ const c = l.bars[j]?.close;
223
+ const b = l.bars[from(own[k] ?? [], secs[base] ?? 0)]?.close;
224
+ return {
225
+ s: l.symbol,
226
+ c: css(l.color),
227
+ v: c === undefined ? '—' : percent && b ? pct((c / b - 1) * 100) : price(c),
228
+ };
229
+ });
230
+ const at = axis[i]?.[1];
231
+ setLegend({ date: at === undefined ? '' : timeText(at, timeZone, true), rows });
232
+ };
233
+ let pending = 0;
234
+ const moved = (r) => {
235
+ if (!r || !secs.length)
236
+ return;
237
+ const last = secs.length - 1;
238
+ const a = Math.max(0, Math.min(last, Math.round(r.from)));
239
+ const b = Math.max(a, Math.min(last, Math.round(r.to)));
240
+ shownEnd = b;
241
+ cancelAnimationFrame(pending);
242
+ pending = requestAnimationFrame(() => {
243
+ draw(a);
244
+ legendAt(shownEnd);
245
+ });
246
+ const span = { from: secs[a], to: secs[b] };
247
+ const person = touch.current.down || performance.now() - touch.current.at < 400;
248
+ if (person)
249
+ target.current = span;
250
+ report.current?.(span, person);
251
+ };
252
+ chart.timeScale().subscribeVisibleLogicalRangeChange(moved);
253
+ chart.subscribeCrosshairMove((p) => {
254
+ const i = p.time === undefined ? -1 : from(secs, secOf(p.time));
255
+ legendAt(i < 0 ? shownEnd : i);
256
+ });
257
+ legendAt(shownEnd);
258
+ live.current = chart;
259
+ setApi(chart);
260
+ return () => {
261
+ sized.disconnect();
262
+ cancelAnimationFrame(frame);
263
+ cancelAnimationFrame(pending);
264
+ el.removeEventListener('pointerdown', press);
265
+ el.removeEventListener('wheel', wheel);
266
+ window.removeEventListener('pointerup', release);
267
+ window.removeEventListener('pointercancel', release);
268
+ chart.remove();
269
+ };
270
+ }, [lines, own, secs, axis, mode, overlays, panes, theme, timeZone, intraday]);
271
+ useEffect(() => {
272
+ if (!api || !range || range === applied.current)
273
+ return;
274
+ applied.current = range;
275
+ target.current = range;
276
+ showSpan(api, secs, range);
277
+ }, [api, range, secs]);
278
+ const goTo = (span) => {
279
+ touch.current.at = performance.now();
280
+ const day = 86_400;
281
+ if (onOutside &&
282
+ secs.length &&
283
+ (span.from < secs[0] - day ||
284
+ (Number.isFinite(span.to) && span.to > secs.at(-1) + day))) {
285
+ onOutside(span);
286
+ return;
287
+ }
288
+ if (live.current)
289
+ showSpan(live.current, secs, span);
290
+ };
291
+ return (_jsxs("div", { "data-testid": "compare-chart", "data-mode": mode, className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex min-h-5 flex-wrap items-center gap-x-4 gap-y-0.5 px-2 text-xs", children: [_jsx("span", { className: "num text-muted-foreground", children: legend.date }), legend.rows.map((r) => (_jsxs("span", { className: "num flex items-center gap-1.5 whitespace-nowrap", children: [_jsx("span", { "aria-hidden": true, className: "inline-block size-2 rounded-full", style: { background: r.c } }), _jsx("span", { className: "text-muted-foreground", children: r.s }), _jsx("span", { className: r.v.startsWith('+') ? 'text-gain' : r.v.startsWith(MINUS) ? 'text-loss' : '', children: r.v })] }, r.s)))] }), _jsxs("div", { className: "relative", children: [_jsx("div", { ref: host, style: { height } }), _jsx("div", { className: "pointer-events-none absolute inset-0 text-muted-foreground text-xs", children: panes.map((p, n) => (_jsx("div", { className: "absolute inset-x-0", style: { top: PRICE_PANE + n * OSC_PANE, height: OSC_PANE }, children: _jsx("h3", { className: "px-2 pt-2", children: p.label }) }, p.id))) })] }), api && timeline.length > 2 && (_jsx(Navigator, { chart: api, bars: axisBars, secs: secs, timeline: timeline, theme: theme, timeZone: timeZone, multiDay: multiDay, goTo: goTo }))] }));
292
+ }
@@ -0,0 +1,29 @@
1
+ import { type ChartFormula } from '@columenlabs/lumen-ui-types/chart-formula';
2
+ import { type ChartIndicator } from './indicators.js';
3
+ /** "For" in the compare chart: every symbol shown, or one of them. */
4
+ export declare const ALL_SHOWN = "all";
5
+ /** Saved formulas the picker lists first, and how to keep a new one. */
6
+ type Mine = {
7
+ list: ChartIndicator[];
8
+ save?: (f: ChartFormula) => Promise<void>;
9
+ remove?: (id: string) => Promise<void>;
10
+ };
11
+ /** "Indicators" button and a searchable list of everything the chart can draw. */
12
+ export declare function IndicatorPicker({ selected, available, reasons, targets, target, onTarget, onToggle, mine, onAdd, }: {
13
+ /** Indicator ids picked (for the current `target`). */
14
+ selected: string[];
15
+ /** Indicator ids this chart has values for. */
16
+ available: Set<string>;
17
+ /** Why an indicator is not available here, by id; left out: "no values". */
18
+ reasons?: Map<string, string>;
19
+ /** The symbols on a compare chart: the "For" picker shows All shown and each of them. */
20
+ targets?: string[];
21
+ target?: string;
22
+ onTarget?: (target: string) => void;
23
+ onToggle: (id: string) => void;
24
+ /** The person's saved formulas (Mine); left out: none. */
25
+ mine?: Mine;
26
+ /** Adds a built-in with its own window or a formula; left out: only the catalog is offered. */
27
+ onAdd?: (i: ChartIndicator) => void;
28
+ }): import("react").JSX.Element;
29
+ export {};
@@ -0,0 +1,125 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { parseChartFormula } from '@columenlabs/lumen-ui-types/chart-formula';
3
+ import { PlusIcon, SaveIcon, Trash2Icon } from 'lucide-react';
4
+ import { useState } from 'react';
5
+ import { LumenConfirm } from '../confirm.js';
6
+ import { IconButton } from '../icon-button.js';
7
+ import { LumenSelector } from '../lumen-selector.js';
8
+ import { Field, FieldDescription, FieldLabel } from '../ui/field.js';
9
+ import { Input } from '../ui/input.js';
10
+ import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group.js';
11
+ import { CHART_INDICATORS, formulaIndicator, resolveIndicator, } from './indicators.js';
12
+ const GROUPS = [
13
+ { heading: 'On price', has: (i) => !i.sourced && i.where === 'overlay' },
14
+ { heading: 'Below price', has: (i) => !i.sourced && i.where === 'pane' },
15
+ { heading: 'From market data', has: (i) => !!i.sourced },
16
+ ];
17
+ /** "For" in the compare chart: every symbol shown, or one of them. */
18
+ export const ALL_SHOWN = 'all';
19
+ /**
20
+ * A built-in with its own window, typed in the search (`ema 13`, `rsi 7`, `bollinger 20 2.5`,
21
+ * `macd 8 21 5`); null when the search is not one.
22
+ */
23
+ function typedBuiltin(search) {
24
+ const m = /^\s*(sma|ema|rsi|atr|bollinger|bb|macd)[\s_,]+([\d.\s,_]+)$/i.exec(search);
25
+ if (!m)
26
+ return null;
27
+ const kind = m[1].toLowerCase() === 'bb' ? 'bollinger' : m[1].toLowerCase();
28
+ const nums = m[2].split(/[\s,_]+/).filter(Boolean);
29
+ const id = [kind, ...nums.map((n) => n.replace('.', 'p'))].join('_');
30
+ const i = resolveIndicator(id);
31
+ return i && !i.sourced ? i : null;
32
+ }
33
+ /** "Indicators" button and a searchable list of everything the chart can draw. */
34
+ export function IndicatorPicker({ selected, available, reasons, targets, target = ALL_SHOWN, onTarget, onToggle, mine, onAdd, }) {
35
+ const [label, setLabel] = useState('');
36
+ const [f, setF] = useState('');
37
+ const [busy, setBusy] = useState(false);
38
+ const [error, setError] = useState(null);
39
+ const [removing, setRemoving] = useState(null);
40
+ // The new formula checked as it is typed: how many bars it needs first, or why it can't run.
41
+ let check = null;
42
+ if (f.trim()) {
43
+ try {
44
+ const { lookback } = parseChartFormula(f);
45
+ check = { ok: true, text: `Checks out · needs ${lookback} bars before its first value` };
46
+ }
47
+ catch (e) {
48
+ check = { ok: false, text: e instanceof Error ? e.message : String(e) };
49
+ }
50
+ }
51
+ const formula = check?.ok && label.trim() ? { label: label.trim(), f: f.trim() } : null;
52
+ const addFormula = () => {
53
+ if (!formula || !onAdd)
54
+ return;
55
+ onAdd(formulaIndicator(formula));
56
+ setLabel('');
57
+ setF('');
58
+ };
59
+ const saveFormula = () => {
60
+ if (!formula || !mine?.save)
61
+ return;
62
+ setBusy(true);
63
+ setError(null);
64
+ mine
65
+ .save(formula)
66
+ .then(() => {
67
+ setLabel('');
68
+ setF('');
69
+ })
70
+ .catch((e) => setError(e.message))
71
+ .finally(() => setBusy(false));
72
+ };
73
+ const byId = new Map([...(mine?.list ?? []), ...CHART_INDICATORS].map((i) => [i.id, i]));
74
+ const option = (i, extra) => {
75
+ const has = available.has(i.id) || !CHART_INDICATORS.some((c) => c.id === i.id);
76
+ return {
77
+ value: i.id,
78
+ label: i.formula ? `ƒ ${i.label}` : i.label,
79
+ keywords: i.f ? [i.f] : undefined,
80
+ disabled: has ? false : (reasons?.get(i.id) ?? 'no values'),
81
+ detail: has ? undefined : (reasons?.get(i.id) ?? 'no values'),
82
+ content: (_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "shrink-0", children: i.formula ? `ƒ ${i.label}` : i.label }), i.f && _jsx("span", { className: "truncate font-mono text-xs text-muted-foreground", children: i.f }), extra] })),
83
+ };
84
+ };
85
+ const groups = [
86
+ ...(mine && mine.list.length > 0
87
+ ? [
88
+ {
89
+ label: 'Mine',
90
+ options: mine.list.map((i) => option(i, mine.remove && (_jsx(IconButton, { icon: Trash2Icon, size: "icon-xs", label: `Delete ${i.label}`, className: "ml-auto text-muted-foreground hover:text-destructive", onPointerDown: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onClick: (e) => {
91
+ e.stopPropagation();
92
+ e.preventDefault();
93
+ setRemoving(i);
94
+ } })))),
95
+ },
96
+ ]
97
+ : []),
98
+ ...GROUPS.map((g) => ({
99
+ label: g.heading,
100
+ options: CHART_INDICATORS.filter(g.has).map((i) => option(i)),
101
+ })),
102
+ ];
103
+ // A picked row adds a built-in with its own window (or toggles one already on); unpicking turns
104
+ // it off, as the list's rows did.
105
+ const change = (next) => {
106
+ for (const id of selected)
107
+ if (!next.includes(id))
108
+ onToggle(id);
109
+ for (const id of next) {
110
+ if (selected.includes(id))
111
+ continue;
112
+ const i = byId.get(id) ?? resolveIndicator(id, mine?.list);
113
+ if (onAdd && i)
114
+ onAdd(i);
115
+ else
116
+ onToggle(id);
117
+ }
118
+ };
119
+ return (_jsxs(_Fragment, { children: [_jsx(LumenSelector, { multiple: true, trigger: "icon", icon: PlusIcon, label: "Add indicators", align: "end", search: true, empty: "No indicator matches.", groups: groups, value: selected, onChange: change, create: onAdd ? (q) => typedBuiltin(q)?.id ?? null : undefined, createLabel: (q) => typedBuiltin(q)?.label ?? 'Type a window: ema 13, rsi 7', header: _jsxs(_Fragment, { children: [targets && targets.length > 1 && onTarget && (_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2 text-xs text-muted-foreground", children: ["For", _jsxs(ToggleGroup, { type: "single", size: "sm", variant: "outline", spacing: 0, value: target, onValueChange: (v) => v && onTarget(v), "aria-label": "For", className: "flex-wrap", children: [_jsx(ToggleGroupItem, { value: ALL_SHOWN, className: "px-2.5", children: "All shown" }), targets.map((t) => (_jsx(ToggleGroupItem, { value: t, className: "px-2.5 font-mono", children: t }, t)))] })] })), onAdd && (_jsx("div", { className: "flex flex-col gap-2 border-b p-3", children: _jsxs(Field, { children: [_jsx(FieldLabel, { children: "New formula" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { "aria-label": "Name", placeholder: "Name", className: "h-8 flex-1", value: label, maxLength: 60, onChange: (e) => setLabel(e.target.value) }), _jsx(IconButton, { icon: PlusIcon, label: "Add to the chart", variant: "outline", disabled: !formula, onClick: addFormula }), mine?.save && (_jsx(IconButton, { icon: SaveIcon, label: "Save to Mine", variant: "outline", disabled: !formula || busy, busy: busy, onClick: saveFormula }))] }), _jsx(Input, { "aria-label": "Formula", placeholder: "(ema(close, 9) - ema(close, 21)) / close * 100", className: "h-8 font-mono", value: f, maxLength: 400, onChange: (e) => setF(e.target.value), onKeyDown: (e) => e.key === 'Enter' && addFormula() }), _jsx(FieldDescription, { className: check && !check.ok ? 'text-loss' : undefined, children: error ??
120
+ check?.text ??
121
+ 'Fields open high low close · sma ema rsi stdev highest lowest sum ref change (x, n) · atr(n) · abs min max' })] }) }))] }) }), removing && mine?.remove && (_jsx(LumenConfirm, { open: true, onOpenChange: (o) => !o && setRemoving(null), title: `Delete ${removing.label}?`, description: "It leaves Mine on every desk.", onConfirm: () => {
122
+ void mine.remove?.(removing.id).catch((e) => setError(e.message));
123
+ setRemoving(null);
124
+ } }))] }));
125
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Indicators the chart can draw (moved from an app's Chart tab, 2026-10-12). `lines` are output
3
+ * series keyed like an app's stored `indicators_1d` names, so an app with stored values (an app's
4
+ * daily chart) passes those. The chart dialog makes each one only when it is added: `computed` ones in the browser with indicatorts over warm-up bars before the window,
5
+ * `sourced` ones (P/E, IV, EPS estimate) read once from market data, since bars cannot give them.
6
+ */
7
+ import { type ChartFormula, type ChartIndicatorId, type FormulaNode, type IndicatorSpec } from '@columenlabs/lumen-ui-types/chart-formula';
8
+ export type ChartIndicator = {
9
+ id: string;
10
+ label: string;
11
+ /** overlay: drawn on the price; pane: its own chart under the price. */
12
+ where: 'overlay' | 'pane';
13
+ lines: {
14
+ key: string;
15
+ label: string;
16
+ bar?: boolean;
17
+ }[];
18
+ /** Reference levels drawn as dashed lines in a pane. */
19
+ refs?: number[];
20
+ /** Fixed value axis for a pane; otherwise it follows the data. */
21
+ domain?: [number, number];
22
+ /** The browser can work it out from bars alone (no volume). */
23
+ computed?: boolean;
24
+ /** Read from market data when added (`market/indicators`); daily, stocks and ETFs only. */
25
+ sourced?: boolean;
26
+ /** Only on daily bars (52 weeks is a count of days). */
27
+ daily?: boolean;
28
+ /** A formula's tree (the assistant's or the person's), run over the bars; `lookback` bars come first. */
29
+ formula?: FormulaNode;
30
+ /** A saved formula's text, for the picker and the assistant. */
31
+ f?: string;
32
+ };
33
+ /** Every catalog id is in `CHART_INDICATOR_IDS`, which the server checks the assistant's specs against. */
34
+ export declare const CHART_INDICATORS: (ChartIndicator & {
35
+ id: ChartIndicatorId;
36
+ })[];
37
+ /** Colour of each line, fixed by its place in the catalog so a new pick never recolours the others. */
38
+ export declare const LINE_COLOR: Record<string, string>;
39
+ /** A line's colour: its catalog place, else one picked from its key so it stays put. */
40
+ export declare function lineColor(key: string): string;
41
+ /** A formula's id on the chart: the same label and formula are the same line. */
42
+ export declare function formulaId(f: ChartFormula): string;
43
+ /** A formula as a chart line; throws with why it can't run. */
44
+ export declare function formulaIndicator(f: ChartFormula, id?: string): ChartIndicator;
45
+ /**
46
+ * What a spec draws: a catalog indicator, a built-in with any window, a saved formula (`mine:<id>`,
47
+ * looked up in `mine`) or a formula; null when it is none of them.
48
+ */
49
+ export declare function resolveIndicator(spec: IndicatorSpec, mine?: ChartIndicator[]): ChartIndicator | null;
50
+ /** Values per line key, per bar time. */
51
+ export type IndicatorValues = Record<string, Map<string | number, number>>;
52
+ interface Ohlc {
53
+ time: string | number;
54
+ open: number;
55
+ high: number;
56
+ low: number;
57
+ close: number;
58
+ }
59
+ /** A formula's values per bar (NaN where it has none yet), with indicatorts doing the series math. */
60
+ export declare function runFormula(node: FormulaNode, bars: Ohlc[]): number[];
61
+ /** The chosen indicators' lines worked out from `bars`, skipping each one's warm-up bars. */
62
+ export declare function computeIndicators(bars: Ohlc[], chosen: ChartIndicator[]): IndicatorValues;
63
+ export {};