@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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Market widgets (mockup "Dashboard Outlook"): a view widget of kind `market`
|
|
3
|
+
* shows symbols' 1m bars for the session, live while it trades. Three looks:
|
|
4
|
+
*
|
|
5
|
+
* lines one wide chart of each symbol's % change from the prior close (the indexes), with a
|
|
6
|
+
* legend of name, change and level; the crosshair reads every line at that minute
|
|
7
|
+
* tiles one widget per symbol: change, a sparkline over the prior close, the value, and Live,
|
|
8
|
+
* "via ETF" or a gold Delayed chip
|
|
9
|
+
* today one row per symbol: up or down today, by how much, and its price now
|
|
10
|
+
*
|
|
11
|
+
* While the session is shut, each draws the trades since its close (pre-market, after hours,
|
|
12
|
+
* overnight; an index through its ETF) dashed after the session, and Today measures from the
|
|
13
|
+
* session's close once the overnight session has begun.
|
|
14
|
+
*
|
|
15
|
+
* A name or a widget opens the chart dialog. Without a market provider (apps other than an app,
|
|
16
|
+
* or the exported an app) the widget says live data is not available here.
|
|
17
|
+
*/
|
|
18
|
+
import { type MarketSeries } from '@columenlabs/lumen-ui-types/market';
|
|
19
|
+
export declare function fmtValue(s: Pick<MarketSeries, 'prefix' | 'suffix' | 'decimals'>, v: number): string;
|
|
20
|
+
/** Change from the prior close, in percent. */
|
|
21
|
+
export declare const changeOf: (s: MarketSeries) => number | null;
|
|
22
|
+
/** "● Live · 10:18 AM PDT", or the closed line, for a page or card header. */
|
|
23
|
+
export declare function MarketBadge({ symbols }: {
|
|
24
|
+
symbols: string[];
|
|
25
|
+
}): import("react").JSX.Element | null;
|
|
26
|
+
/** A widget of kind `market`. */
|
|
27
|
+
export declare function MarketWidget({ symbols, style, }: {
|
|
28
|
+
symbols: string[];
|
|
29
|
+
style: 'lines' | 'tiles' | 'today';
|
|
30
|
+
}): import("react").JSX.Element;
|
|
31
|
+
/** One wide chart: each symbol's % change from the prior close, 1m. */
|
|
32
|
+
export declare function MarketLines({ symbols: all }: {
|
|
33
|
+
symbols: string[];
|
|
34
|
+
}): import("react").JSX.Element;
|
|
35
|
+
/** One widget per symbol: change, sparkline over the prior close, value, and its source chip. */
|
|
36
|
+
export declare function MarketTiles({ symbols }: {
|
|
37
|
+
symbols: string[];
|
|
38
|
+
}): import("react").JSX.Element;
|
|
39
|
+
/**
|
|
40
|
+
* Up or down today: one row per symbol with its price now and its change today,
|
|
41
|
+
* in the session or since it closed (the session it traded in named), and how many are up and down.
|
|
42
|
+
*/
|
|
43
|
+
export declare function MarketToday({ symbols: all }: {
|
|
44
|
+
symbols: string[];
|
|
45
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Market widgets (mockup "Dashboard Outlook"): a view widget of kind `market`
|
|
4
|
+
* shows symbols' 1m bars for the session, live while it trades. Three looks:
|
|
5
|
+
*
|
|
6
|
+
* lines one wide chart of each symbol's % change from the prior close (the indexes), with a
|
|
7
|
+
* legend of name, change and level; the crosshair reads every line at that minute
|
|
8
|
+
* tiles one widget per symbol: change, a sparkline over the prior close, the value, and Live,
|
|
9
|
+
* "via ETF" or a gold Delayed chip
|
|
10
|
+
* today one row per symbol: up or down today, by how much, and its price now
|
|
11
|
+
*
|
|
12
|
+
* While the session is shut, each draws the trades since its close (pre-market, after hours,
|
|
13
|
+
* overnight; an index through its ETF) dashed after the session, and Today measures from the
|
|
14
|
+
* session's close once the overnight session has begun.
|
|
15
|
+
*
|
|
16
|
+
* A name or a widget opens the chart dialog. Without a market provider (apps other than an app,
|
|
17
|
+
* or the exported an app) the widget says live data is not available here.
|
|
18
|
+
*/
|
|
19
|
+
import { isDailyMarketKey } from '@columenlabs/lumen-ui-types/market';
|
|
20
|
+
import { ColorType, CrosshairMode, LineSeries, createChart, } from 'lightweight-charts';
|
|
21
|
+
import { CircleAlert, CircleSlash, LoaderCircle, RotateCw } from 'lucide-react';
|
|
22
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
23
|
+
import { Area, AreaChart, ReferenceLine, YAxis } from 'recharts';
|
|
24
|
+
import { IconButton } from '../icon-button.js';
|
|
25
|
+
import { KIT } from '../kit-config.js';
|
|
26
|
+
import { cn } from '../lib/utils.js';
|
|
27
|
+
import { usePrefs } from '../prefs.js';
|
|
28
|
+
import { Badge } from '../ui/badge.js';
|
|
29
|
+
import { Button } from '../ui/button.js';
|
|
30
|
+
import { ChartContainer } from '../ui/chart.js';
|
|
31
|
+
import { Skeleton } from '../ui/skeleton.js';
|
|
32
|
+
import { extendedOf, todayOf, useMarket, useMarketDaily } from './live.js';
|
|
33
|
+
import { useSymbolChart } from './lumen-chart.js';
|
|
34
|
+
import { faint } from './price-chart.js';
|
|
35
|
+
const LINE_TOKENS = ['--chart-1', '--chart-3', '--chart-4', '--chart-6', '--chart-2', '--chart-5'];
|
|
36
|
+
const MINUS = '−';
|
|
37
|
+
export function fmtValue(s, v) {
|
|
38
|
+
return `${s.prefix}${v
|
|
39
|
+
.toLocaleString('en-US', {
|
|
40
|
+
minimumFractionDigits: s.decimals,
|
|
41
|
+
maximumFractionDigits: s.decimals,
|
|
42
|
+
})
|
|
43
|
+
.replace('-', MINUS)}${s.suffix}`;
|
|
44
|
+
}
|
|
45
|
+
const fmtPct = (p) => `${p >= 0 ? '+' : MINUS}${Math.abs(p).toFixed(2)}% ${p >= 0 ? '▲' : '▼'}`;
|
|
46
|
+
/** A daily series' change in its own units: points for a percentage (`+4.4 pts`). */
|
|
47
|
+
const fmtPts = (s, p) => `${p >= 0 ? '+' : MINUS}${Math.abs(p).toFixed(s.decimals)}${s.suffix === '%' ? ' pts' : ''}`;
|
|
48
|
+
const day = (iso) => iso
|
|
49
|
+
? new Date(`${iso}T12:00:00Z`).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
50
|
+
: '';
|
|
51
|
+
/** Change from the prior close, in percent. */
|
|
52
|
+
export const changeOf = (s) => {
|
|
53
|
+
const last = s.bars.at(-1)?.c;
|
|
54
|
+
return last !== undefined && s.prevClose ? (last / s.prevClose - 1) * 100 : null;
|
|
55
|
+
};
|
|
56
|
+
function clock(sec, zone) {
|
|
57
|
+
return new Date(sec * 1000).toLocaleTimeString('en-US', {
|
|
58
|
+
timeZone: zone,
|
|
59
|
+
hour: 'numeric',
|
|
60
|
+
minute: '2-digit',
|
|
61
|
+
timeZoneName: 'short',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** "● Live · 10:18 AM PDT", or the closed line, for a page or card header. */
|
|
65
|
+
export function MarketBadge({ symbols }) {
|
|
66
|
+
const { timeZone } = usePrefs();
|
|
67
|
+
const m = useMarket(symbols);
|
|
68
|
+
if (m.status === 'off')
|
|
69
|
+
return null;
|
|
70
|
+
const lastBar = Math.max(0, ...Object.values(m.series).map((s) => s.bars.at(-1)?.t ?? 0));
|
|
71
|
+
const ext = Object.values(m.series)
|
|
72
|
+
.map(extendedOf)
|
|
73
|
+
.filter((x) => !!x)
|
|
74
|
+
.sort((a, b) => b.at - a.at)[0];
|
|
75
|
+
const live = m.status === 'live';
|
|
76
|
+
const text = m.status === 'loading'
|
|
77
|
+
? 'Loading'
|
|
78
|
+
: m.status === 'connecting'
|
|
79
|
+
? 'Connecting'
|
|
80
|
+
: m.status === 'error'
|
|
81
|
+
? 'Not reachable'
|
|
82
|
+
: live
|
|
83
|
+
? `Live${lastBar ? ` · ${clock(lastBar + 60, timeZone)}` : ''}`
|
|
84
|
+
: ext
|
|
85
|
+
? `${ext.label} · ${clock(ext.at + 300, timeZone)}`
|
|
86
|
+
: `Closed${m.session ? ` · last bar ${clock(m.session.close, timeZone)}` : ''}`;
|
|
87
|
+
return (_jsxs(Badge, { variant: "outline", className: cn('gap-1.5 font-normal', live ? 'border-gain/40 text-gain' : 'text-muted-foreground'), children: [_jsx("span", { className: cn('size-1.5 rounded-full', live ? 'bg-gain' : 'bg-muted-foreground') }), text] }));
|
|
88
|
+
}
|
|
89
|
+
function Unavailable({ status, error, onRetry, }) {
|
|
90
|
+
return (_jsxs("div", { className: "flex flex-col items-center gap-2 py-6 text-center text-sm text-muted-foreground", children: [_jsx("p", { children: status === 'off'
|
|
91
|
+
? KIT.marketOff
|
|
92
|
+
: `Market data is not reachable · ${error ?? 'no answer'}` }), status !== 'off' && onRetry && (_jsx(IconButton, { icon: RotateCw, label: "Retry", variant: "outline", onClick: onRetry }))] }));
|
|
93
|
+
}
|
|
94
|
+
/** A widget of kind `market`. */
|
|
95
|
+
export function MarketWidget({ symbols, style, }) {
|
|
96
|
+
if (style === 'lines')
|
|
97
|
+
return _jsx(MarketLines, { symbols: symbols });
|
|
98
|
+
if (style === 'today')
|
|
99
|
+
return _jsx(MarketToday, { symbols: symbols });
|
|
100
|
+
return _jsx(MarketTiles, { symbols: symbols });
|
|
101
|
+
}
|
|
102
|
+
/** One wide chart: each symbol's % change from the prior close, 1m. */
|
|
103
|
+
export function MarketLines({ symbols: all }) {
|
|
104
|
+
const symbols = useMemo(() => all.filter((k) => !isDailyMarketKey(k)), [all]);
|
|
105
|
+
const m = useMarket(symbols);
|
|
106
|
+
const open = useSymbolChart();
|
|
107
|
+
const { timeZone } = usePrefs();
|
|
108
|
+
const host = useRef(null);
|
|
109
|
+
const chart = useRef(null);
|
|
110
|
+
const lines = useRef(new Map());
|
|
111
|
+
const extLines = useRef(new Map());
|
|
112
|
+
const [hover, setHover] = useState(null);
|
|
113
|
+
const shown = symbols.map((s) => m.series[s]).filter((s) => !!s);
|
|
114
|
+
const ready = shown.length > 0;
|
|
115
|
+
// The chart itself, once; lines are added as their series arrive.
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const el = host.current;
|
|
118
|
+
if (!el || !ready)
|
|
119
|
+
return;
|
|
120
|
+
const css = getComputedStyle(document.documentElement);
|
|
121
|
+
const v = (n) => css.getPropertyValue(n).trim();
|
|
122
|
+
const c = createChart(el, {
|
|
123
|
+
autoSize: true,
|
|
124
|
+
layout: {
|
|
125
|
+
background: { type: ColorType.Solid, color: 'transparent' },
|
|
126
|
+
textColor: v('--muted-foreground'),
|
|
127
|
+
fontFamily: v('--font-mono'),
|
|
128
|
+
fontSize: 11,
|
|
129
|
+
attributionLogo: false,
|
|
130
|
+
},
|
|
131
|
+
grid: { vertLines: { visible: false }, horzLines: { color: faint(v('--border')) } },
|
|
132
|
+
rightPriceScale: { borderVisible: false },
|
|
133
|
+
timeScale: {
|
|
134
|
+
borderVisible: false,
|
|
135
|
+
timeVisible: true,
|
|
136
|
+
fixLeftEdge: true,
|
|
137
|
+
fixRightEdge: true,
|
|
138
|
+
tickMarkFormatter: (t) => new Date(t * 1000).toLocaleTimeString('en-US', {
|
|
139
|
+
timeZone,
|
|
140
|
+
hour: 'numeric',
|
|
141
|
+
minute: '2-digit',
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
localization: {
|
|
145
|
+
timeFormatter: (t) => clock(t, timeZone),
|
|
146
|
+
priceFormatter: (p) => `${p >= 0 ? '+' : MINUS}${Math.abs(p).toFixed(2)}%`,
|
|
147
|
+
},
|
|
148
|
+
crosshair: { mode: CrosshairMode.Normal },
|
|
149
|
+
handleScale: false,
|
|
150
|
+
handleScroll: false,
|
|
151
|
+
});
|
|
152
|
+
chart.current = c;
|
|
153
|
+
c.subscribeCrosshairMove((p) => {
|
|
154
|
+
if (!p.time)
|
|
155
|
+
return setHover(null);
|
|
156
|
+
const at = {};
|
|
157
|
+
for (const [k, s] of lines.current) {
|
|
158
|
+
const d = p.seriesData.get(s);
|
|
159
|
+
if (d?.value !== undefined)
|
|
160
|
+
at[k] = d.value;
|
|
161
|
+
}
|
|
162
|
+
for (const [k, s] of extLines.current) {
|
|
163
|
+
const d = p.seriesData.get(s);
|
|
164
|
+
if (d?.value !== undefined)
|
|
165
|
+
at[k] = d.value;
|
|
166
|
+
}
|
|
167
|
+
setHover(at);
|
|
168
|
+
});
|
|
169
|
+
return () => {
|
|
170
|
+
chart.current = null;
|
|
171
|
+
lines.current.clear();
|
|
172
|
+
extLines.current.clear();
|
|
173
|
+
c.remove();
|
|
174
|
+
};
|
|
175
|
+
}, [ready, timeZone]);
|
|
176
|
+
// Each symbol's line, redrawn as its bars change.
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const c = chart.current;
|
|
179
|
+
if (!c)
|
|
180
|
+
return;
|
|
181
|
+
const css = getComputedStyle(document.documentElement);
|
|
182
|
+
shown.forEach((s, i) => {
|
|
183
|
+
let line = lines.current.get(s.key);
|
|
184
|
+
if (!line) {
|
|
185
|
+
line = c.addSeries(LineSeries, {
|
|
186
|
+
color: css.getPropertyValue(LINE_TOKENS[i % LINE_TOKENS.length]).trim(),
|
|
187
|
+
lineWidth: 2,
|
|
188
|
+
priceLineVisible: false,
|
|
189
|
+
lastValueVisible: true,
|
|
190
|
+
});
|
|
191
|
+
if (i === 0)
|
|
192
|
+
line.createPriceLine({
|
|
193
|
+
price: 0,
|
|
194
|
+
color: css.getPropertyValue('--muted-foreground').trim(),
|
|
195
|
+
lineWidth: 1,
|
|
196
|
+
lineStyle: 2,
|
|
197
|
+
axisLabelVisible: false,
|
|
198
|
+
});
|
|
199
|
+
lines.current.set(s.key, line);
|
|
200
|
+
}
|
|
201
|
+
const base = s.prevClose;
|
|
202
|
+
line.setData(base ? s.bars.map((b) => ({ time: b.t, value: (b.c / base - 1) * 100 })) : []);
|
|
203
|
+
// After the session, dashed: its last minute, then the trades since the close.
|
|
204
|
+
const ext = extendedOf(s);
|
|
205
|
+
const last = s.bars.at(-1);
|
|
206
|
+
let dashed = extLines.current.get(s.key);
|
|
207
|
+
if (ext && base && last) {
|
|
208
|
+
if (!dashed) {
|
|
209
|
+
dashed = c.addSeries(LineSeries, {
|
|
210
|
+
color: css.getPropertyValue(LINE_TOKENS[i % LINE_TOKENS.length]).trim(),
|
|
211
|
+
lineWidth: 2,
|
|
212
|
+
lineStyle: 2,
|
|
213
|
+
priceLineVisible: false,
|
|
214
|
+
lastValueVisible: true,
|
|
215
|
+
});
|
|
216
|
+
extLines.current.set(s.key, dashed);
|
|
217
|
+
}
|
|
218
|
+
dashed.setData([{ t: last.t, c: last.c }, ...ext.bars.filter((b) => b.t > last.t)].map((b) => ({
|
|
219
|
+
time: b.t,
|
|
220
|
+
value: (b.c / base - 1) * 100,
|
|
221
|
+
})));
|
|
222
|
+
line.applyOptions({ lastValueVisible: false });
|
|
223
|
+
}
|
|
224
|
+
else if (dashed) {
|
|
225
|
+
dashed.setData([]);
|
|
226
|
+
line.applyOptions({ lastValueVisible: true });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
c.timeScale().fitContent();
|
|
230
|
+
});
|
|
231
|
+
if (m.status === 'off' || (m.status === 'error' && !ready))
|
|
232
|
+
return _jsx(Unavailable, { status: m.status, error: m.error, onRetry: m.retry });
|
|
233
|
+
if (!ready && symbols.length && m.missing.length === symbols.length)
|
|
234
|
+
return (_jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No bars for this session yet." }));
|
|
235
|
+
if (!ready)
|
|
236
|
+
return _jsx(Skeleton, { "aria-busy": true, className: "h-72 w-full sm:h-80" });
|
|
237
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsx("div", { className: "flex flex-nowrap justify-between gap-x-1 overflow-x-auto overflow-y-hidden [scrollbar-width:none] sm:justify-start sm:gap-x-3", children: shown.map((s, i) => {
|
|
238
|
+
const ch = hover?.[s.key] ?? changeOf(s);
|
|
239
|
+
const last = s.bars.at(-1)?.c;
|
|
240
|
+
const ext = extendedOf(s);
|
|
241
|
+
return (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 min-w-0 shrink-0 justify-start gap-1 px-0 max-sm:text-xs sm:gap-1.5 sm:px-1.5", disabled: !open, onClick: () => open?.(s.key), title: `Open ${s.label}'s chart`, children: [_jsx("span", { "aria-hidden": true, className: "h-0.5 w-1.5 shrink-0 rounded-full sm:w-2.5", style: { background: `var(${LINE_TOKENS[i % LINE_TOKENS.length]})` } }), _jsx("span", { className: "font-semibold", children: s.label }), _jsx("span", { className: "hidden text-muted-foreground 2xl:inline", children: s.via ? `via ${s.via}` : s.name }), ch !== null && (_jsxs("span", { className: cn('num', ch >= 0 ? 'text-gain' : 'text-loss'), children: [fmtPct(ch).slice(0, -2), _jsxs("span", { className: "max-sm:hidden", children: [" ", ch >= 0 ? '▲' : '▼'] })] })), last !== undefined && !hover && (_jsx("span", { className: "num hidden text-muted-foreground lg:inline", children: fmtValue(s, last) })), ext && !hover && (_jsxs("span", { className: "num hidden text-muted-foreground lg:inline", children: ["\u00B7 ", ext.label, ' ', _jsx("span", { className: ext.change >= 0 ? 'text-gain' : 'text-loss', children: fmtPct(ext.change).slice(0, -2) }), ext.via ? ` via ${ext.via}` : ''] })), s.delayed && _jsx(DelayedChip, {})] }, s.key));
|
|
242
|
+
}) }), _jsx("div", { ref: host, className: "h-72 w-full sm:h-80" }), shown.some((s) => s.extended) && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Dashed: trades since the close (pre-market, after hours, overnight); an index reads through its ETF." })), shown.some((s) => s.error) && (_jsx("p", { className: "text-xs text-muted-foreground", children: shown
|
|
243
|
+
.filter((s) => s.error)
|
|
244
|
+
.map((s) => `${s.label}: ${s.error}`)
|
|
245
|
+
.join(' · ') }))] }));
|
|
246
|
+
}
|
|
247
|
+
/** A small status chip beside a symbol. */
|
|
248
|
+
const CHIP = 'h-4 px-1.5 text-[10px]';
|
|
249
|
+
/** "≈ SPY": the row is priced from that ETF. Purple, so it never reads as a warning. */
|
|
250
|
+
const ProxyChip = ({ text, title }) => (_jsx(Badge, { variant: "outline", title: title, className: cn(CHIP, 'num shrink-0 border-proxy/40 bg-proxy/10 text-proxy'), children: text }));
|
|
251
|
+
const DelayedChip = () => (_jsx(Badge, { variant: "outline", className: cn(CHIP, 'border-warning/50 bg-warning/10 text-warning'), children: "Delayed" }));
|
|
252
|
+
/** One widget per symbol: change, sparkline over the prior close, value, and its source chip. */
|
|
253
|
+
export function MarketTiles({ symbols }) {
|
|
254
|
+
const liveKeys = useMemo(() => symbols.filter((k) => !isDailyMarketKey(k)), [symbols]);
|
|
255
|
+
const dailyKeys = useMemo(() => symbols.filter(isDailyMarketKey), [symbols]);
|
|
256
|
+
const m = useMarket(liveKeys);
|
|
257
|
+
const d = useMarketDaily(dailyKeys);
|
|
258
|
+
const open = useSymbolChart();
|
|
259
|
+
if (m.status === 'off')
|
|
260
|
+
return _jsx(Unavailable, { status: m.status, error: m.error });
|
|
261
|
+
return (_jsx("div", { className: "grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))]", children: symbols.map((k) => {
|
|
262
|
+
const s = m.series[k] ?? d.series[k];
|
|
263
|
+
if (!s) {
|
|
264
|
+
const daily = isDailyMarketKey(k);
|
|
265
|
+
const error = daily
|
|
266
|
+
? d.error
|
|
267
|
+
: m.status === 'error'
|
|
268
|
+
? (m.error ?? 'not reachable')
|
|
269
|
+
: null;
|
|
270
|
+
if (error)
|
|
271
|
+
return (_jsx(TileState, { label: k, tone: "error", text: `Not reachable · ${error}`, children: _jsx(IconButton, { icon: RotateCw, label: "Retry", variant: "outline", size: "icon-xs", onClick: daily ? d.retry : m.retry }) }, k));
|
|
272
|
+
const none = daily ? !d.loading : m.missing.includes(k);
|
|
273
|
+
if (none)
|
|
274
|
+
return (_jsx(TileState, { label: k, text: m.status === 'closed' ? 'No data · market closed' : 'No data for today' }, k));
|
|
275
|
+
return _jsx(TileState, { label: k, text: "Loading", loading: true }, k);
|
|
276
|
+
}
|
|
277
|
+
return (_jsx(Widget, { s: s, onOpen: open ? () => open(k) : undefined, live: m.status === 'live' }, k));
|
|
278
|
+
}) }));
|
|
279
|
+
}
|
|
280
|
+
/** A tile without a series yet: loading, no data, or failed with a reason and Retry. */
|
|
281
|
+
function TileState({ label, text, tone, loading, children, }) {
|
|
282
|
+
return (_jsxs("div", { "aria-busy": loading || undefined, className: "flex h-[7.5rem] min-w-0 flex-col gap-1 rounded-lg border bg-card p-2.5", children: [_jsx("span", { className: "truncate font-semibold", children: label }), loading ? (_jsx(Skeleton, { className: "h-14 w-full" })) : (_jsxs("span", { className: "flex h-14 items-center gap-1.5 text-xs text-muted-foreground", children: [tone === 'error' ? (_jsx(CircleAlert, { className: "size-3.5 shrink-0 text-loss" })) : (_jsx(CircleSlash, { className: "size-3.5 shrink-0" })), _jsx("span", { className: "line-clamp-3", children: text })] })), _jsxs("span", { className: "flex items-center justify-between gap-2 text-[10px] text-muted-foreground", children: [loading ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(LoaderCircle, { className: "size-3 animate-spin" }), text] })) : (_jsx("span", {})), children] })] }));
|
|
283
|
+
}
|
|
284
|
+
function Widget({ s, onOpen, live }) {
|
|
285
|
+
const last = s.bars.at(-1)?.c;
|
|
286
|
+
const ch = s.daily?.points && last !== undefined && s.prevClose !== null
|
|
287
|
+
? last - s.prevClose
|
|
288
|
+
: changeOf(s);
|
|
289
|
+
const up = (ch ?? 0) >= 0;
|
|
290
|
+
const fmtCh = (c) => (s.daily?.points ? fmtPts(s, c) : fmtPct(c));
|
|
291
|
+
const ext = useMemo(() => extendedOf(s), [s]);
|
|
292
|
+
// The session's bars, then (dashed) the trades since its close, joined at its last minute.
|
|
293
|
+
const data = useMemo(() => {
|
|
294
|
+
const rows = s.bars.map((b) => ({ t: b.t, c: b.c }));
|
|
295
|
+
const last = rows.at(-1);
|
|
296
|
+
if (ext && last) {
|
|
297
|
+
last.e = last.c;
|
|
298
|
+
for (const b of ext.bars)
|
|
299
|
+
if (b.t > last.t)
|
|
300
|
+
rows.push({ t: b.t, e: b.c });
|
|
301
|
+
}
|
|
302
|
+
return rows;
|
|
303
|
+
}, [s.bars, ext]);
|
|
304
|
+
const values = data.flatMap((d) => [d.c, d.e]).filter((v) => v !== undefined);
|
|
305
|
+
const lo = Math.min(...values, s.prevClose ?? Number.POSITIVE_INFINITY);
|
|
306
|
+
const hi = Math.max(...values, s.prevClose ?? Number.NEGATIVE_INFINITY);
|
|
307
|
+
const pad = (hi - lo) * 0.12 || 1;
|
|
308
|
+
const tone = up ? 'var(--gain)' : 'var(--loss)';
|
|
309
|
+
return (_jsxs("button", { type: "button", onClick: onOpen, disabled: !onOpen, "aria-label": `${s.label} ${ch === null ? '' : fmtCh(ch)}, open chart`, className: "flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-2.5 text-left transition-colors hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-default", children: [_jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-semibold", children: s.label }), ch !== null && (_jsx("span", { className: cn('num shrink-0 text-xs whitespace-nowrap', up ? 'text-gain' : 'text-loss'), children: fmtCh(ch) }))] }), ext ? (_jsxs("span", { className: "num truncate text-xs text-muted-foreground", children: [ext.label, ' ', _jsx("span", { className: ext.change >= 0 ? 'text-gain' : 'text-loss', children: fmtPct(ext.change).slice(0, -2) })] })) : (_jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: s.name })), data.length > 1 ? (_jsx(ChartContainer, { config: { c: { label: s.label, color: tone } }, className: "aspect-auto h-14 w-full", children: _jsxs(AreaChart, { data: data, margin: { top: 2, bottom: 2, left: 0, right: 0 }, children: [_jsx(YAxis, { hide: true, domain: [lo - pad, hi + pad] }), s.prevClose !== null && !s.daily && (_jsx(ReferenceLine, { y: s.prevClose, stroke: "var(--muted-foreground)", strokeDasharray: "3 3" })), _jsx(Area, { dataKey: "c", type: "linear", stroke: tone, strokeWidth: 1.5, fill: tone, fillOpacity: 0.1, isAnimationActive: false, dot: false }), ext && (_jsx(Area, { dataKey: "e", type: "linear", stroke: ext.change >= 0 ? 'var(--gain)' : 'var(--loss)', strokeWidth: 1.5, strokeDasharray: "3 2", fill: "none", isAnimationActive: false, dot: false }))] }) })) : (_jsx("span", { className: "grid h-14 place-items-center text-xs text-muted-foreground", children: s.error ?? 'No bars yet' })), _jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "num text-sm", children: last !== undefined ? fmtValue(s, last) : '—' }), s.daily ? (_jsxs("span", { className: "text-[10px] text-muted-foreground", children: ["Daily \u00B7 ", day(s.daily.asOf)] })) : s.delayed ? (_jsx(DelayedChip, {})) : s.via ? (_jsxs("span", { className: "text-[10px] text-muted-foreground", children: ["via ", s.via] })) : live ? (_jsx("span", { className: "text-[10px] text-muted-foreground", children: "Live" })) : null] })] }));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Up or down today: one row per symbol with its price now and its change today,
|
|
313
|
+
* in the session or since it closed (the session it traded in named), and how many are up and down.
|
|
314
|
+
*/
|
|
315
|
+
export function MarketToday({ symbols: all }) {
|
|
316
|
+
const symbols = useMemo(() => all.filter((k) => !isDailyMarketKey(k)), [all]);
|
|
317
|
+
const m = useMarket(symbols);
|
|
318
|
+
const open = useSymbolChart();
|
|
319
|
+
const rows = symbols
|
|
320
|
+
.map((k) => m.series[k])
|
|
321
|
+
.filter((s) => !!s)
|
|
322
|
+
.map((s) => {
|
|
323
|
+
const today = todayOf(s);
|
|
324
|
+
// The ETF a row is priced from: an index outside the session, or a symbol the feed has
|
|
325
|
+
// only through one (Gold through GLD).
|
|
326
|
+
const proxy = (today?.label ? s.extended?.via : s.via) ?? null;
|
|
327
|
+
return { s, today, proxy };
|
|
328
|
+
});
|
|
329
|
+
if (m.status === 'off' || (m.status === 'error' && !rows.length))
|
|
330
|
+
return _jsx(Unavailable, { status: m.status, error: m.error, onRetry: m.retry });
|
|
331
|
+
if (!rows.length && symbols.length && m.missing.length === symbols.length)
|
|
332
|
+
return _jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No prices yet." });
|
|
333
|
+
if (!rows.length)
|
|
334
|
+
return _jsx(Skeleton, { "aria-busy": true, className: "h-40 w-full" });
|
|
335
|
+
const up = rows.filter((r) => (r.today?.change ?? 0) > 0).length;
|
|
336
|
+
const down = rows.filter((r) => (r.today?.change ?? 0) < 0).length;
|
|
337
|
+
const session = rows.find((r) => r.today?.label)?.today?.label ?? (m.status === 'live' ? 'Live' : null);
|
|
338
|
+
const proxied = rows.filter((r) => r.proxy);
|
|
339
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("p", { className: "num flex items-center gap-2 text-xs text-muted-foreground", children: [_jsxs("span", { className: "text-gain", children: ["\u25B2 ", up, " up"] }), _jsxs("span", { className: "text-loss", children: ["\u25BC ", down, " down"] }), session && _jsx("span", { className: "ml-auto font-sans", children: session })] }), _jsx("div", { className: "flex flex-col", children: rows.map(({ s, today, proxy }) => {
|
|
340
|
+
const ch = today?.change ?? null;
|
|
341
|
+
const tone = ch === null ? 'text-muted-foreground' : ch >= 0 ? 'text-gain' : 'text-loss';
|
|
342
|
+
return (_jsxs(Button, { variant: "ghost", className: "h-auto min-w-0 justify-between gap-3 px-2 py-1.5 font-normal", disabled: !open, onClick: () => open?.(s.key), title: `Open ${s.label}'s chart`, children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "truncate font-semibold", children: s.label }), proxy && (_jsx(ProxyChip, { text: `≈ ${proxy}`, title: `${s.label} is priced from ${proxy}'s move${today?.label ? ` ${today.label.toLowerCase()}` : ''}.` })), s.delayed && !today?.label && _jsx(DelayedChip, {})] }), _jsxs("span", { className: "flex shrink-0 items-center gap-3", children: [_jsx("span", { className: "num text-xs text-muted-foreground", children: today ? fmtValue(s, today.value) : '—' }), _jsx("span", { className: cn('num w-16 text-right text-sm font-semibold', tone), children: ch === null ? '—' : fmtPct(ch) })] })] }, s.key));
|
|
343
|
+
}) }), proxied.length > 0 && (_jsxs("p", { className: "flex items-start gap-2 border-t px-2 pt-2 text-xs text-muted-foreground", children: [_jsx(ProxyChip, { text: "\u2248" }), _jsx("span", { children: proxied.length === 1
|
|
344
|
+
? `Proxy: ${proxied[0]?.s.label} moves with ${proxied[0]?.proxy}.`
|
|
345
|
+
: "Proxy: these don't trade now, so each moves with its ETF." })] }))] }));
|
|
346
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface OptionChainRow<Q> {
|
|
2
|
+
strike: number;
|
|
3
|
+
call?: Q;
|
|
4
|
+
put?: Q;
|
|
5
|
+
}
|
|
6
|
+
export interface OptionChainField<Q> {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
value: (q: Q) => number | null;
|
|
10
|
+
format: (v: number | null) => string;
|
|
11
|
+
}
|
|
12
|
+
export declare function OptionChainTable<Q>({ rows, fields, spot, format, }: {
|
|
13
|
+
rows: OptionChainRow<Q>[];
|
|
14
|
+
/** Read outward from the strike: the first field sits next to it on both sides. */
|
|
15
|
+
fields: OptionChainField<Q>[];
|
|
16
|
+
spot: number | null;
|
|
17
|
+
/** How a strike and the price print. */
|
|
18
|
+
format: (v: number) => string;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// An option chain as calls | strike | puts, one row per strike, the strike pinned in the centre
|
|
3
|
+
// (an app's live chain; in the shell since Edge drift step 2, 2026-10-14). Calls read leftward
|
|
4
|
+
// and puts rightward from the strike, a row marks the price between strikes, and in-the-money
|
|
5
|
+
// cells are shaded. The app passes its rows and how each field reads and prints.
|
|
6
|
+
import { createColumnHelper, tableFeatures, useTable } from '@tanstack/react-table';
|
|
7
|
+
import { Fragment, useEffect, useMemo, useRef } from 'react';
|
|
8
|
+
import { cn } from '../lib/utils.js';
|
|
9
|
+
import { Badge } from '../ui/badge.js';
|
|
10
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table.js';
|
|
11
|
+
const features = tableFeatures({});
|
|
12
|
+
export function OptionChainTable({ rows, fields, spot, format, }) {
|
|
13
|
+
const columns = useMemo(() => {
|
|
14
|
+
const h = createColumnHelper();
|
|
15
|
+
const side = (type, list) => h.columns(list.map((f) => h.accessor((r) => {
|
|
16
|
+
const q = r[type];
|
|
17
|
+
return q === undefined ? null : f.value(q);
|
|
18
|
+
}, { id: `${type}-${f.key}`, header: f.label, cell: (c) => f.format(c.getValue()) })));
|
|
19
|
+
return h.columns([
|
|
20
|
+
h.group({ id: 'calls', header: 'Calls', columns: side('call', [...fields].reverse()) }),
|
|
21
|
+
h.accessor('strike', { header: 'Strike', cell: (c) => format(c.getValue()) }),
|
|
22
|
+
h.group({ id: 'puts', header: 'Puts', columns: side('put', fields) }),
|
|
23
|
+
]);
|
|
24
|
+
}, [fields, format]);
|
|
25
|
+
const table = useTable({ features, columns, data: rows });
|
|
26
|
+
// Open centred on the money and on the strike column; re-centre only when the strike set changes.
|
|
27
|
+
const box = useRef(null);
|
|
28
|
+
const strikeKey = rows.length ? `${rows[0]?.strike}-${rows.length}` : '';
|
|
29
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the strike set, not every tick
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const el = box.current;
|
|
32
|
+
const row = el?.querySelector('[data-spot]');
|
|
33
|
+
if (el && row)
|
|
34
|
+
el.scrollTop = row.offsetTop - el.clientHeight / 2;
|
|
35
|
+
const strike = el?.querySelector('[data-strike]');
|
|
36
|
+
if (el && strike)
|
|
37
|
+
el.scrollLeft = strike.offsetLeft + strike.offsetWidth / 2 - el.clientWidth / 2;
|
|
38
|
+
}, [strikeKey, spot === null]);
|
|
39
|
+
return (_jsx("div", { ref: box, className: "max-h-[70vh] overflow-auto rounded-lg border bg-card [&>[data-slot=table-container]]:overflow-visible", children: _jsxs(Table, { className: "num text-xs", children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-muted", children: table.getHeaderGroups().map((hg) => (_jsx(TableRow, { className: "hover:bg-transparent", children: hg.headers.map((hd) => (_jsx(TableHead, { colSpan: hd.colSpan, className: cn('h-(--row-h) px-(--cell-x) text-right font-sans text-xs font-medium text-muted-foreground', hd.column.id === 'strike' && 'text-center',
|
|
40
|
+
// Group labels sit next to the strike, so both read when the table opens centred.
|
|
41
|
+
hd.subHeaders.length > 0 && 'text-foreground', hd.column.id === 'puts' && 'text-left'), children: hd.isPlaceholder ? null : _jsx(table.FlexRender, { header: hd }) }, hd.id))) }, hg.id))) }), _jsx(TableBody, { children: table.getRowModel().rows.map((row, i, all) => {
|
|
42
|
+
const k = row.original.strike;
|
|
43
|
+
const prev = all[i - 1]?.original.strike;
|
|
44
|
+
// The price sits between the last strike below it and the first at or above it.
|
|
45
|
+
const marker = spot !== null && k >= spot && (prev === undefined ? k > spot : prev < spot) ? (_jsxs(TableRow, { "data-spot": true, "aria-label": `Price ${format(spot)}`, className: "bg-primary/10 hover:bg-primary/10", children: [_jsx(TableCell, { colSpan: fields.length, className: "h-6 p-0" }), _jsx(TableCell, { className: "h-6 px-(--cell-x) py-0 text-center", children: _jsx(Badge, { className: "num h-5 px-1.5", children: format(spot) }) }), _jsx(TableCell, { colSpan: fields.length, className: "h-6 p-0" })] }, "spot")) : null;
|
|
46
|
+
return (_jsxs(Fragment, { children: [marker, _jsx(TableRow, { children: row.getAllCells().map((cell) => {
|
|
47
|
+
const id = cell.column.id;
|
|
48
|
+
const itm = spot !== null &&
|
|
49
|
+
((id.startsWith('call') && k < spot) || (id.startsWith('put') && k > spot));
|
|
50
|
+
return (_jsx(TableCell, { "data-strike": id === 'strike' || undefined, className: cn('h-(--row-h) px-(--cell-x) py-0 text-right', id === 'strike' && 'bg-muted/60 text-center font-medium', itm && 'bg-muted/40', cell.getValue() === null && 'text-ink-subtle'), children: _jsx(table.FlexRender, { cell: cell }) }, cell.id));
|
|
51
|
+
}) })] }, row.id));
|
|
52
|
+
}) })] }) }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's price chart (moved from an app's Chart tab): price in the chosen
|
|
3
|
+
* type, volume when the bars have it, then a pane per oscillator, all on one time axis; crosshair
|
|
4
|
+
* legend, drag to pan, wheel or pinch to zoom, and an optional timeline navigator under it.
|
|
5
|
+
* lightweight-charts is the one place the shell draws with something other than Recharts: it has the
|
|
6
|
+
* financial time axis and panning (approved in the market-live design).
|
|
7
|
+
*/
|
|
8
|
+
import { type IChartApi, type Time } from 'lightweight-charts';
|
|
9
|
+
import { type ChartIndicator, type IndicatorValues } from './indicators.js';
|
|
10
|
+
/** One bar: `time` is a `YYYY-MM-DD` day or Unix seconds. */
|
|
11
|
+
export interface ChartBar {
|
|
12
|
+
time: string | number;
|
|
13
|
+
open: number;
|
|
14
|
+
high: number;
|
|
15
|
+
low: number;
|
|
16
|
+
close: number;
|
|
17
|
+
volume?: number;
|
|
18
|
+
}
|
|
19
|
+
/** How the price is drawn: the basics every charting tool offers. */
|
|
20
|
+
export declare const CHART_TYPES: readonly [{
|
|
21
|
+
readonly id: 'candles';
|
|
22
|
+
readonly label: 'Candles';
|
|
23
|
+
}, {
|
|
24
|
+
readonly id: 'ohlc';
|
|
25
|
+
readonly label: 'OHLC bars';
|
|
26
|
+
}, {
|
|
27
|
+
readonly id: 'line';
|
|
28
|
+
readonly label: 'Line';
|
|
29
|
+
}, {
|
|
30
|
+
readonly id: 'area';
|
|
31
|
+
readonly label: 'Area';
|
|
32
|
+
}, {
|
|
33
|
+
readonly id: 'heikin';
|
|
34
|
+
readonly label: 'Heikin-Ashi';
|
|
35
|
+
}];
|
|
36
|
+
export type ChartType = (typeof CHART_TYPES)[number]['id'];
|
|
37
|
+
/** Heikin-Ashi candles: each bar averages itself with the one before, which smooths the trend. */
|
|
38
|
+
export declare function heikinAshi(bars: ChartBar[]): ChartBar[];
|
|
39
|
+
export declare const PRICE_PANE = 288;
|
|
40
|
+
export declare const OSC_PANE = 112;
|
|
41
|
+
export declare const AXIS = 28;
|
|
42
|
+
export declare const price: (n: number) => string;
|
|
43
|
+
/** A colour at half strength (grid lines fainter than borders). */
|
|
44
|
+
export declare const faint: (color: string) => string;
|
|
45
|
+
/** Theme tokens as concrete colours: the canvas cannot read `var(--x)`. */
|
|
46
|
+
declare function readTheme(): {
|
|
47
|
+
text: string;
|
|
48
|
+
border: string;
|
|
49
|
+
grid: string;
|
|
50
|
+
up: string;
|
|
51
|
+
down: string;
|
|
52
|
+
primary: string;
|
|
53
|
+
css: (name: string) => string;
|
|
54
|
+
};
|
|
55
|
+
export type Theme = ReturnType<typeof readTheme>;
|
|
56
|
+
/** Colours follow the light/dark switch, which sets the `dark` class on the root. */
|
|
57
|
+
export declare function useTheme(): {
|
|
58
|
+
text: string;
|
|
59
|
+
border: string;
|
|
60
|
+
grid: string;
|
|
61
|
+
up: string;
|
|
62
|
+
down: string;
|
|
63
|
+
primary: string;
|
|
64
|
+
css: (name: string) => string;
|
|
65
|
+
};
|
|
66
|
+
/** A hex token with alpha (the shell's colour tokens are hex). */
|
|
67
|
+
export declare const alpha: (c: string, a: number) => string;
|
|
68
|
+
/** Time labels in the person's zone (the axis would otherwise read UTC). */
|
|
69
|
+
export declare function timeText(t: Time, zone: string, withDay: boolean): string;
|
|
70
|
+
/** A bar's time as Unix seconds (days read as UTC midnight). */
|
|
71
|
+
export declare const secOf: (t: string | number | Time) => number;
|
|
72
|
+
/** A span of time in Unix seconds; either end may be infinite (from the first bar, to the last). */
|
|
73
|
+
export interface TimeSpan {
|
|
74
|
+
from: number;
|
|
75
|
+
to: number;
|
|
76
|
+
}
|
|
77
|
+
/** The bars of `secs` inside `span`, as an index range (at least one bar). */
|
|
78
|
+
export declare function indexSpan(secs: number[], span: TimeSpan): [number, number];
|
|
79
|
+
export interface PriceChartProps {
|
|
80
|
+
bars: ChartBar[];
|
|
81
|
+
/** Bars shown at first, counted from the latest; left out: all of them. */
|
|
82
|
+
visible?: number;
|
|
83
|
+
type: ChartType;
|
|
84
|
+
/** Overlays drawn on the price and oscillators in their own panes. */
|
|
85
|
+
chosen: ChartIndicator[];
|
|
86
|
+
values: IndicatorValues;
|
|
87
|
+
/** The person's time zone, for intraday bars (Unix seconds). */
|
|
88
|
+
timeZone?: string;
|
|
89
|
+
/** Wheel and pinch zoom (off where the page itself scrolls under the chart). */
|
|
90
|
+
zoom?: boolean;
|
|
91
|
+
/** The timeline under the chart: drag its window to move, its edges to zoom. */
|
|
92
|
+
navigator?: boolean;
|
|
93
|
+
/** Draws this level as a dashed line (the prior close). */
|
|
94
|
+
baseline?: number | null;
|
|
95
|
+
/**
|
|
96
|
+
* The navigator's bars when they differ from the chart's: the symbol's whole history, coarser,
|
|
97
|
+
* with the shown span cut out of it. Left out: the chart's own bars.
|
|
98
|
+
*/
|
|
99
|
+
timeline?: ChartBar[];
|
|
100
|
+
/** The span to show; a new object moves the chart there, panning and zooming move it after. */
|
|
101
|
+
range?: TimeSpan;
|
|
102
|
+
/** The shown span (first and last bar), and whether the person moved it (drag, wheel, timeline). */
|
|
103
|
+
onRange?: (span: TimeSpan, byPerson: boolean) => void;
|
|
104
|
+
/** The timeline asked for a span the chart's bars do not cover (intraday bars, older days). */
|
|
105
|
+
onOutside?: (span: TimeSpan) => void;
|
|
106
|
+
className?: string;
|
|
107
|
+
}
|
|
108
|
+
/** One chart: price in the chosen type, volume, then a pane per oscillator, all sharing the time axis. */
|
|
109
|
+
export declare function PriceChart({ bars, visible, type, chosen, values, timeZone, zoom, navigator, baseline, timeline, range, onRange, onOutside, className, }: PriceChartProps): import("react").JSX.Element;
|
|
110
|
+
/** Shows the bars inside `span` (at least one), edge to edge. */
|
|
111
|
+
export declare function showSpan(chart: IChartApi, secs: number[], span: TimeSpan): void;
|
|
112
|
+
/**
|
|
113
|
+
* The timeline under the chart (whole history): the symbol's whole
|
|
114
|
+
* history as a small area chart, with the span the chart shows cut out of it. Drag the window to
|
|
115
|
+
* move, drag its edges to zoom, click outside it to jump there. lightweight-charts has no
|
|
116
|
+
* navigator, so this is a second chart kept in step by time.
|
|
117
|
+
*/
|
|
118
|
+
export declare function Navigator({ chart, bars, secs, timeline, theme, timeZone, multiDay, goTo, }: {
|
|
119
|
+
chart: IChartApi;
|
|
120
|
+
bars: ChartBar[];
|
|
121
|
+
secs: number[];
|
|
122
|
+
timeline: ChartBar[];
|
|
123
|
+
theme: Theme;
|
|
124
|
+
timeZone: string;
|
|
125
|
+
multiDay: boolean;
|
|
126
|
+
goTo: (span: TimeSpan) => void;
|
|
127
|
+
}): import("react").JSX.Element;
|
|
128
|
+
export {};
|