@columenlabs/lumen-ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +12 -0
  3. package/README.md +4 -2
  4. package/dist/add-column.d.ts +23 -0
  5. package/dist/add-column.js +62 -0
  6. package/dist/api-fetch.d.ts +9 -0
  7. package/dist/api-fetch.js +60 -0
  8. package/dist/app-switcher.d.ts +21 -0
  9. package/dist/app-switcher.js +12 -0
  10. package/dist/app-views.d.ts +18 -0
  11. package/dist/app-views.js +41 -0
  12. package/dist/assistant-context.d.ts +72 -0
  13. package/dist/assistant-context.js +30 -0
  14. package/dist/assistant-panel.d.ts +58 -0
  15. package/dist/assistant-panel.js +244 -0
  16. package/dist/assistant-screen.d.ts +124 -0
  17. package/dist/assistant-screen.js +252 -0
  18. package/dist/card-switch.d.ts +16 -0
  19. package/dist/card-switch.js +16 -0
  20. package/dist/color-dot.d.ts +14 -0
  21. package/dist/color-dot.js +19 -0
  22. package/dist/confirm.d.ts +18 -0
  23. package/dist/confirm.js +5 -0
  24. package/dist/connections.d.ts +45 -0
  25. package/dist/connections.js +275 -0
  26. package/dist/coverage-calendar-map.d.ts +27 -0
  27. package/dist/coverage-calendar-map.js +84 -0
  28. package/dist/coverage-cells.d.ts +55 -0
  29. package/dist/coverage-cells.js +85 -0
  30. package/dist/coverage-map.d.ts +112 -0
  31. package/dist/coverage-map.js +428 -0
  32. package/dist/details-table.d.ts +20 -0
  33. package/dist/details-table.js +13 -0
  34. package/dist/drawer-section.d.ts +27 -0
  35. package/dist/drawer-section.js +23 -0
  36. package/dist/floating-head.d.ts +2 -0
  37. package/dist/floating-head.js +57 -0
  38. package/dist/fold.d.ts +16 -0
  39. package/dist/fold.js +70 -0
  40. package/dist/form-issues.d.ts +32 -0
  41. package/dist/form-issues.js +60 -0
  42. package/dist/formula-input.d.ts +17 -0
  43. package/dist/formula-input.js +104 -0
  44. package/dist/frame-script.d.ts +7 -0
  45. package/dist/frame-script.js +103 -0
  46. package/dist/freshness.d.ts +24 -0
  47. package/dist/freshness.js +99 -0
  48. package/dist/group-button.d.ts +33 -0
  49. package/dist/group-button.js +67 -0
  50. package/dist/growth-chart.d.ts +77 -0
  51. package/dist/growth-chart.js +195 -0
  52. package/dist/hooks/use-keyboard-fit.d.ts +15 -0
  53. package/dist/hooks/use-keyboard-fit.js +29 -0
  54. package/dist/hooks/use-mobile.d.ts +1 -0
  55. package/dist/hooks/use-mobile.js +15 -0
  56. package/dist/icon-button.d.ts +21 -0
  57. package/dist/icon-button.js +22 -0
  58. package/dist/icon-picker.d.ts +14 -0
  59. package/dist/icon-picker.js +36 -0
  60. package/dist/index.d.ts +83 -0
  61. package/dist/index.js +90 -0
  62. package/dist/integrations.d.ts +57 -0
  63. package/dist/integrations.js +73 -0
  64. package/dist/joins.d.ts +128 -0
  65. package/dist/joins.js +767 -0
  66. package/dist/kit-config.d.ts +13 -0
  67. package/dist/kit-config.js +15 -0
  68. package/dist/lib/utils.d.ts +2 -0
  69. package/dist/lib/utils.js +5 -0
  70. package/dist/lumen-boundary.d.ts +7 -0
  71. package/dist/lumen-boundary.js +10 -0
  72. package/dist/lumen-code-editor-impl.d.ts +2 -0
  73. package/dist/lumen-code-editor-impl.js +16 -0
  74. package/dist/lumen-code-editor.d.ts +11 -0
  75. package/dist/lumen-code-editor.js +11 -0
  76. package/dist/lumen-code.d.ts +22 -0
  77. package/dist/lumen-code.js +75 -0
  78. package/dist/lumen-drawer.d.ts +30 -0
  79. package/dist/lumen-drawer.js +27 -0
  80. package/dist/lumen-echart.d.ts +58 -0
  81. package/dist/lumen-echart.js +129 -0
  82. package/dist/lumen-filter.d.ts +75 -0
  83. package/dist/lumen-filter.js +546 -0
  84. package/dist/lumen-finder.d.ts +62 -0
  85. package/dist/lumen-finder.js +275 -0
  86. package/dist/lumen-form.d.ts +34 -0
  87. package/dist/lumen-form.js +81 -0
  88. package/dist/lumen-geo-map.d.ts +9 -0
  89. package/dist/lumen-geo-map.js +135 -0
  90. package/dist/lumen-grid.d.ts +49 -0
  91. package/dist/lumen-grid.js +203 -0
  92. package/dist/lumen-layout.d.ts +150 -0
  93. package/dist/lumen-layout.js +658 -0
  94. package/dist/lumen-links.d.ts +96 -0
  95. package/dist/lumen-links.js +213 -0
  96. package/dist/lumen-page-state.d.ts +20 -0
  97. package/dist/lumen-page-state.js +15 -0
  98. package/dist/lumen-selector.d.ts +139 -0
  99. package/dist/lumen-selector.js +308 -0
  100. package/dist/lumen-source.d.ts +137 -0
  101. package/dist/lumen-source.js +327 -0
  102. package/dist/lumen-tab-drawer.d.ts +27 -0
  103. package/dist/lumen-tab-drawer.js +26 -0
  104. package/dist/lumen-tabs.d.ts +21 -0
  105. package/dist/lumen-tabs.js +6 -0
  106. package/dist/lumen-toast.d.ts +11 -0
  107. package/dist/lumen-toast.js +16 -0
  108. package/dist/lumen-trend.d.ts +18 -0
  109. package/dist/lumen-trend.js +157 -0
  110. package/dist/lumen-view-card.d.ts +35 -0
  111. package/dist/lumen-view-card.js +50 -0
  112. package/dist/lumen-view.d.ts +172 -0
  113. package/dist/lumen-view.js +645 -0
  114. package/dist/lumen-widget-charts.d.ts +57 -0
  115. package/dist/lumen-widget-charts.js +69 -0
  116. package/dist/lumen-widget.d.ts +614 -0
  117. package/dist/lumen-widget.js +594 -0
  118. package/dist/market/compare-chart.d.ts +34 -0
  119. package/dist/market/compare-chart.js +292 -0
  120. package/dist/market/indicator-picker.d.ts +29 -0
  121. package/dist/market/indicator-picker.js +125 -0
  122. package/dist/market/indicators.d.ts +63 -0
  123. package/dist/market/indicators.js +314 -0
  124. package/dist/market/live.d.ts +120 -0
  125. package/dist/market/live.js +444 -0
  126. package/dist/market/lumen-chart.d.ts +88 -0
  127. package/dist/market/lumen-chart.js +645 -0
  128. package/dist/market/market-widget.d.ts +45 -0
  129. package/dist/market/market-widget.js +346 -0
  130. package/dist/market/option-chain.d.ts +19 -0
  131. package/dist/market/option-chain.js +53 -0
  132. package/dist/market/price-chart.d.ts +128 -0
  133. package/dist/market/price-chart.js +619 -0
  134. package/dist/mcp-settings.d.ts +20 -0
  135. package/dist/mcp-settings.js +65 -0
  136. package/dist/nav.d.ts +77 -0
  137. package/dist/nav.js +73 -0
  138. package/dist/note-line.d.ts +13 -0
  139. package/dist/note-line.js +5 -0
  140. package/dist/notifications.d.ts +9 -0
  141. package/dist/notifications.js +166 -0
  142. package/dist/origin-legend.d.ts +14 -0
  143. package/dist/origin-legend.js +38 -0
  144. package/dist/overview.d.ts +84 -0
  145. package/dist/overview.js +105 -0
  146. package/dist/page-inputs.d.ts +14 -0
  147. package/dist/page-inputs.js +16 -0
  148. package/dist/page.d.ts +32 -0
  149. package/dist/page.js +45 -0
  150. package/dist/pager.d.ts +10 -0
  151. package/dist/pager.js +14 -0
  152. package/dist/prefs.d.ts +71 -0
  153. package/dist/prefs.js +126 -0
  154. package/dist/query-client.d.ts +7 -0
  155. package/dist/query-client.js +19 -0
  156. package/dist/refresh.d.ts +4 -0
  157. package/dist/refresh.js +19 -0
  158. package/dist/rename-field.d.ts +11 -0
  159. package/dist/rename-field.js +26 -0
  160. package/dist/reset-button.d.ts +9 -0
  161. package/dist/reset-button.js +10 -0
  162. package/dist/route-error.d.ts +42 -0
  163. package/dist/route-error.js +229 -0
  164. package/dist/schedules.d.ts +88 -0
  165. package/dist/schedules.js +221 -0
  166. package/dist/settings.d.ts +51 -0
  167. package/dist/settings.js +181 -0
  168. package/dist/shell.d.ts +99 -0
  169. package/dist/shell.js +137 -0
  170. package/dist/source-health.d.ts +12 -0
  171. package/dist/source-health.js +58 -0
  172. package/dist/spotlight.d.ts +23 -0
  173. package/dist/spotlight.js +138 -0
  174. package/dist/status-health.d.ts +16 -0
  175. package/dist/status-health.js +41 -0
  176. package/dist/status.d.ts +9 -0
  177. package/dist/status.js +12 -0
  178. package/dist/summary-cards.d.ts +56 -0
  179. package/dist/summary-cards.js +270 -0
  180. package/dist/symbol-sheet-context.d.ts +4 -0
  181. package/dist/symbol-sheet-context.js +5 -0
  182. package/dist/symbol-sheet.d.ts +9 -0
  183. package/dist/symbol-sheet.js +24 -0
  184. package/dist/tasks.d.ts +29 -0
  185. package/dist/tasks.js +278 -0
  186. package/dist/theme.css +606 -0
  187. package/dist/tile.d.ts +37 -0
  188. package/dist/tile.js +23 -0
  189. package/dist/ui/alert-dialog.d.ts +18 -0
  190. package/dist/ui/alert-dialog.js +41 -0
  191. package/dist/ui/alert.d.ts +10 -0
  192. package/dist/ui/alert.js +27 -0
  193. package/dist/ui/avatar.d.ts +11 -0
  194. package/dist/ui/avatar.js +22 -0
  195. package/dist/ui/badge.d.ts +9 -0
  196. package/dist/ui/badge.js +27 -0
  197. package/dist/ui/breadcrumb.d.ts +11 -0
  198. package/dist/ui/breadcrumb.js +27 -0
  199. package/dist/ui/button-group.d.ts +11 -0
  200. package/dist/ui/button-group.js +27 -0
  201. package/dist/ui/button.d.ts +10 -0
  202. package/dist/ui/button.js +38 -0
  203. package/dist/ui/card.d.ts +11 -0
  204. package/dist/ui/card.js +24 -0
  205. package/dist/ui/chart.d.ts +44 -0
  206. package/dist/ui/chart.js +117 -0
  207. package/dist/ui/checkbox.d.ts +4 -0
  208. package/dist/ui/checkbox.js +8 -0
  209. package/dist/ui/collapsible.d.ts +5 -0
  210. package/dist/ui/collapsible.js +12 -0
  211. package/dist/ui/combobox.d.ts +24 -0
  212. package/dist/ui/combobox.js +60 -0
  213. package/dist/ui/command.d.ts +18 -0
  214. package/dist/ui/command.js +34 -0
  215. package/dist/ui/dialog.d.ts +23 -0
  216. package/dist/ui/dialog.js +50 -0
  217. package/dist/ui/dropdown-menu.d.ts +29 -0
  218. package/dist/ui/dropdown-menu.js +50 -0
  219. package/dist/ui/emoji-picker.d.ts +6 -0
  220. package/dist/ui/emoji-picker.js +28 -0
  221. package/dist/ui/empty.d.ts +11 -0
  222. package/dist/ui/empty.js +33 -0
  223. package/dist/ui/field.d.ts +24 -0
  224. package/dist/ui/field.js +65 -0
  225. package/dist/ui/hover-card.d.ts +6 -0
  226. package/dist/ui/hover-card.js +13 -0
  227. package/dist/ui/input-group.d.ts +16 -0
  228. package/dist/ui/input-group.js +56 -0
  229. package/dist/ui/input.d.ts +3 -0
  230. package/dist/ui/input.js +6 -0
  231. package/dist/ui/item.d.ts +23 -0
  232. package/dist/ui/item.js +67 -0
  233. package/dist/ui/kbd.d.ts +3 -0
  234. package/dist/ui/kbd.js +9 -0
  235. package/dist/ui/label.d.ts +4 -0
  236. package/dist/ui/label.js +7 -0
  237. package/dist/ui/popover.d.ts +10 -0
  238. package/dist/ui/popover.js +25 -0
  239. package/dist/ui/progress.d.ts +4 -0
  240. package/dist/ui/progress.js +9 -0
  241. package/dist/ui/resizable.d.ts +7 -0
  242. package/dist/ui/resizable.js +13 -0
  243. package/dist/ui/scroll-area.d.ts +5 -0
  244. package/dist/ui/scroll-area.js +10 -0
  245. package/dist/ui/select.d.ts +15 -0
  246. package/dist/ui/select.js +36 -0
  247. package/dist/ui/separator.d.ts +4 -0
  248. package/dist/ui/separator.js +7 -0
  249. package/dist/ui/sheet.d.ts +14 -0
  250. package/dist/ui/sheet.js +54 -0
  251. package/dist/ui/sidebar.d.ts +69 -0
  252. package/dist/ui/sidebar.js +206 -0
  253. package/dist/ui/skeleton.d.ts +2 -0
  254. package/dist/ui/skeleton.js +6 -0
  255. package/dist/ui/slider.d.ts +4 -0
  256. package/dist/ui/slider.js +9 -0
  257. package/dist/ui/sonner.d.ts +3 -0
  258. package/dist/ui/sonner.js +24 -0
  259. package/dist/ui/spinner.d.ts +2 -0
  260. package/dist/ui/spinner.js +7 -0
  261. package/dist/ui/switch.d.ts +6 -0
  262. package/dist/ui/switch.js +7 -0
  263. package/dist/ui/table.d.ts +10 -0
  264. package/dist/ui/table.js +31 -0
  265. package/dist/ui/tabs.d.ts +7 -0
  266. package/dist/ui/tabs.js +22 -0
  267. package/dist/ui/textarea.d.ts +3 -0
  268. package/dist/ui/textarea.js +6 -0
  269. package/dist/ui/toggle-group.d.ts +10 -0
  270. package/dist/ui/toggle-group.js +22 -0
  271. package/dist/ui/toggle.d.ts +9 -0
  272. package/dist/ui/toggle.js +25 -0
  273. package/dist/ui/tooltip.d.ts +7 -0
  274. package/dist/ui/tooltip.js +16 -0
  275. package/dist/view-edit-cell.d.ts +49 -0
  276. package/dist/view-edit-cell.js +198 -0
  277. package/dist/view-editor.d.ts +52 -0
  278. package/dist/view-editor.js +940 -0
  279. package/dist/view-formulas.d.ts +61 -0
  280. package/dist/view-formulas.js +247 -0
  281. package/dist/view-icons.d.ts +20 -0
  282. package/dist/view-icons.js +70 -0
  283. package/dist/view-model.d.ts +390 -0
  284. package/dist/view-model.js +316 -0
  285. package/dist/whats-new.d.ts +3 -0
  286. package/dist/whats-new.js +53 -0
  287. package/dist/widget-controls.d.ts +9 -0
  288. package/dist/widget-controls.js +26 -0
  289. package/dist/widget-settings.d.ts +436 -0
  290. package/dist/widget-settings.js +290 -0
  291. package/dist/widget-thumbs.d.ts +3 -0
  292. package/dist/widget-thumbs.js +29 -0
  293. package/dist/workspace-keys.d.ts +12 -0
  294. package/dist/workspace-keys.js +13 -0
  295. package/dist/workspaces.d.ts +57 -0
  296. package/dist/workspaces.js +473 -0
  297. package/package.json +82 -4
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Live market data in the browser (design market-live): one hub per tab, one
3
+ * socket per tab however many widgets show, subscriptions counted per symbol across widgets. The socket
4
+ * opens only while the session trades and the tab is visible: it closes 30 s after the tab is hidden
5
+ * and reconnects with backoff, and every (re)connect gets a fresh snapshot, so gaps fill themselves.
6
+ * Outside the session the widgets read the last session's bars over REST (cached by the Worker until
7
+ * the next open). Each device and tab is its own viewer of the person's feed on the server.
8
+ * While the session is shut they also read the trades since its close (pre-market, after hours,
9
+ * overnight) once a minute, which the widgets draw after the session.
10
+ */
11
+ import { EXTENDED_LABEL, } from '@columenlabs/lumen-ui-types/market';
12
+ import { createContext, createElement, useContext, useEffect, useMemo, useState, useSyncExternalStore, } from 'react';
13
+ const HIDDEN_CLOSE_MS = 30_000;
14
+ const CLOSED_CHECK_MS = 60_000;
15
+ /** Folds changed bars into a series (same minute replaced, new ones appended in order). */
16
+ function merge(into, add) {
17
+ const out = into.slice();
18
+ for (const b of add) {
19
+ const i = out.findIndex((x) => x.t === b.t);
20
+ if (i >= 0)
21
+ out[i] = b;
22
+ else {
23
+ const at = out.findIndex((x) => x.t > b.t);
24
+ out.splice(at < 0 ? out.length : at, 0, b);
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ class MarketHub {
30
+ api;
31
+ session = null;
32
+ status = 'loading';
33
+ error = null;
34
+ series = new Map();
35
+ want = new Map();
36
+ ws = null;
37
+ backoff = 1000;
38
+ retryTimer = null;
39
+ hiddenTimer = null;
40
+ closedTimer = null;
41
+ listeners = new Set();
42
+ version = 0;
43
+ fetching = null;
44
+ onVisibility = () => this.visibility();
45
+ constructor(api) {
46
+ this.api = api;
47
+ document.addEventListener('visibilitychange', this.onVisibility);
48
+ }
49
+ subscribe = (cb) => {
50
+ this.listeners.add(cb);
51
+ return () => this.listeners.delete(cb);
52
+ };
53
+ snapshot = () => this.version;
54
+ changed() {
55
+ this.version++;
56
+ for (const l of this.listeners)
57
+ l();
58
+ }
59
+ probe = null;
60
+ days = new Map();
61
+ /** Daily breadth and macro series, read once per set of keys per tab (they change once a day). */
62
+ daily(keys) {
63
+ const k = keys.join(',');
64
+ let p = this.days.get(k);
65
+ if (!p) {
66
+ p = fetch(`${this.api}/daily?symbols=${encodeURIComponent(k)}`, {
67
+ credentials: 'same-origin',
68
+ }).then(async (r) => {
69
+ if (!r.ok)
70
+ throw new Error(`server answered ${r.status}`);
71
+ return (await r.json()).series;
72
+ });
73
+ p.catch(() => this.days.delete(k));
74
+ this.days.set(k, p);
75
+ }
76
+ return p;
77
+ }
78
+ /** Whether the app serves market data at all (a 404 means no feed here), read once. */
79
+ available() {
80
+ this.probe ??= fetch(`${this.api}/session`, { credentials: 'same-origin' })
81
+ .then((r) => r.status !== 404)
82
+ .catch(() => true);
83
+ return this.probe;
84
+ }
85
+ /** A widget shows these symbols; returns its release. */
86
+ hold(keys) {
87
+ let added = false;
88
+ for (const k of keys) {
89
+ const n = this.want.get(k) ?? 0;
90
+ this.want.set(k, n + 1);
91
+ if (!n)
92
+ added = true;
93
+ }
94
+ if (added)
95
+ this.sync();
96
+ return () => {
97
+ for (const k of keys) {
98
+ const n = (this.want.get(k) ?? 1) - 1;
99
+ if (n <= 0)
100
+ this.want.delete(k);
101
+ else
102
+ this.want.set(k, n);
103
+ }
104
+ if (!this.want.size)
105
+ this.stop();
106
+ else
107
+ this.sendSub();
108
+ };
109
+ }
110
+ keys() {
111
+ return [...this.want.keys()].sort();
112
+ }
113
+ /** Reads the session (and its bars when it is not trading), then opens the socket if it trades. */
114
+ sync() {
115
+ if (this.ws && this.ws.readyState <= WebSocket.OPEN && this.session?.live) {
116
+ this.sendSub();
117
+ return;
118
+ }
119
+ if (this.fetching)
120
+ return;
121
+ this.fetching = this.read().finally(() => {
122
+ this.fetching = null;
123
+ // Symbols a widget added while that read was in flight (the Outlook's tiles mount just after
124
+ // its chart): read them now. Each is asked once, so a symbol the server leaves out ends as
125
+ // "No data" instead of a loop.
126
+ if (!this.session?.live && this.status !== 'error' && this.unasked().length)
127
+ this.sync();
128
+ });
129
+ }
130
+ /** Symbols asked for and read, so a widget can tell "still loading" from "the server had none". */
131
+ asked = new Set();
132
+ unasked() {
133
+ return this.keys().filter((k) => !this.series.has(k) && !this.asked.has(k));
134
+ }
135
+ /** True once `key` was read and the server sent no series for it. */
136
+ missing(key) {
137
+ return this.status !== 'loading' && this.asked.has(key) && !this.series.has(key);
138
+ }
139
+ /** Reads again after a failure (a widget's Retry). */
140
+ retry() {
141
+ for (const k of this.keys())
142
+ if (!this.series.has(k))
143
+ this.asked.delete(k);
144
+ this.status = 'loading';
145
+ this.error = null;
146
+ this.changed();
147
+ this.sync();
148
+ }
149
+ async read() {
150
+ const missing = this.unasked();
151
+ try {
152
+ const q = missing.length ? `?symbols=${encodeURIComponent(missing.join(','))}` : '';
153
+ const res = await fetch(`${this.api}/session${q}`, { credentials: 'same-origin' });
154
+ if (res.status === 404) {
155
+ this.status = 'off';
156
+ this.changed();
157
+ return;
158
+ }
159
+ if (!res.ok)
160
+ throw new Error(`server answered ${res.status}`);
161
+ const body = (await res.json());
162
+ this.session = body.session;
163
+ this.error = null;
164
+ for (const s of body.series ?? [])
165
+ this.series.set(s.key, s);
166
+ for (const k of missing)
167
+ this.asked.add(k);
168
+ if (body.session.live && document.visibilityState === 'visible')
169
+ this.open();
170
+ else {
171
+ this.status = body.session.live ? 'connecting' : 'closed';
172
+ this.watchClosed();
173
+ if (!body.session.live)
174
+ void this.readExtended();
175
+ }
176
+ }
177
+ catch (e) {
178
+ this.status = 'error';
179
+ this.error = e.message;
180
+ }
181
+ this.changed();
182
+ }
183
+ /** The trades since the session's close, folded into each series (none without a source). */
184
+ async readExtended() {
185
+ const keys = this.keys().filter((k) => this.series.has(k));
186
+ if (!keys.length || this.session?.live)
187
+ return;
188
+ const res = await fetch(`${this.api}/extended?symbols=${encodeURIComponent(keys.join(','))}`, {
189
+ credentials: 'same-origin',
190
+ }).catch(() => null);
191
+ if (!res?.ok)
192
+ return;
193
+ const body = (await res.json());
194
+ if (body.session !== this.session?.date)
195
+ return;
196
+ for (const [k, extended] of Object.entries(body.extended)) {
197
+ const s = this.series.get(k);
198
+ if (s)
199
+ this.series.set(k, { ...s, extended });
200
+ }
201
+ this.changed();
202
+ }
203
+ /**
204
+ * While closed, check once a minute (a cheap read: no bars) whether the session has opened, and
205
+ * read the trades since the close again.
206
+ */
207
+ watchClosed() {
208
+ if (this.closedTimer)
209
+ return;
210
+ this.closedTimer = setInterval(async () => {
211
+ if (!this.want.size || document.visibilityState !== 'visible')
212
+ return;
213
+ const res = await fetch(`${this.api}/session`, { credentials: 'same-origin' }).catch(() => null);
214
+ const body = res?.ok ? (await res.json()) : null;
215
+ if (!body)
216
+ return;
217
+ if (body.session.date === this.session?.date) {
218
+ if (!body.session.live)
219
+ void this.readExtended();
220
+ return;
221
+ }
222
+ this.series.clear();
223
+ this.asked.clear();
224
+ this.sync();
225
+ }, CLOSED_CHECK_MS);
226
+ }
227
+ open() {
228
+ if (this.closedTimer) {
229
+ clearInterval(this.closedTimer);
230
+ this.closedTimer = null;
231
+ }
232
+ if (this.ws)
233
+ return;
234
+ this.status = 'connecting';
235
+ const url = new URL(`${this.api}/live`, location.href);
236
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
237
+ const ws = new WebSocket(url);
238
+ this.ws = ws;
239
+ ws.onopen = () => {
240
+ this.backoff = 1000;
241
+ this.sendSub();
242
+ };
243
+ ws.onmessage = (e) => {
244
+ let m;
245
+ try {
246
+ m = JSON.parse(String(e.data));
247
+ }
248
+ catch {
249
+ return;
250
+ }
251
+ if (m.type === 'session') {
252
+ this.session = m.session;
253
+ if (!m.session.live) {
254
+ // The session closed: drop the socket and read the day's bars instead.
255
+ this.close();
256
+ this.series.clear();
257
+ this.asked.clear();
258
+ this.sync();
259
+ return;
260
+ }
261
+ this.status = 'live';
262
+ }
263
+ else if (m.type === 'snapshot') {
264
+ for (const s of m.series)
265
+ this.series.set(s.key, s);
266
+ this.status = 'live';
267
+ }
268
+ else if (m.type === 'bars') {
269
+ for (const [k, bars] of Object.entries(m.bars)) {
270
+ const s = this.series.get(k);
271
+ if (s)
272
+ this.series.set(k, { ...s, bars: merge(s.bars, bars) });
273
+ }
274
+ }
275
+ this.changed();
276
+ };
277
+ ws.onclose = () => {
278
+ if (this.ws !== ws)
279
+ return;
280
+ this.ws = null;
281
+ if (!this.want.size || document.visibilityState !== 'visible')
282
+ return;
283
+ this.status = 'connecting';
284
+ this.changed();
285
+ this.retryTimer = setTimeout(() => {
286
+ this.retryTimer = null;
287
+ this.sync();
288
+ }, this.backoff);
289
+ this.backoff = Math.min(this.backoff * 2, 30_000);
290
+ };
291
+ }
292
+ sendSub() {
293
+ if (this.ws?.readyState === WebSocket.OPEN)
294
+ this.ws.send(JSON.stringify({ type: 'sub', symbols: this.keys() }));
295
+ }
296
+ close() {
297
+ const ws = this.ws;
298
+ this.ws = null;
299
+ ws?.close(1000, 'done');
300
+ if (this.retryTimer)
301
+ clearTimeout(this.retryTimer);
302
+ this.retryTimer = null;
303
+ }
304
+ stop() {
305
+ this.close();
306
+ if (this.closedTimer)
307
+ clearInterval(this.closedTimer);
308
+ this.closedTimer = null;
309
+ }
310
+ visibility() {
311
+ if (document.visibilityState === 'hidden') {
312
+ this.hiddenTimer ??= setTimeout(() => {
313
+ this.hiddenTimer = null;
314
+ this.close();
315
+ }, HIDDEN_CLOSE_MS);
316
+ return;
317
+ }
318
+ if (this.hiddenTimer)
319
+ clearTimeout(this.hiddenTimer);
320
+ this.hiddenTimer = null;
321
+ if (this.want.size && !this.ws && this.session?.live)
322
+ this.sync();
323
+ }
324
+ }
325
+ /** The trades since the close, scaled to the series (a stand-in ETF to its index), or null. */
326
+ export function extendedOf(s) {
327
+ const ext = s.extended;
328
+ const close = s.bars.at(-1)?.c;
329
+ const lastBar = ext?.bars.at(-1);
330
+ if (!ext || !lastBar || close === undefined || !ext.base)
331
+ return null;
332
+ const k = close / ext.base;
333
+ return {
334
+ via: ext.via,
335
+ close,
336
+ bars: ext.bars.map((b) => ({ t: b.t, o: b.o * k, h: b.h * k, l: b.l * k, c: b.c * k })),
337
+ last: lastBar.c * k,
338
+ at: lastBar.t,
339
+ label: EXTENDED_LABEL[lastBar.s],
340
+ /** Change from the session's close, in percent. */
341
+ change: (lastBar.c / ext.base - 1) * 100,
342
+ };
343
+ }
344
+ /**
345
+ * Up or down today: the price now against the close before today's trading. A broker's trading
346
+ * day starts with the overnight session, so from then on it is the session's close; before (the
347
+ * session, after hours) it is the prior close.
348
+ */
349
+ export function todayOf(s) {
350
+ const ext = extendedOf(s);
351
+ const close = s.bars.at(-1)?.c;
352
+ const newDay = s.extended?.bars.some((b) => b.s !== 'post');
353
+ const base = newDay ? close : s.prevClose;
354
+ const value = ext?.last ?? close;
355
+ if (value === undefined || !base)
356
+ return null;
357
+ return {
358
+ value,
359
+ base,
360
+ change: (value / base - 1) * 100,
361
+ label: ext?.label ?? null,
362
+ at: ext?.at,
363
+ };
364
+ }
365
+ const MarketContext = createContext(null);
366
+ /** Gives market widgets and the chart dialog live data from `api` (`/api/v1/market`). */
367
+ export function MarketProvider({ api, children }) {
368
+ const hub = useMemo(() => new MarketHub(api), [api]);
369
+ return createElement(MarketContext.Provider, { value: hub }, children);
370
+ }
371
+ /** Today's (or the last) session of `symbols`, live while it trades. */
372
+ export function useMarket(symbols) {
373
+ const hub = useContext(MarketContext);
374
+ const key = symbols.join(',');
375
+ useEffect(() => (hub && key ? hub.hold(key.split(',')) : undefined), [hub, key]);
376
+ useSyncExternalStore(hub?.subscribe ?? (() => () => { }), hub?.snapshot ?? (() => 0), () => 0);
377
+ if (!hub)
378
+ return {
379
+ status: 'off',
380
+ session: null,
381
+ series: {},
382
+ missing: [],
383
+ error: null,
384
+ retry: () => { },
385
+ };
386
+ const series = {};
387
+ for (const s of symbols) {
388
+ const x = hub.series.get(s);
389
+ if (x)
390
+ series[s] = x;
391
+ }
392
+ return {
393
+ status: hub.status,
394
+ session: hub.session,
395
+ series,
396
+ missing: symbols.filter((s) => hub.missing(s)),
397
+ error: hub.error,
398
+ retry: () => hub.retry(),
399
+ };
400
+ }
401
+ /** Daily breadth and macro series of `keys` (never live); `error` when they could not be read. */
402
+ export function useMarketDaily(keys) {
403
+ const hub = useContext(MarketContext);
404
+ const k = keys.join(',');
405
+ const [attempt, setAttempt] = useState(0);
406
+ const [state, setState] = useState({ series: {}, loading: true, error: null });
407
+ useEffect(() => {
408
+ if (!hub || !k)
409
+ return;
410
+ let on = true;
411
+ setState((s) => ({ ...s, loading: true, error: null }));
412
+ void attempt;
413
+ hub
414
+ .daily(k.split(','))
415
+ .then((list) => on &&
416
+ setState({
417
+ series: Object.fromEntries(list.map((s) => [s.key, s])),
418
+ loading: false,
419
+ error: null,
420
+ }))
421
+ .catch((e) => on && setState({ series: {}, loading: false, error: e.message }));
422
+ return () => {
423
+ on = false;
424
+ };
425
+ }, [hub, k, attempt]);
426
+ return { ...state, retry: () => setAttempt((n) => n + 1) };
427
+ }
428
+ /** True where a market provider is mounted (an app, suite or not). */
429
+ export const useHasMarket = () => useContext(MarketContext) !== null;
430
+ /** Whether this app has a market feed, without subscribing to anything: null while checking. */
431
+ export function useMarketFeed() {
432
+ const hub = useContext(MarketContext);
433
+ const [has, setHas] = useState(hub ? null : false);
434
+ useEffect(() => {
435
+ if (!hub)
436
+ return;
437
+ let on = true;
438
+ void hub.available().then((v) => on && setHas(v));
439
+ return () => {
440
+ on = false;
441
+ };
442
+ }, [hub]);
443
+ return has;
444
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * LumenChart: the one price chart of every app, as a component
3
+ * (`<LumenChart symbols={[…]} />`, an app's symbol page), the `chart` widget any view can add, and
4
+ * the chart dialog (mockup "Symbol chart dialog"): any widget, legend name or
5
+ * symbol on any app opens it. Windows 1D to Max, candles or line, crosshair, drag and zoom,
6
+ * and the timeline under it. 1D shows today's live bars where the app has live data (an app);
7
+ * every other window reads history from the app's `bars` route (the warehouse, or Yahoo labelled
8
+ * Delayed for what the warehouse does not hold).
9
+ *
10
+ * Compare: up to 8 symbols. Two or more
11
+ * draw as % from the left edge of what is visible (dragging the timeline moves the baseline) or as
12
+ * Price, each on its own scale. Every indicator is made only when added, for one symbol or All
13
+ * shown: computed ones in the browser from the bars plus warm-up bars before the window (SMA 200
14
+ * draws from the first bar); P/E, IV and the EPS estimate read once from market data.
15
+ */
16
+ import type { ChartFormula, IndicatorSpec } from '@columenlabs/lumen-ui-types/chart-formula';
17
+ import { type MarketBar, type MarketWindow } from '@columenlabs/lumen-ui-types/market';
18
+ import { type ReactNode } from 'react';
19
+ import { type LumenLoad } from '../lumen-source.js';
20
+ import { type ChartIndicator } from './indicators.js';
21
+ import { type ChartBar } from './price-chart.js';
22
+ /** Opens the chart dialog: one symbol, or up to 8 to compare, with its indicators if given. */
23
+ export type OpenChart = (symbols: string | string[], indicators?: IndicatorSpec[]) => void;
24
+ /**
25
+ * The person's saved formulas (Mine): drawn by `mine:<id>`, listed first in the
26
+ * picker; `save` and `remove` where the app keeps them.
27
+ */
28
+ export interface MineIndicators {
29
+ list: ChartIndicator[];
30
+ save?: (f: ChartFormula) => Promise<void>;
31
+ remove?: (id: string) => Promise<void>;
32
+ }
33
+ /** Opens the chart dialog; null where no app mounted the dialog. */
34
+ export declare const useSymbolChart: () => OpenChart | null;
35
+ /** The app's history route (`bars`) the chart reads; null where no app mounted the dialog. */
36
+ export declare const useBarsUrl: () => string | null;
37
+ /** Symbols one chart compares at most. */
38
+ export declare const MAX_COMPARE = 8;
39
+ /**
40
+ * Mounts LumenChart once for an app, and its dialog: `bars` is the app's history route,
41
+ * `indicators` its sourced indicator route (P/E, IV, EPS estimate); left out, those indicators are
42
+ * not offered. `mine` is the route of the person's saved formulas; left out, there is no Mine.
43
+ */
44
+ export declare function SymbolChartProvider({ bars, indicators, mine, children, }: {
45
+ bars: string;
46
+ indicators?: string;
47
+ mine?: string;
48
+ children: ReactNode;
49
+ }): import("react").JSX.Element;
50
+ export declare const toChart: (b: MarketBar) => ChartBar;
51
+ /** Daily bars sit at UTC midnight; the chart reads them as days. */
52
+ export declare const toDaily: (b: MarketBar) => ChartBar;
53
+ /** What a LumenChart starts with; key it by them to start it over when they change. */
54
+ export interface LumenChartOptions {
55
+ /** One symbol, or up to 8 to compare. */
56
+ symbols: string[];
57
+ /** The window it opens on; left out: 1D for one symbol, 1Y to compare, 6M for daily series. */
58
+ window?: MarketWindow;
59
+ type?: 'candles' | 'line';
60
+ /**
61
+ * Indicators drawn for every symbol shown: catalog ids, built-ins with any window (`ema_13`,
62
+ * `bollinger_20_2p5`), saved formulas (`mine:<id>`) or formulas; left out: SMA 20 and RSI 14 for
63
+ * one symbol.
64
+ */
65
+ indicators?: IndicatorSpec[];
66
+ /** `dialog` inside the chart dialog; `card` (default) inside a page's card or a sheet. */
67
+ variant?: 'dialog' | 'card';
68
+ /** Reads nothing until the chart (`ref`) scrolls into view. */
69
+ lazy?: boolean;
70
+ /** Shows a Close button. */
71
+ onClose?: () => void;
72
+ /** The window buttons (a card's Window control); left out: shown. */
73
+ windows?: boolean;
74
+ }
75
+ /** LumenChart as one symbol, or up to 8 compared, inside a card, a sheet or the chart dialog. */
76
+ export declare function LumenChart(props: LumenChartOptions): import("react").JSX.Element;
77
+ /**
78
+ * LumenChart's state and body, for a card that shows its source: `load` (its ↻ and load time),
79
+ * `rows` (bars drawn), `ref` (for `lazy`) and `element`. `noDesk`: the app mounted no
80
+ * SymbolChartProvider, so there is nothing to read.
81
+ */
82
+ export declare function useLumenChart({ symbols: initial, window: start, type: startType, indicators, variant, lazy, onClose, windows: showWindows, }: LumenChartOptions): {
83
+ load: LumenLoad;
84
+ rows: number;
85
+ ref: (el: Element | null) => void;
86
+ element: ReactNode;
87
+ noDesk: boolean;
88
+ };