@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.
- package/LICENSE +21 -0
- package/NOTICE +12 -0
- package/README.md +4 -2
- package/dist/add-column.d.ts +23 -0
- package/dist/add-column.js +62 -0
- package/dist/api-fetch.d.ts +9 -0
- package/dist/api-fetch.js +60 -0
- package/dist/app-switcher.d.ts +21 -0
- package/dist/app-switcher.js +12 -0
- package/dist/app-views.d.ts +18 -0
- package/dist/app-views.js +41 -0
- package/dist/assistant-context.d.ts +72 -0
- package/dist/assistant-context.js +30 -0
- package/dist/assistant-panel.d.ts +58 -0
- package/dist/assistant-panel.js +244 -0
- package/dist/assistant-screen.d.ts +124 -0
- package/dist/assistant-screen.js +252 -0
- package/dist/card-switch.d.ts +16 -0
- package/dist/card-switch.js +16 -0
- package/dist/color-dot.d.ts +14 -0
- package/dist/color-dot.js +19 -0
- package/dist/confirm.d.ts +18 -0
- package/dist/confirm.js +5 -0
- package/dist/connections.d.ts +45 -0
- package/dist/connections.js +275 -0
- package/dist/coverage-calendar-map.d.ts +27 -0
- package/dist/coverage-calendar-map.js +84 -0
- package/dist/coverage-cells.d.ts +55 -0
- package/dist/coverage-cells.js +85 -0
- package/dist/coverage-map.d.ts +112 -0
- package/dist/coverage-map.js +428 -0
- package/dist/details-table.d.ts +20 -0
- package/dist/details-table.js +13 -0
- package/dist/drawer-section.d.ts +27 -0
- package/dist/drawer-section.js +23 -0
- package/dist/floating-head.d.ts +2 -0
- package/dist/floating-head.js +57 -0
- package/dist/fold.d.ts +16 -0
- package/dist/fold.js +70 -0
- package/dist/form-issues.d.ts +32 -0
- package/dist/form-issues.js +60 -0
- package/dist/formula-input.d.ts +17 -0
- package/dist/formula-input.js +104 -0
- package/dist/frame-script.d.ts +7 -0
- package/dist/frame-script.js +103 -0
- package/dist/freshness.d.ts +24 -0
- package/dist/freshness.js +99 -0
- package/dist/group-button.d.ts +33 -0
- package/dist/group-button.js +67 -0
- package/dist/growth-chart.d.ts +77 -0
- package/dist/growth-chart.js +195 -0
- package/dist/hooks/use-keyboard-fit.d.ts +15 -0
- package/dist/hooks/use-keyboard-fit.js +29 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/icon-button.d.ts +21 -0
- package/dist/icon-button.js +22 -0
- package/dist/icon-picker.d.ts +14 -0
- package/dist/icon-picker.js +36 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +90 -0
- package/dist/integrations.d.ts +57 -0
- package/dist/integrations.js +73 -0
- package/dist/joins.d.ts +128 -0
- package/dist/joins.js +767 -0
- package/dist/kit-config.d.ts +13 -0
- package/dist/kit-config.js +15 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/lumen-boundary.d.ts +7 -0
- package/dist/lumen-boundary.js +10 -0
- package/dist/lumen-code-editor-impl.d.ts +2 -0
- package/dist/lumen-code-editor-impl.js +16 -0
- package/dist/lumen-code-editor.d.ts +11 -0
- package/dist/lumen-code-editor.js +11 -0
- package/dist/lumen-code.d.ts +22 -0
- package/dist/lumen-code.js +75 -0
- package/dist/lumen-drawer.d.ts +30 -0
- package/dist/lumen-drawer.js +27 -0
- package/dist/lumen-echart.d.ts +58 -0
- package/dist/lumen-echart.js +129 -0
- package/dist/lumen-filter.d.ts +75 -0
- package/dist/lumen-filter.js +546 -0
- package/dist/lumen-finder.d.ts +62 -0
- package/dist/lumen-finder.js +275 -0
- package/dist/lumen-form.d.ts +34 -0
- package/dist/lumen-form.js +81 -0
- package/dist/lumen-geo-map.d.ts +9 -0
- package/dist/lumen-geo-map.js +135 -0
- package/dist/lumen-grid.d.ts +49 -0
- package/dist/lumen-grid.js +203 -0
- package/dist/lumen-layout.d.ts +150 -0
- package/dist/lumen-layout.js +658 -0
- package/dist/lumen-links.d.ts +96 -0
- package/dist/lumen-links.js +213 -0
- package/dist/lumen-page-state.d.ts +20 -0
- package/dist/lumen-page-state.js +15 -0
- package/dist/lumen-selector.d.ts +139 -0
- package/dist/lumen-selector.js +308 -0
- package/dist/lumen-source.d.ts +137 -0
- package/dist/lumen-source.js +327 -0
- package/dist/lumen-tab-drawer.d.ts +27 -0
- package/dist/lumen-tab-drawer.js +26 -0
- package/dist/lumen-tabs.d.ts +21 -0
- package/dist/lumen-tabs.js +6 -0
- package/dist/lumen-toast.d.ts +11 -0
- package/dist/lumen-toast.js +16 -0
- package/dist/lumen-trend.d.ts +18 -0
- package/dist/lumen-trend.js +157 -0
- package/dist/lumen-view-card.d.ts +35 -0
- package/dist/lumen-view-card.js +50 -0
- package/dist/lumen-view.d.ts +172 -0
- package/dist/lumen-view.js +645 -0
- package/dist/lumen-widget-charts.d.ts +57 -0
- package/dist/lumen-widget-charts.js +69 -0
- package/dist/lumen-widget.d.ts +614 -0
- package/dist/lumen-widget.js +594 -0
- package/dist/market/compare-chart.d.ts +34 -0
- package/dist/market/compare-chart.js +292 -0
- package/dist/market/indicator-picker.d.ts +29 -0
- package/dist/market/indicator-picker.js +125 -0
- package/dist/market/indicators.d.ts +63 -0
- package/dist/market/indicators.js +314 -0
- package/dist/market/live.d.ts +120 -0
- package/dist/market/live.js +444 -0
- package/dist/market/lumen-chart.d.ts +88 -0
- package/dist/market/lumen-chart.js +645 -0
- package/dist/market/market-widget.d.ts +45 -0
- package/dist/market/market-widget.js +346 -0
- package/dist/market/option-chain.d.ts +19 -0
- package/dist/market/option-chain.js +53 -0
- package/dist/market/price-chart.d.ts +128 -0
- package/dist/market/price-chart.js +619 -0
- package/dist/mcp-settings.d.ts +20 -0
- package/dist/mcp-settings.js +65 -0
- package/dist/nav.d.ts +77 -0
- package/dist/nav.js +73 -0
- package/dist/note-line.d.ts +13 -0
- package/dist/note-line.js +5 -0
- package/dist/notifications.d.ts +9 -0
- package/dist/notifications.js +166 -0
- package/dist/origin-legend.d.ts +14 -0
- package/dist/origin-legend.js +38 -0
- package/dist/overview.d.ts +84 -0
- package/dist/overview.js +105 -0
- package/dist/page-inputs.d.ts +14 -0
- package/dist/page-inputs.js +16 -0
- package/dist/page.d.ts +32 -0
- package/dist/page.js +45 -0
- package/dist/pager.d.ts +10 -0
- package/dist/pager.js +14 -0
- package/dist/prefs.d.ts +71 -0
- package/dist/prefs.js +126 -0
- package/dist/query-client.d.ts +7 -0
- package/dist/query-client.js +19 -0
- package/dist/refresh.d.ts +4 -0
- package/dist/refresh.js +19 -0
- package/dist/rename-field.d.ts +11 -0
- package/dist/rename-field.js +26 -0
- package/dist/reset-button.d.ts +9 -0
- package/dist/reset-button.js +10 -0
- package/dist/route-error.d.ts +42 -0
- package/dist/route-error.js +229 -0
- package/dist/schedules.d.ts +88 -0
- package/dist/schedules.js +221 -0
- package/dist/settings.d.ts +51 -0
- package/dist/settings.js +181 -0
- package/dist/shell.d.ts +99 -0
- package/dist/shell.js +137 -0
- package/dist/source-health.d.ts +12 -0
- package/dist/source-health.js +58 -0
- package/dist/spotlight.d.ts +23 -0
- package/dist/spotlight.js +138 -0
- package/dist/status-health.d.ts +16 -0
- package/dist/status-health.js +41 -0
- package/dist/status.d.ts +9 -0
- package/dist/status.js +12 -0
- package/dist/summary-cards.d.ts +56 -0
- package/dist/summary-cards.js +270 -0
- package/dist/symbol-sheet-context.d.ts +4 -0
- package/dist/symbol-sheet-context.js +5 -0
- package/dist/symbol-sheet.d.ts +9 -0
- package/dist/symbol-sheet.js +24 -0
- package/dist/tasks.d.ts +29 -0
- package/dist/tasks.js +278 -0
- package/dist/theme.css +606 -0
- package/dist/tile.d.ts +37 -0
- package/dist/tile.js +23 -0
- package/dist/ui/alert-dialog.d.ts +18 -0
- package/dist/ui/alert-dialog.js +41 -0
- package/dist/ui/alert.d.ts +10 -0
- package/dist/ui/alert.js +27 -0
- package/dist/ui/avatar.d.ts +11 -0
- package/dist/ui/avatar.js +22 -0
- package/dist/ui/badge.d.ts +9 -0
- package/dist/ui/badge.js +27 -0
- package/dist/ui/breadcrumb.d.ts +11 -0
- package/dist/ui/breadcrumb.js +27 -0
- package/dist/ui/button-group.d.ts +11 -0
- package/dist/ui/button-group.js +27 -0
- package/dist/ui/button.d.ts +10 -0
- package/dist/ui/button.js +38 -0
- package/dist/ui/card.d.ts +11 -0
- package/dist/ui/card.js +24 -0
- package/dist/ui/chart.d.ts +44 -0
- package/dist/ui/chart.js +117 -0
- package/dist/ui/checkbox.d.ts +4 -0
- package/dist/ui/checkbox.js +8 -0
- package/dist/ui/collapsible.d.ts +5 -0
- package/dist/ui/collapsible.js +12 -0
- package/dist/ui/combobox.d.ts +24 -0
- package/dist/ui/combobox.js +60 -0
- package/dist/ui/command.d.ts +18 -0
- package/dist/ui/command.js +34 -0
- package/dist/ui/dialog.d.ts +23 -0
- package/dist/ui/dialog.js +50 -0
- package/dist/ui/dropdown-menu.d.ts +29 -0
- package/dist/ui/dropdown-menu.js +50 -0
- package/dist/ui/emoji-picker.d.ts +6 -0
- package/dist/ui/emoji-picker.js +28 -0
- package/dist/ui/empty.d.ts +11 -0
- package/dist/ui/empty.js +33 -0
- package/dist/ui/field.d.ts +24 -0
- package/dist/ui/field.js +65 -0
- package/dist/ui/hover-card.d.ts +6 -0
- package/dist/ui/hover-card.js +13 -0
- package/dist/ui/input-group.d.ts +16 -0
- package/dist/ui/input-group.js +56 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.js +6 -0
- package/dist/ui/item.d.ts +23 -0
- package/dist/ui/item.js +67 -0
- package/dist/ui/kbd.d.ts +3 -0
- package/dist/ui/kbd.js +9 -0
- package/dist/ui/label.d.ts +4 -0
- package/dist/ui/label.js +7 -0
- package/dist/ui/popover.d.ts +10 -0
- package/dist/ui/popover.js +25 -0
- package/dist/ui/progress.d.ts +4 -0
- package/dist/ui/progress.js +9 -0
- package/dist/ui/resizable.d.ts +7 -0
- package/dist/ui/resizable.js +13 -0
- package/dist/ui/scroll-area.d.ts +5 -0
- package/dist/ui/scroll-area.js +10 -0
- package/dist/ui/select.d.ts +15 -0
- package/dist/ui/select.js +36 -0
- package/dist/ui/separator.d.ts +4 -0
- package/dist/ui/separator.js +7 -0
- package/dist/ui/sheet.d.ts +14 -0
- package/dist/ui/sheet.js +54 -0
- package/dist/ui/sidebar.d.ts +69 -0
- package/dist/ui/sidebar.js +206 -0
- package/dist/ui/skeleton.d.ts +2 -0
- package/dist/ui/skeleton.js +6 -0
- package/dist/ui/slider.d.ts +4 -0
- package/dist/ui/slider.js +9 -0
- package/dist/ui/sonner.d.ts +3 -0
- package/dist/ui/sonner.js +24 -0
- package/dist/ui/spinner.d.ts +2 -0
- package/dist/ui/spinner.js +7 -0
- package/dist/ui/switch.d.ts +6 -0
- package/dist/ui/switch.js +7 -0
- package/dist/ui/table.d.ts +10 -0
- package/dist/ui/table.js +31 -0
- package/dist/ui/tabs.d.ts +7 -0
- package/dist/ui/tabs.js +22 -0
- package/dist/ui/textarea.d.ts +3 -0
- package/dist/ui/textarea.js +6 -0
- package/dist/ui/toggle-group.d.ts +10 -0
- package/dist/ui/toggle-group.js +22 -0
- package/dist/ui/toggle.d.ts +9 -0
- package/dist/ui/toggle.js +25 -0
- package/dist/ui/tooltip.d.ts +7 -0
- package/dist/ui/tooltip.js +16 -0
- package/dist/view-edit-cell.d.ts +49 -0
- package/dist/view-edit-cell.js +198 -0
- package/dist/view-editor.d.ts +52 -0
- package/dist/view-editor.js +940 -0
- package/dist/view-formulas.d.ts +61 -0
- package/dist/view-formulas.js +247 -0
- package/dist/view-icons.d.ts +20 -0
- package/dist/view-icons.js +70 -0
- package/dist/view-model.d.ts +390 -0
- package/dist/view-model.js +316 -0
- package/dist/whats-new.d.ts +3 -0
- package/dist/whats-new.js +53 -0
- package/dist/widget-controls.d.ts +9 -0
- package/dist/widget-controls.js +26 -0
- package/dist/widget-settings.d.ts +436 -0
- package/dist/widget-settings.js +290 -0
- package/dist/widget-thumbs.d.ts +3 -0
- package/dist/widget-thumbs.js +29 -0
- package/dist/workspace-keys.d.ts +12 -0
- package/dist/workspace-keys.js +13 -0
- package/dist/workspaces.d.ts +57 -0
- package/dist/workspaces.js +473 -0
- package/package.json +82 -4
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The shell's price chart (moved from an app's Chart tab): price in the chosen
|
|
4
|
+
* type, volume when the bars have it, then a pane per oscillator, all on one time axis; crosshair
|
|
5
|
+
* legend, drag to pan, wheel or pinch to zoom, and an optional timeline navigator under it.
|
|
6
|
+
* lightweight-charts is the one place the shell draws with something other than Recharts: it has the
|
|
7
|
+
* financial time axis and panning (approved in the market-live design).
|
|
8
|
+
*/
|
|
9
|
+
import { AreaSeries, BarSeries, CandlestickSeries, ColorType, CrosshairMode, HistogramSeries, LineSeries, createChart, } from 'lightweight-charts';
|
|
10
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
+
import { cn } from '../lib/utils.js';
|
|
12
|
+
import { lineColor } from './indicators.js';
|
|
13
|
+
/** How the price is drawn: the basics every charting tool offers. */
|
|
14
|
+
export const CHART_TYPES = [
|
|
15
|
+
{ id: 'candles', label: 'Candles' },
|
|
16
|
+
{ id: 'ohlc', label: 'OHLC bars' },
|
|
17
|
+
{ id: 'line', label: 'Line' },
|
|
18
|
+
{ id: 'area', label: 'Area' },
|
|
19
|
+
{ id: 'heikin', label: 'Heikin-Ashi' },
|
|
20
|
+
];
|
|
21
|
+
/** Heikin-Ashi candles: each bar averages itself with the one before, which smooths the trend. */
|
|
22
|
+
export function heikinAshi(bars) {
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const b of bars) {
|
|
25
|
+
const close = (b.open + b.high + b.low + b.close) / 4;
|
|
26
|
+
const prev = out.at(-1);
|
|
27
|
+
const open = prev ? (prev.open + prev.close) / 2 : (b.open + b.close) / 2;
|
|
28
|
+
out.push({
|
|
29
|
+
time: b.time,
|
|
30
|
+
open,
|
|
31
|
+
high: Math.max(b.high, open, close),
|
|
32
|
+
low: Math.min(b.low, open, close),
|
|
33
|
+
close,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export const PRICE_PANE = 288;
|
|
39
|
+
const VOLUME_PANE = 80;
|
|
40
|
+
export const OSC_PANE = 112;
|
|
41
|
+
export const AXIS = 28;
|
|
42
|
+
const NAV = 56;
|
|
43
|
+
const MINUS = '−';
|
|
44
|
+
export const price = (n) => n
|
|
45
|
+
.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
46
|
+
.replace('-', MINUS);
|
|
47
|
+
const signed = (n) => (n > 0 ? `+${price(n)}` : price(n));
|
|
48
|
+
const COMPACT = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 });
|
|
49
|
+
/** A colour at half strength (grid lines fainter than borders). */
|
|
50
|
+
export const faint = (color) => (/^#[0-9a-f]{6}$/i.test(color) ? `${color}80` : color);
|
|
51
|
+
/** Theme tokens as concrete colours: the canvas cannot read `var(--x)`. */
|
|
52
|
+
function readTheme() {
|
|
53
|
+
const css = getComputedStyle(document.documentElement);
|
|
54
|
+
const v = (name) => css.getPropertyValue(name).trim();
|
|
55
|
+
return {
|
|
56
|
+
text: v('--muted-foreground'),
|
|
57
|
+
border: v('--border'),
|
|
58
|
+
grid: faint(v('--border')),
|
|
59
|
+
up: v('--gain'),
|
|
60
|
+
down: v('--loss'),
|
|
61
|
+
primary: v('--primary'),
|
|
62
|
+
css: v,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Colours follow the light/dark switch, which sets the `dark` class on the root. */
|
|
66
|
+
export function useTheme() {
|
|
67
|
+
const [theme, setTheme] = useState(readTheme);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const refresh = () => setTheme(readTheme());
|
|
70
|
+
const observer = new MutationObserver(refresh);
|
|
71
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
72
|
+
const media = matchMedia('(prefers-color-scheme: dark)');
|
|
73
|
+
media.addEventListener('change', refresh);
|
|
74
|
+
return () => {
|
|
75
|
+
observer.disconnect();
|
|
76
|
+
media.removeEventListener('change', refresh);
|
|
77
|
+
};
|
|
78
|
+
}, []);
|
|
79
|
+
return theme;
|
|
80
|
+
}
|
|
81
|
+
const colorOf = (theme, key) => theme.css(lineColor(key).replace(/^var\((.*)\)$/, '$1'));
|
|
82
|
+
const labelOf = (i, l) => i.lines.length === 1 ? i.label : `${i.label} · ${l.label}`;
|
|
83
|
+
/** A hex token with alpha (the shell's colour tokens are hex). */
|
|
84
|
+
export const alpha = (c, a) => /^#[0-9a-f]{6}$/i.test(c)
|
|
85
|
+
? `${c}${Math.round(a * 255)
|
|
86
|
+
.toString(16)
|
|
87
|
+
.padStart(2, '0')}`
|
|
88
|
+
: c;
|
|
89
|
+
/** Time labels in the person's zone (the axis would otherwise read UTC). */
|
|
90
|
+
export function timeText(t, zone, withDay) {
|
|
91
|
+
if (typeof t === 'string')
|
|
92
|
+
return t;
|
|
93
|
+
if (typeof t !== 'number')
|
|
94
|
+
return `${t.year}-${t.month}-${t.day}`;
|
|
95
|
+
return new Date(t * 1000).toLocaleString('en-US', {
|
|
96
|
+
timeZone: zone,
|
|
97
|
+
hour: 'numeric',
|
|
98
|
+
minute: '2-digit',
|
|
99
|
+
...(withDay ? { month: 'short', day: 'numeric' } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** A bar's time as Unix seconds (days read as UTC midnight). */
|
|
103
|
+
export const secOf = (t) => typeof t === 'number'
|
|
104
|
+
? t
|
|
105
|
+
: typeof t === 'string'
|
|
106
|
+
? Date.parse(`${t}T00:00:00Z`) / 1000
|
|
107
|
+
: Date.UTC(t.year, t.month - 1, t.day) / 1000;
|
|
108
|
+
/** First index whose time is at or after `t` (`secs` ascending); `secs.length` when none is. */
|
|
109
|
+
function lowerBound(secs, t) {
|
|
110
|
+
let lo = 0;
|
|
111
|
+
let hi = secs.length;
|
|
112
|
+
while (lo < hi) {
|
|
113
|
+
const mid = (lo + hi) >> 1;
|
|
114
|
+
if (secs[mid] < t)
|
|
115
|
+
lo = mid + 1;
|
|
116
|
+
else
|
|
117
|
+
hi = mid;
|
|
118
|
+
}
|
|
119
|
+
return lo;
|
|
120
|
+
}
|
|
121
|
+
/** The bars of `secs` inside `span`, as an index range (at least one bar). */
|
|
122
|
+
export function indexSpan(secs, span) {
|
|
123
|
+
const last = secs.length - 1;
|
|
124
|
+
const a = Math.min(last, lowerBound(secs, span.from));
|
|
125
|
+
const b = Math.max(a, Math.min(last, lowerBound(secs, span.to + 1) - 1));
|
|
126
|
+
return [a, b];
|
|
127
|
+
}
|
|
128
|
+
/** One chart: price in the chosen type, volume, then a pane per oscillator, all sharing the time axis. */
|
|
129
|
+
export function PriceChart({ bars, visible, type, chosen, values, timeZone = 'America/Los_Angeles', zoom = false, navigator = false, baseline = null, timeline, range, onRange, onOutside, className, }) {
|
|
130
|
+
const host = useRef(null);
|
|
131
|
+
const secs = useMemo(() => bars.map((b) => secOf(b.time)), [bars]);
|
|
132
|
+
// The span the chart keeps showing: set by `range`, by the person, and kept through rebuilds
|
|
133
|
+
// (a new indicator) and resizes (the dialog opening at zero width moved it before).
|
|
134
|
+
const target = useRef(null);
|
|
135
|
+
const applied = useRef(undefined);
|
|
136
|
+
const touch = useRef({ down: false, at: 0 });
|
|
137
|
+
const byPerson = () => touch.current.down || performance.now() - touch.current.at < 400;
|
|
138
|
+
const report = useRef(onRange);
|
|
139
|
+
report.current = onRange;
|
|
140
|
+
const theme = useTheme();
|
|
141
|
+
const [legend, setLegend] = useState([]);
|
|
142
|
+
const [api, setApi] = useState(null);
|
|
143
|
+
const live = useRef(null);
|
|
144
|
+
const hasVolume = bars.some((b) => (b.volume ?? 0) > 0);
|
|
145
|
+
const intraday = typeof bars[0]?.time === 'number';
|
|
146
|
+
const multiDay = intraday &&
|
|
147
|
+
bars.length > 1 &&
|
|
148
|
+
bars.at(-1)?.time - bars[0]?.time > 86_400;
|
|
149
|
+
const panes = useMemo(() => chosen.filter((i) => i.where === 'pane'), [chosen]);
|
|
150
|
+
const height = PRICE_PANE + (hasVolume ? VOLUME_PANE : 0) + panes.length * OSC_PANE + AXIS;
|
|
151
|
+
const drawn = useMemo(() => (type === 'heikin' ? heikinAshi(bars) : bars), [bars, type]);
|
|
152
|
+
// The chart is rebuilt whenever the type, picks, data or theme change. `range` is read through
|
|
153
|
+
// `applied`: a new span is shown by the effect below, not by a rebuild.
|
|
154
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: range, see above
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const el = host.current;
|
|
157
|
+
if (!el)
|
|
158
|
+
return;
|
|
159
|
+
const overlays = chosen.filter((i) => i.where === 'overlay');
|
|
160
|
+
const chart = createChart(el, {
|
|
161
|
+
autoSize: true,
|
|
162
|
+
layout: {
|
|
163
|
+
background: { type: ColorType.Solid, color: 'transparent' },
|
|
164
|
+
textColor: theme.text,
|
|
165
|
+
fontFamily: theme.css('--font-mono'),
|
|
166
|
+
fontSize: 11,
|
|
167
|
+
panes: { separatorColor: theme.border, enableResize: false },
|
|
168
|
+
},
|
|
169
|
+
grid: { vertLines: { visible: false }, horzLines: { color: theme.grid } },
|
|
170
|
+
rightPriceScale: { borderVisible: false },
|
|
171
|
+
timeScale: {
|
|
172
|
+
borderVisible: false,
|
|
173
|
+
rightOffset: 2,
|
|
174
|
+
// Max shows every bar on a phone too (the default 0.5 px stops at ~700).
|
|
175
|
+
minBarSpacing: 0.001,
|
|
176
|
+
timeVisible: intraday,
|
|
177
|
+
tickMarkFormatter: intraday ? (t) => timeText(t, timeZone, false) : undefined,
|
|
178
|
+
},
|
|
179
|
+
localization: { timeFormatter: (t) => timeText(t, timeZone, true) },
|
|
180
|
+
crosshair: { mode: CrosshairMode.Normal },
|
|
181
|
+
handleScale: { mouseWheel: zoom, pinch: zoom },
|
|
182
|
+
handleScroll: { mouseWheel: false },
|
|
183
|
+
});
|
|
184
|
+
const main = addPrice(chart, type, drawn, theme);
|
|
185
|
+
if (baseline !== null)
|
|
186
|
+
main.createPriceLine({
|
|
187
|
+
price: baseline,
|
|
188
|
+
color: theme.text,
|
|
189
|
+
lineWidth: 1,
|
|
190
|
+
lineStyle: 2,
|
|
191
|
+
axisLabelVisible: false,
|
|
192
|
+
});
|
|
193
|
+
const tracked = [];
|
|
194
|
+
for (const i of overlays) {
|
|
195
|
+
for (const l of i.lines) {
|
|
196
|
+
const color = colorOf(theme, l.key);
|
|
197
|
+
const s = chart.addSeries(LineSeries, {
|
|
198
|
+
color,
|
|
199
|
+
lineWidth: 2,
|
|
200
|
+
priceLineVisible: false,
|
|
201
|
+
lastValueVisible: false,
|
|
202
|
+
crosshairMarkerVisible: false,
|
|
203
|
+
}, 0);
|
|
204
|
+
s.setData(points(bars, values[l.key]));
|
|
205
|
+
tracked.push({ key: l.key, label: labelOf(i, l), color, series: s });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const first = hasVolume ? 2 : 1;
|
|
209
|
+
if (hasVolume) {
|
|
210
|
+
const volume = chart.addSeries(HistogramSeries, { priceFormat: { type: 'volume' }, priceLineVisible: false, lastValueVisible: false }, 1);
|
|
211
|
+
volume.setData(bars.map((b) => ({
|
|
212
|
+
time: b.time,
|
|
213
|
+
value: b.volume ?? 0,
|
|
214
|
+
color: alpha(b.close >= b.open ? theme.up : theme.down, 0.5),
|
|
215
|
+
})));
|
|
216
|
+
}
|
|
217
|
+
panes.forEach((i, n) => {
|
|
218
|
+
for (const l of i.lines) {
|
|
219
|
+
const color = colorOf(theme, l.key);
|
|
220
|
+
const s = l.bar
|
|
221
|
+
? chart.addSeries(HistogramSeries, { priceLineVisible: false, lastValueVisible: false }, n + first)
|
|
222
|
+
: chart.addSeries(LineSeries, { color, lineWidth: 2, priceLineVisible: false, lastValueVisible: false }, n + first);
|
|
223
|
+
if (l.bar) {
|
|
224
|
+
s.setData(points(bars, values[l.key]).map((p) => ({
|
|
225
|
+
...p,
|
|
226
|
+
color: p.value < 0 ? theme.down : theme.up,
|
|
227
|
+
})));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
s.setData(points(bars, values[l.key]));
|
|
231
|
+
}
|
|
232
|
+
tracked.push({ key: l.key, label: labelOf(i, l), color, series: s });
|
|
233
|
+
const domain = i.domain;
|
|
234
|
+
if (domain && !l.bar) {
|
|
235
|
+
s.applyOptions({
|
|
236
|
+
autoscaleInfoProvider: () => ({
|
|
237
|
+
priceRange: { minValue: domain[0], maxValue: domain[1] },
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const anchor = tracked.find((t) => t.key === i.lines[0]?.key)?.series;
|
|
243
|
+
for (const r of i.refs ?? []) {
|
|
244
|
+
anchor?.createPriceLine({
|
|
245
|
+
price: r,
|
|
246
|
+
color: theme.border,
|
|
247
|
+
lineWidth: 1,
|
|
248
|
+
lineStyle: 2,
|
|
249
|
+
axisLabelVisible: false,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
// Stretch factors keep the panes in these proportions at any chart height.
|
|
254
|
+
chart.panes()[0]?.setStretchFactor(PRICE_PANE);
|
|
255
|
+
if (hasVolume)
|
|
256
|
+
chart.panes()[1]?.setStretchFactor(VOLUME_PANE);
|
|
257
|
+
panes.forEach((_, n) => chart.panes()[n + first]?.setStretchFactor(OSC_PANE));
|
|
258
|
+
if (range && range !== applied.current) {
|
|
259
|
+
applied.current = range;
|
|
260
|
+
target.current = range;
|
|
261
|
+
}
|
|
262
|
+
else if (!target.current) {
|
|
263
|
+
const n = visible && visible < bars.length ? bars.length - visible : 0;
|
|
264
|
+
target.current = range ?? { from: secs[n] ?? 0, to: Number.POSITIVE_INFINITY };
|
|
265
|
+
}
|
|
266
|
+
const show = () => {
|
|
267
|
+
if (target.current)
|
|
268
|
+
showSpan(chart, secs, target.current);
|
|
269
|
+
};
|
|
270
|
+
show();
|
|
271
|
+
// The host can be 0 wide when the chart is made (a dialog still opening): show the span again
|
|
272
|
+
// once it has its size, and after any resize, rather than keep the bar spacing.
|
|
273
|
+
let frame = 0;
|
|
274
|
+
const sized = new ResizeObserver(() => {
|
|
275
|
+
cancelAnimationFrame(frame);
|
|
276
|
+
frame = requestAnimationFrame(show);
|
|
277
|
+
});
|
|
278
|
+
sized.observe(el);
|
|
279
|
+
const press = () => {
|
|
280
|
+
touch.current.down = true;
|
|
281
|
+
};
|
|
282
|
+
const release = () => {
|
|
283
|
+
if (touch.current.down)
|
|
284
|
+
touch.current = { down: false, at: performance.now() };
|
|
285
|
+
};
|
|
286
|
+
const wheel = () => {
|
|
287
|
+
touch.current.at = performance.now();
|
|
288
|
+
};
|
|
289
|
+
el.addEventListener('pointerdown', press);
|
|
290
|
+
el.addEventListener('wheel', wheel, { passive: true });
|
|
291
|
+
window.addEventListener('pointerup', release);
|
|
292
|
+
window.addEventListener('pointercancel', release);
|
|
293
|
+
const moved = (r) => {
|
|
294
|
+
if (!r || !secs.length)
|
|
295
|
+
return;
|
|
296
|
+
const last = secs.length - 1;
|
|
297
|
+
const a = Math.max(0, Math.min(last, Math.round(r.from)));
|
|
298
|
+
const b = Math.max(a, Math.min(last, Math.round(r.to)));
|
|
299
|
+
const span = { from: secs[a], to: secs[b] };
|
|
300
|
+
const person = byPerson();
|
|
301
|
+
if (person)
|
|
302
|
+
target.current = span;
|
|
303
|
+
report.current?.(span, person);
|
|
304
|
+
};
|
|
305
|
+
chart.timeScale().subscribeVisibleLogicalRangeChange(moved);
|
|
306
|
+
const at = (t) => {
|
|
307
|
+
const i = t === undefined ? -1 : bars.findIndex((b) => b.time === t);
|
|
308
|
+
return i < 0 ? bars.length - 1 : i;
|
|
309
|
+
};
|
|
310
|
+
const legendFor = (param) => {
|
|
311
|
+
const i = at(param?.time);
|
|
312
|
+
const bar = bars[i];
|
|
313
|
+
const draw = drawn[i];
|
|
314
|
+
const rows = [];
|
|
315
|
+
if (bar && draw) {
|
|
316
|
+
// From the baseline when there is one (the prior close, as the header measures), else
|
|
317
|
+
// from the bar before.
|
|
318
|
+
const prev = baseline ?? bars[i - 1]?.close ?? bar.open;
|
|
319
|
+
const change = bar.close - prev;
|
|
320
|
+
const tone = change >= 0 ? theme.up : theme.down;
|
|
321
|
+
const prefix = type === 'heikin' ? 'HA ' : '';
|
|
322
|
+
rows.push({ key: 'date', value: timeText(bar.time, timeZone, true) });
|
|
323
|
+
rows.push({
|
|
324
|
+
key: 'ohlc',
|
|
325
|
+
color: tone,
|
|
326
|
+
value: type === 'line' || type === 'area'
|
|
327
|
+
? `C ${price(bar.close)}`
|
|
328
|
+
: `${prefix}O ${price(draw.open)} H ${price(draw.high)} L ${price(draw.low)} C ${price(draw.close)}`,
|
|
329
|
+
});
|
|
330
|
+
rows.push({
|
|
331
|
+
key: 'change',
|
|
332
|
+
color: tone,
|
|
333
|
+
value: `${change >= 0 ? '▲' : '▼'} ${signed(change)}${baseline ? ` (${signed((change / baseline) * 100)}%)` : ''}`,
|
|
334
|
+
});
|
|
335
|
+
if (hasVolume)
|
|
336
|
+
rows.push({ key: 'volume', value: `Vol ${COMPACT.format(bar.volume ?? 0)}` });
|
|
337
|
+
for (const t2 of tracked) {
|
|
338
|
+
const v = values[t2.key]?.get(bar.time);
|
|
339
|
+
if (v !== undefined)
|
|
340
|
+
rows.push({
|
|
341
|
+
key: t2.key,
|
|
342
|
+
label: t2.label,
|
|
343
|
+
color: t2.color,
|
|
344
|
+
value: price(v),
|
|
345
|
+
dot: true,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return rows;
|
|
350
|
+
};
|
|
351
|
+
setLegend(legendFor(null));
|
|
352
|
+
chart.subscribeCrosshairMove((p) => setLegend(legendFor(p.time ? p : null)));
|
|
353
|
+
live.current = chart;
|
|
354
|
+
setApi(chart);
|
|
355
|
+
return () => {
|
|
356
|
+
sized.disconnect();
|
|
357
|
+
cancelAnimationFrame(frame);
|
|
358
|
+
el.removeEventListener('pointerdown', press);
|
|
359
|
+
el.removeEventListener('wheel', wheel);
|
|
360
|
+
window.removeEventListener('pointerup', release);
|
|
361
|
+
window.removeEventListener('pointercancel', release);
|
|
362
|
+
// The navigator stays up (and keeps a drag going) while the chart is rebuilt under it.
|
|
363
|
+
chart.remove();
|
|
364
|
+
};
|
|
365
|
+
}, [
|
|
366
|
+
bars,
|
|
367
|
+
secs,
|
|
368
|
+
drawn,
|
|
369
|
+
visible,
|
|
370
|
+
type,
|
|
371
|
+
chosen,
|
|
372
|
+
panes,
|
|
373
|
+
values,
|
|
374
|
+
theme,
|
|
375
|
+
timeZone,
|
|
376
|
+
zoom,
|
|
377
|
+
baseline,
|
|
378
|
+
hasVolume,
|
|
379
|
+
intraday,
|
|
380
|
+
]);
|
|
381
|
+
// A new `range` (a window button) moves the chart there.
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (!api || !range || range === applied.current)
|
|
384
|
+
return;
|
|
385
|
+
applied.current = range;
|
|
386
|
+
target.current = range;
|
|
387
|
+
showSpan(api, secs, range);
|
|
388
|
+
}, [api, range, secs]);
|
|
389
|
+
const goTo = (span) => {
|
|
390
|
+
touch.current.at = performance.now();
|
|
391
|
+
const day = 86_400;
|
|
392
|
+
if (onOutside &&
|
|
393
|
+
secs.length &&
|
|
394
|
+
(span.from < secs[0] - day ||
|
|
395
|
+
(Number.isFinite(span.to) && span.to > secs.at(-1) + day))) {
|
|
396
|
+
onOutside(span);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (live.current)
|
|
400
|
+
showSpan(live.current, secs, span);
|
|
401
|
+
};
|
|
402
|
+
return (_jsxs("div", { "data-testid": "price-chart", "data-chart-type": type, "data-bars": bars.length, className: cn('flex flex-col gap-1', className), children: [_jsx("div", { className: "flex min-h-5 flex-wrap items-center gap-x-4 gap-y-0.5 px-2 text-xs", "data-testid": "chart-legend", children: legend.map(({ key, label, color, value, dot }) => (_jsxs("div", { className: "num flex items-center gap-2 whitespace-nowrap text-muted-foreground", children: [dot && (_jsx("span", { "aria-hidden": true, className: "inline-block size-2 rounded-full", style: { background: color } })), label && _jsx("span", { children: label }), _jsx("span", { className: dot ? 'text-foreground' : undefined, style: dot ? undefined : { color }, children: value })] }, key))) }), _jsxs("div", { className: "relative", children: [_jsx("div", { ref: host, style: { height } }), _jsxs("div", { className: "pointer-events-none absolute inset-0 text-muted-foreground text-xs", children: [hasVolume && (_jsx("span", { className: "absolute left-2", style: { top: PRICE_PANE + 8 }, children: "Volume" })), panes.map((p, n) => (_jsx("div", { className: "absolute inset-x-0", style: {
|
|
403
|
+
top: PRICE_PANE + (hasVolume ? VOLUME_PANE : 0) + n * OSC_PANE,
|
|
404
|
+
height: OSC_PANE,
|
|
405
|
+
}, children: _jsx("h3", { className: "px-2 pt-2", children: p.label }) }, p.id)))] })] }), navigator && api && (timeline ?? bars).length > 2 && (_jsx(Navigator, { chart: api, bars: bars, secs: secs, timeline: timeline ?? bars, theme: theme, timeZone: timeZone, multiDay: multiDay, goTo: goTo }))] }));
|
|
406
|
+
}
|
|
407
|
+
/** Shows the bars inside `span` (at least one), edge to edge. */
|
|
408
|
+
export function showSpan(chart, secs, span) {
|
|
409
|
+
if (!secs.length)
|
|
410
|
+
return;
|
|
411
|
+
const [a, b] = indexSpan(secs, span);
|
|
412
|
+
const pad = b === secs.length - 1 ? 2 : 0.5;
|
|
413
|
+
chart.timeScale().setVisibleLogicalRange({ from: a - 0.5, to: b + pad });
|
|
414
|
+
}
|
|
415
|
+
const points = (bars, by) => bars.flatMap((b) => {
|
|
416
|
+
const v = by?.get(b.time);
|
|
417
|
+
return v === undefined ? [] : [{ time: b.time, value: v }];
|
|
418
|
+
});
|
|
419
|
+
function addPrice(chart, type, data, theme) {
|
|
420
|
+
const ohlc = data.map((b) => ({
|
|
421
|
+
time: b.time,
|
|
422
|
+
open: b.open,
|
|
423
|
+
high: b.high,
|
|
424
|
+
low: b.low,
|
|
425
|
+
close: b.close,
|
|
426
|
+
}));
|
|
427
|
+
// Heikin-Ashi closes are averages, so the axis tag would not match the real last price.
|
|
428
|
+
const common = { priceLineVisible: false, lastValueVisible: type !== 'heikin' };
|
|
429
|
+
if (type === 'line') {
|
|
430
|
+
const s = chart.addSeries(LineSeries, { ...common, color: theme.primary, lineWidth: 2 }, 0);
|
|
431
|
+
s.setData(data.map((b) => ({ time: b.time, value: b.close })));
|
|
432
|
+
return s;
|
|
433
|
+
}
|
|
434
|
+
if (type === 'area') {
|
|
435
|
+
const s = chart.addSeries(AreaSeries, {
|
|
436
|
+
...common,
|
|
437
|
+
lineColor: theme.primary,
|
|
438
|
+
topColor: alpha(theme.primary, 0.33),
|
|
439
|
+
bottomColor: alpha(theme.primary, 0.02),
|
|
440
|
+
lineWidth: 2,
|
|
441
|
+
}, 0);
|
|
442
|
+
s.setData(data.map((b) => ({ time: b.time, value: b.close })));
|
|
443
|
+
return s;
|
|
444
|
+
}
|
|
445
|
+
if (type === 'ohlc') {
|
|
446
|
+
const s = chart.addSeries(BarSeries, { ...common, upColor: theme.up, downColor: theme.down }, 0);
|
|
447
|
+
s.setData(ohlc);
|
|
448
|
+
return s;
|
|
449
|
+
}
|
|
450
|
+
const s = chart.addSeries(CandlestickSeries, {
|
|
451
|
+
...common,
|
|
452
|
+
upColor: theme.up,
|
|
453
|
+
downColor: theme.down,
|
|
454
|
+
wickUpColor: theme.up,
|
|
455
|
+
wickDownColor: theme.down,
|
|
456
|
+
borderVisible: false,
|
|
457
|
+
}, 0);
|
|
458
|
+
s.setData(ohlc);
|
|
459
|
+
return s;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* The timeline under the chart (whole history): the symbol's whole
|
|
463
|
+
* history as a small area chart, with the span the chart shows cut out of it. Drag the window to
|
|
464
|
+
* move, drag its edges to zoom, click outside it to jump there. lightweight-charts has no
|
|
465
|
+
* navigator, so this is a second chart kept in step by time.
|
|
466
|
+
*/
|
|
467
|
+
export function Navigator({ chart, bars, secs, timeline, theme, timeZone, multiDay, goTo, }) {
|
|
468
|
+
const host = useRef(null);
|
|
469
|
+
const strip = useRef(null);
|
|
470
|
+
const tSecs = useMemo(() => timeline.map((b) => secOf(b.time)), [timeline]);
|
|
471
|
+
const last = timeline.length - 1;
|
|
472
|
+
const [shown, setShown] = useState(null);
|
|
473
|
+
const drag = useRef(null);
|
|
474
|
+
useEffect(() => {
|
|
475
|
+
const el = host.current;
|
|
476
|
+
if (!el)
|
|
477
|
+
return;
|
|
478
|
+
const nav = createChart(el, {
|
|
479
|
+
autoSize: true,
|
|
480
|
+
layout: {
|
|
481
|
+
background: { type: ColorType.Solid, color: 'transparent' },
|
|
482
|
+
textColor: theme.text,
|
|
483
|
+
attributionLogo: false,
|
|
484
|
+
},
|
|
485
|
+
grid: { vertLines: { visible: false }, horzLines: { visible: false } },
|
|
486
|
+
rightPriceScale: { visible: false },
|
|
487
|
+
leftPriceScale: { visible: false },
|
|
488
|
+
timeScale: { visible: false, borderVisible: false, rightOffset: 0, minBarSpacing: 0.001 },
|
|
489
|
+
crosshair: { vertLine: { visible: false }, horzLine: { visible: false } },
|
|
490
|
+
handleScale: false,
|
|
491
|
+
handleScroll: false,
|
|
492
|
+
});
|
|
493
|
+
const s = nav.addSeries(AreaSeries, {
|
|
494
|
+
lineColor: theme.primary,
|
|
495
|
+
topColor: alpha(theme.primary, 0.25),
|
|
496
|
+
bottomColor: alpha(theme.primary, 0.02),
|
|
497
|
+
lineWidth: 1,
|
|
498
|
+
priceLineVisible: false,
|
|
499
|
+
lastValueVisible: false,
|
|
500
|
+
crosshairMarkerVisible: false,
|
|
501
|
+
});
|
|
502
|
+
s.setData(timeline.map((b) => ({ time: b.time, value: b.close })));
|
|
503
|
+
// Stretch the whole history edge to edge, again once the strip has its width.
|
|
504
|
+
const fit = () => nav.timeScale().setVisibleLogicalRange({ from: -0.5, to: timeline.length - 0.5 });
|
|
505
|
+
fit();
|
|
506
|
+
const sized = new ResizeObserver(() => requestAnimationFrame(fit));
|
|
507
|
+
sized.observe(el);
|
|
508
|
+
return () => {
|
|
509
|
+
sized.disconnect();
|
|
510
|
+
nav.remove();
|
|
511
|
+
};
|
|
512
|
+
}, [timeline, theme]);
|
|
513
|
+
// The chart's shown bars, as main-chart indexes.
|
|
514
|
+
useEffect(() => {
|
|
515
|
+
const ts = chart.timeScale();
|
|
516
|
+
const on = (r) => {
|
|
517
|
+
if (!r || !secs.length)
|
|
518
|
+
return;
|
|
519
|
+
const n = secs.length - 1;
|
|
520
|
+
const a = Math.max(0, Math.min(n, Math.round(r.from)));
|
|
521
|
+
setShown([a, Math.max(a, Math.min(n, Math.round(r.to)))]);
|
|
522
|
+
};
|
|
523
|
+
on(ts.getVisibleLogicalRange());
|
|
524
|
+
ts.subscribeVisibleLogicalRangeChange(on);
|
|
525
|
+
return () => {
|
|
526
|
+
// The chart may already be rebuilt and gone.
|
|
527
|
+
try {
|
|
528
|
+
ts.unsubscribeVisibleLogicalRangeChange(on);
|
|
529
|
+
}
|
|
530
|
+
catch { }
|
|
531
|
+
};
|
|
532
|
+
}, [chart, secs]);
|
|
533
|
+
/** Where a time sits on the timeline, as a fractional index. */
|
|
534
|
+
const pos = (t) => {
|
|
535
|
+
const i = lowerBound(tSecs, t);
|
|
536
|
+
if (i <= 0)
|
|
537
|
+
return 0;
|
|
538
|
+
if (i > last)
|
|
539
|
+
return last;
|
|
540
|
+
const a = tSecs[i - 1];
|
|
541
|
+
const b = tSecs[i];
|
|
542
|
+
return i - 1 + (b > a ? (t - a) / (b - a) : 0);
|
|
543
|
+
};
|
|
544
|
+
/** The time at a fractional timeline index. */
|
|
545
|
+
const timeAt = (x) => {
|
|
546
|
+
const i = Math.max(0, Math.min(last, Math.floor(x)));
|
|
547
|
+
const a = tSecs[i];
|
|
548
|
+
const b = tSecs[Math.min(last, i + 1)];
|
|
549
|
+
return a + (b - a) * (x - i);
|
|
550
|
+
};
|
|
551
|
+
const startSec = shown ? secs[shown[0]] : tSecs[0];
|
|
552
|
+
const endSec = shown ? secs[shown[1]] : tSecs[last];
|
|
553
|
+
const from = pos(startSec);
|
|
554
|
+
const to = Math.max(from, pos(endSec));
|
|
555
|
+
const pct = (i) => (last ? (i / last) * 100 : 0);
|
|
556
|
+
const indexAt = (clientX) => {
|
|
557
|
+
const r = strip.current?.getBoundingClientRect();
|
|
558
|
+
if (!r)
|
|
559
|
+
return 0;
|
|
560
|
+
return Math.max(0, Math.min(last, ((clientX - r.left) / r.width) * last));
|
|
561
|
+
};
|
|
562
|
+
// A window reaching the last timeline day runs to the chart's last bar (today's intraday bars
|
|
563
|
+
// sit after the last daily one).
|
|
564
|
+
const set = (a, b) => goTo({ from: timeAt(a), to: b >= last ? Number.POSITIVE_INFINITY : timeAt(b) });
|
|
565
|
+
const MIN = Math.min(10, last);
|
|
566
|
+
const down = (e) => {
|
|
567
|
+
const i = indexAt(e.clientX);
|
|
568
|
+
const near = (last / 100) * 2;
|
|
569
|
+
let mode = 'm';
|
|
570
|
+
if (Math.abs(i - from) <= near && Math.abs(i - to) <= near)
|
|
571
|
+
mode = i < from ? 'l' : 'r';
|
|
572
|
+
else if (Math.abs(i - from) <= near)
|
|
573
|
+
mode = 'l';
|
|
574
|
+
else if (Math.abs(i - to) <= near)
|
|
575
|
+
mode = 'r';
|
|
576
|
+
else if (i < from || i > to) {
|
|
577
|
+
// Jump: centre the window where the timeline was clicked.
|
|
578
|
+
const w = Math.max(to - from, MIN);
|
|
579
|
+
const a = Math.max(0, Math.min(last - w, i - w / 2));
|
|
580
|
+
set(a, a + w);
|
|
581
|
+
drag.current = { mode: 'm', x: i, from: a, to: a + w };
|
|
582
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
drag.current = { mode, x: i, from, to };
|
|
586
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
587
|
+
};
|
|
588
|
+
const move = (e) => {
|
|
589
|
+
const d = drag.current;
|
|
590
|
+
if (!d)
|
|
591
|
+
return;
|
|
592
|
+
const i = indexAt(e.clientX);
|
|
593
|
+
if (d.mode === 'l')
|
|
594
|
+
set(Math.min(i, d.to - MIN), d.to);
|
|
595
|
+
else if (d.mode === 'r')
|
|
596
|
+
set(d.from, Math.max(i, d.from + MIN));
|
|
597
|
+
else {
|
|
598
|
+
const w = d.to - d.from;
|
|
599
|
+
const a = Math.max(0, Math.min(last - w, d.from + (i - d.x)));
|
|
600
|
+
set(a, a + w);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const up = () => {
|
|
604
|
+
drag.current = null;
|
|
605
|
+
};
|
|
606
|
+
const edge = (b) => b ? timeText(b.time, timeZone, multiDay || typeof b.time === 'string') : '';
|
|
607
|
+
const shownLabel = (i) => edge(i === undefined ? undefined : bars[i]);
|
|
608
|
+
return (_jsxs("div", { className: "flex flex-col gap-0.5 pr-14", children: [_jsxs("div", { ref: strip, role: "slider", "aria-label": "Timeline: drag the window or its edges", "aria-valuemin": 0, "aria-valuemax": last, "aria-valuenow": Math.round(from), "aria-valuetext": `${shownLabel(shown?.[0])} to ${shownLabel(shown?.[1])}`, tabIndex: 0, onKeyDown: (e) => {
|
|
609
|
+
const w = to - from;
|
|
610
|
+
const step = Math.max(1, Math.round(w / 10));
|
|
611
|
+
if (e.key === 'ArrowLeft')
|
|
612
|
+
set(Math.max(0, from - step), Math.max(0, from - step) + w);
|
|
613
|
+
if (e.key === 'ArrowRight')
|
|
614
|
+
set(Math.min(last - w, from + step), Math.min(last, to + step));
|
|
615
|
+
}, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up, className: "relative cursor-grab touch-none overflow-hidden rounded-md border bg-muted/30 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none active:cursor-grabbing", style: { height: NAV }, children: [_jsx("div", { ref: host, className: "pointer-events-none absolute inset-0" }), _jsx("div", { className: "pointer-events-none absolute inset-y-0 left-0 bg-background/60", style: { width: `${pct(from)}%` } }), _jsx("div", { className: "pointer-events-none absolute inset-y-0 right-0 bg-background/60", style: { width: `${100 - pct(to)}%` } }), _jsxs("div", { className: "pointer-events-none absolute inset-y-px min-w-1.5 rounded-sm border border-muted-foreground/60", style: {
|
|
616
|
+
left: `min(${pct(from)}%, calc(100% - 0.375rem))`,
|
|
617
|
+
width: `${pct(to) - pct(from)}%`,
|
|
618
|
+
}, children: [_jsx("span", { className: "absolute top-1/2 -left-1 h-6 w-1.5 -translate-y-1/2 rounded-sm bg-muted-foreground" }), _jsx("span", { className: "absolute top-1/2 -right-1 h-6 w-1.5 -translate-y-1/2 rounded-sm bg-muted-foreground" })] })] }), _jsxs("div", { className: "num flex justify-between text-[10px] text-muted-foreground", children: [_jsx("span", { children: edge(timeline[0]) }), _jsxs("span", { children: [shownLabel(shown?.[0]), " to ", shownLabel(shown?.[1])] }), _jsx("span", { children: edge(timeline[last]) })] })] }));
|
|
619
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** One MCP server the app offers. */
|
|
3
|
+
export interface McpServer {
|
|
4
|
+
id: string;
|
|
5
|
+
/** Its name in lists and steps (an app). */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Its whole address. */
|
|
8
|
+
url: string;
|
|
9
|
+
/** The name clients keep it under (`myapp-main`). */
|
|
10
|
+
key: string;
|
|
11
|
+
/** More steps for scripts (pushing data in), and an example to paste. */
|
|
12
|
+
script?: {
|
|
13
|
+
steps?: ReactNode;
|
|
14
|
+
example?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function McpSettings({ servers, current }: {
|
|
18
|
+
servers: McpServer[];
|
|
19
|
+
current?: string;
|
|
20
|
+
}): import("react").JSX.Element | null;
|