@matchstick-trading/primitives 0.0.10
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/AGENTS.md +44 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +201 -0
- package/README.md +636 -0
- package/dist/Chart.d.ts +18 -0
- package/dist/Chart.js +6 -0
- package/dist/ChartParts.d.ts +151 -0
- package/dist/ChartParts.js +389 -0
- package/dist/CsvDrop.d.ts +8 -0
- package/dist/CsvDrop.js +5 -0
- package/dist/CsvDropParts.d.ts +33 -0
- package/dist/CsvDropParts.js +77 -0
- package/dist/Cursor.d.ts +6 -0
- package/dist/Cursor.js +5 -0
- package/dist/CursorParts.d.ts +51 -0
- package/dist/CursorParts.js +40 -0
- package/dist/DepthProfile.d.ts +13 -0
- package/dist/DepthProfile.js +5 -0
- package/dist/DepthProfileParts.d.ts +45 -0
- package/dist/DepthProfileParts.js +96 -0
- package/dist/DiffTable.d.ts +13 -0
- package/dist/DiffTable.js +5 -0
- package/dist/DiffTableParts.d.ts +57 -0
- package/dist/DiffTableParts.js +103 -0
- package/dist/Drawings.d.ts +13 -0
- package/dist/Drawings.js +5 -0
- package/dist/DrawingsParts.d.ts +194 -0
- package/dist/DrawingsParts.js +775 -0
- package/dist/Fullscreen.d.ts +7 -0
- package/dist/Fullscreen.js +5 -0
- package/dist/FullscreenParts.d.ts +54 -0
- package/dist/FullscreenParts.js +250 -0
- package/dist/Ladder.d.ts +13 -0
- package/dist/Ladder.js +5 -0
- package/dist/LadderParts.d.ts +49 -0
- package/dist/LadderParts.js +56 -0
- package/dist/Menu.d.ts +11 -0
- package/dist/Menu.js +5 -0
- package/dist/MenuParts.d.ts +52 -0
- package/dist/MenuParts.js +202 -0
- package/dist/Notice.d.ts +18 -0
- package/dist/Notice.js +9 -0
- package/dist/Panels.d.ts +9 -0
- package/dist/Panels.js +5 -0
- package/dist/PanelsParts.d.ts +62 -0
- package/dist/PanelsParts.js +387 -0
- package/dist/Receipt.d.ts +11 -0
- package/dist/Receipt.js +5 -0
- package/dist/ReceiptParts.d.ts +45 -0
- package/dist/ReceiptParts.js +71 -0
- package/dist/Scrubber.d.ts +9 -0
- package/dist/Scrubber.js +5 -0
- package/dist/ScrubberParts.d.ts +74 -0
- package/dist/ScrubberParts.js +126 -0
- package/dist/Selection.d.ts +14 -0
- package/dist/Selection.js +7 -0
- package/dist/SelectionParts.d.ts +63 -0
- package/dist/SelectionParts.js +208 -0
- package/dist/Studies.d.ts +9 -0
- package/dist/Studies.js +5 -0
- package/dist/StudiesParts.d.ts +45 -0
- package/dist/StudiesParts.js +153 -0
- package/dist/Submenu.d.ts +7 -0
- package/dist/Submenu.js +4 -0
- package/dist/SymbolSearch.d.ts +10 -0
- package/dist/SymbolSearch.js +5 -0
- package/dist/SymbolSearchParts.d.ts +39 -0
- package/dist/SymbolSearchParts.js +86 -0
- package/dist/Tape.d.ts +15 -0
- package/dist/Tape.js +5 -0
- package/dist/TapeParts.d.ts +69 -0
- package/dist/TapeParts.js +139 -0
- package/dist/Toolbar.d.ts +8 -0
- package/dist/Toolbar.js +5 -0
- package/dist/ToolbarParts.d.ts +61 -0
- package/dist/ToolbarParts.js +122 -0
- package/dist/Transport.d.ts +15 -0
- package/dist/Transport.js +5 -0
- package/dist/TransportParts.d.ts +52 -0
- package/dist/TransportParts.js +47 -0
- package/dist/VolumeProfile.d.ts +7 -0
- package/dist/VolumeProfile.js +5 -0
- package/dist/VolumeProfileParts.d.ts +37 -0
- package/dist/VolumeProfileParts.js +57 -0
- package/dist/accessible-name.d.ts +26 -0
- package/dist/accessible-name.js +14 -0
- package/dist/attributes.d.ts +8 -0
- package/dist/attributes.js +9 -0
- package/dist/core/adapter.d.ts +38 -0
- package/dist/core/adapter.js +64 -0
- package/dist/core/attributes.d.ts +36 -0
- package/dist/core/attributes.js +22 -0
- package/dist/core/candles.d.ts +6 -0
- package/dist/core/candles.js +19 -0
- package/dist/core/chart-gesture.d.ts +109 -0
- package/dist/core/chart-gesture.js +161 -0
- package/dist/core/chart-scale.d.ts +141 -0
- package/dist/core/chart-scale.js +302 -0
- package/dist/core/csv-bars.d.ts +54 -0
- package/dist/core/csv-bars.js +325 -0
- package/dist/core/depth-profile.d.ts +43 -0
- package/dist/core/depth-profile.js +66 -0
- package/dist/core/diff-table.d.ts +57 -0
- package/dist/core/diff-table.js +77 -0
- package/dist/core/drawing-fields.d.ts +11 -0
- package/dist/core/drawing-fields.js +15 -0
- package/dist/core/drawings.d.ts +194 -0
- package/dist/core/drawings.js +403 -0
- package/dist/core/fullscreen.d.ts +32 -0
- package/dist/core/fullscreen.js +16 -0
- package/dist/core/index.d.ts +41 -0
- package/dist/core/index.js +23 -0
- package/dist/core/ladder-keys.d.ts +35 -0
- package/dist/core/ladder-keys.js +43 -0
- package/dist/core/ladder.d.ts +65 -0
- package/dist/core/ladder.js +93 -0
- package/dist/core/panels.d.ts +72 -0
- package/dist/core/panels.js +151 -0
- package/dist/core/receipt-rules.d.ts +42 -0
- package/dist/core/receipt-rules.js +20 -0
- package/dist/core/scrubber-gesture.d.ts +66 -0
- package/dist/core/scrubber-gesture.js +69 -0
- package/dist/core/selection.d.ts +24 -0
- package/dist/core/selection.js +33 -0
- package/dist/core/series.d.ts +161 -0
- package/dist/core/series.js +331 -0
- package/dist/core/studies.d.ts +80 -0
- package/dist/core/studies.js +237 -0
- package/dist/core/symbol-search.d.ts +89 -0
- package/dist/core/symbol-search.js +223 -0
- package/dist/core/tape.d.ts +77 -0
- package/dist/core/tape.js +136 -0
- package/dist/core/volume-profile.d.ts +47 -0
- package/dist/core/volume-profile.js +86 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +42 -0
- package/dist/ladder-keys.d.ts +16 -0
- package/dist/ladder-keys.js +86 -0
- package/dist/panels-context.d.ts +47 -0
- package/dist/panels-context.js +3 -0
- package/dist/selection-context.d.ts +41 -0
- package/dist/selection-context.js +4 -0
- package/examples/csv-to-chart-frame.mjs +17 -0
- package/examples/studies.mjs +12 -0
- package/llms.txt +16 -0
- package/package.json +67 -0
- package/src/Chart.tsx +7 -0
- package/src/ChartParts.tsx +529 -0
- package/src/CsvDrop.tsx +6 -0
- package/src/CsvDropParts.tsx +128 -0
- package/src/Cursor.tsx +6 -0
- package/src/CursorParts.tsx +77 -0
- package/src/DepthProfile.tsx +6 -0
- package/src/DepthProfileParts.tsx +154 -0
- package/src/DiffTable.tsx +6 -0
- package/src/DiffTableParts.tsx +160 -0
- package/src/Drawings.tsx +6 -0
- package/src/DrawingsParts.tsx +1080 -0
- package/src/Fullscreen.tsx +6 -0
- package/src/FullscreenParts.tsx +318 -0
- package/src/Ladder.tsx +6 -0
- package/src/LadderParts.tsx +114 -0
- package/src/Menu.tsx +6 -0
- package/src/MenuParts.tsx +364 -0
- package/src/Notice.tsx +27 -0
- package/src/Panels.tsx +6 -0
- package/src/PanelsParts.tsx +454 -0
- package/src/Receipt.tsx +6 -0
- package/src/ReceiptParts.tsx +118 -0
- package/src/Scrubber.tsx +6 -0
- package/src/ScrubberParts.tsx +207 -0
- package/src/Selection.tsx +12 -0
- package/src/SelectionParts.tsx +251 -0
- package/src/Studies.tsx +6 -0
- package/src/StudiesParts.tsx +226 -0
- package/src/Submenu.tsx +5 -0
- package/src/SymbolSearch.tsx +6 -0
- package/src/SymbolSearchParts.tsx +166 -0
- package/src/Tape.tsx +6 -0
- package/src/TapeParts.tsx +208 -0
- package/src/Toolbar.tsx +6 -0
- package/src/ToolbarParts.tsx +179 -0
- package/src/Transport.tsx +6 -0
- package/src/TransportParts.tsx +128 -0
- package/src/VolumeProfile.tsx +6 -0
- package/src/VolumeProfileParts.tsx +106 -0
- package/src/accessible-name.tsx +34 -0
- package/src/attributes.ts +12 -0
- package/src/core/adapter.ts +91 -0
- package/src/core/attributes.ts +51 -0
- package/src/core/candles.ts +22 -0
- package/src/core/chart-gesture.ts +228 -0
- package/src/core/chart-scale.ts +368 -0
- package/src/core/csv-bars.ts +328 -0
- package/src/core/depth-profile.ts +84 -0
- package/src/core/diff-table.ts +105 -0
- package/src/core/drawing-fields.ts +17 -0
- package/src/core/drawings.ts +478 -0
- package/src/core/fullscreen.ts +32 -0
- package/src/core/index.ts +42 -0
- package/src/core/ladder-keys.ts +61 -0
- package/src/core/ladder.ts +156 -0
- package/src/core/panels.ts +177 -0
- package/src/core/receipt-rules.ts +45 -0
- package/src/core/scrubber-gesture.ts +102 -0
- package/src/core/selection.ts +41 -0
- package/src/core/series.ts +374 -0
- package/src/core/studies.ts +260 -0
- package/src/core/symbol-search.ts +256 -0
- package/src/core/tape.ts +159 -0
- package/src/core/volume-profile.ts +116 -0
- package/src/index.ts +83 -0
- package/src/ladder-keys.ts +83 -0
- package/src/panels-context.ts +46 -0
- package/src/selection-context.ts +38 -0
- package/src/tokens.css +235 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type KeyboardEvent } from 'react';
|
|
2
|
+
import { isRowShown, LADDER_ROW_ATTRIBUTE, nextLadderRow, resolveLadderKey } from './core/ladder-keys.js';
|
|
3
|
+
|
|
4
|
+
// The pure contract lives in the core (no React); the hook below is the React layer over it.
|
|
5
|
+
export { LADDER_ROW_ATTRIBUTE, nextLadderRow, resolveLadderKey, type LadderKeyAction, type LadderKeyInput } from './core/ladder-keys.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Roving focus for the ladder rows. Spread `containerProps` on the element wrapping every row, give each row `data-ladder-row={id}` and
|
|
9
|
+
* `tabIndex={id === tabStop(ids, bestBidId) ? 0 : -1}`, where `id` is the row's price level (identity is the price, not the index).
|
|
10
|
+
*/
|
|
11
|
+
export function useLadderKeys() {
|
|
12
|
+
const container = useRef<HTMLElement | null>(null);
|
|
13
|
+
const [active, setActive] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const onKeyDown = useCallback((event: KeyboardEvent<HTMLElement>) => {
|
|
16
|
+
if (!(event.target instanceof HTMLElement)) return;
|
|
17
|
+
const action = resolveLadderKey(event);
|
|
18
|
+
// A read-only row is a plain div with no activation of its own: Space on it does nothing, and must not scroll the page
|
|
19
|
+
// or reach the transport's play/pause. (A seeking row is a button, which handles Space itself.)
|
|
20
|
+
const row = event.target.closest(`[${LADDER_ROW_ATTRIBUTE}]`);
|
|
21
|
+
if (event.key === ' ' && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey && row && row.tagName !== 'BUTTON') {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
event.stopPropagation();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!action) {
|
|
27
|
+
// A modified Home or End has no ladder meaning, but must not reach the global transport jump either.
|
|
28
|
+
if ((event.key === 'Home' || event.key === 'End') && event.target.closest(`[${LADDER_ROW_ATTRIBUTE}]`)) event.stopPropagation();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const host = container.current;
|
|
32
|
+
if (!host) return;
|
|
33
|
+
// Rows inside a closed disclosure are in the DOM but not on screen: they are not part of the walk.
|
|
34
|
+
const rows = Array.from(host.querySelectorAll<HTMLElement>(`[${LADDER_ROW_ATTRIBUTE}]`)).filter(isRowShown);
|
|
35
|
+
const current = event.target.closest<HTMLElement>(`[${LADDER_ROW_ATTRIBUTE}]`);
|
|
36
|
+
if (!current) return;
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
event.stopPropagation(); // Home and End must not reach the global transport shortcuts from here
|
|
39
|
+
const target = nextLadderRow(rows, current, action);
|
|
40
|
+
if (!target) return;
|
|
41
|
+
setActive(target.getAttribute(LADDER_ROW_ATTRIBUTE));
|
|
42
|
+
target.focus();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const onFocus = useCallback((event: React.FocusEvent<HTMLElement>) => {
|
|
46
|
+
const row = event.target.closest<HTMLElement>(`[${LADDER_ROW_ATTRIBUTE}]`);
|
|
47
|
+
if (row) setActive(row.getAttribute(LADDER_ROW_ATTRIBUTE));
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
// The row that holds the tab stop can also leave the screen without a toggle: a replay update can push it past the visible
|
|
51
|
+
// levels into a closed disclosure. After each render, hand the stop back if it is no longer shown (it sets state only then).
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const host = container.current;
|
|
54
|
+
if (!host || active === null) return;
|
|
55
|
+
const stop = Array.from(host.querySelectorAll<HTMLElement>(`[${LADDER_ROW_ATTRIBUTE}]`)).find((r) => r.getAttribute(LADDER_ROW_ATTRIBUTE) === active);
|
|
56
|
+
if (!stop || !isRowShown(stop)) setActive(null);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// A disclosure that closes hides the row that held the tab stop; `toggle` does not bubble, so listen in the capture phase.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const host = container.current;
|
|
62
|
+
if (!host) return;
|
|
63
|
+
const onToggle = () => {
|
|
64
|
+
const stop = host.querySelector<HTMLElement>(`[${LADDER_ROW_ATTRIBUTE}][tabindex="0"]`);
|
|
65
|
+
if (stop && !isRowShown(stop)) setActive(null);
|
|
66
|
+
};
|
|
67
|
+
host.addEventListener('toggle', onToggle, true);
|
|
68
|
+
return () => host.removeEventListener('toggle', onToggle, true);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
containerProps: {
|
|
73
|
+
ref: (node: HTMLElement | null) => {
|
|
74
|
+
container.current = node;
|
|
75
|
+
},
|
|
76
|
+
onKeyDown,
|
|
77
|
+
onFocus,
|
|
78
|
+
},
|
|
79
|
+
/** The id of the one row that is a tab stop: the focused row while it is still on screen, else `fallback` (the best bid, or the best ask when there are no bids). */
|
|
80
|
+
tabStop: (ids: readonly string[], fallback: string | undefined): string | undefined =>
|
|
81
|
+
active !== null && ids.includes(active) ? active : fallback,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import type { CandleBar, PriceSpan } from './core/chart-scale.js';
|
|
3
|
+
import type { MoveDestination, PanelLayout } from './core/panels.js';
|
|
4
|
+
import type { ResolvedSeries, SeriesPoint } from './core/series.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One series a host puts on the chart besides its bars. Give it `points` (a time and a value or a reason for none, see
|
|
8
|
+
* `core/series`); it is placed on the bars by time. `values` (one number per bar, by position) still works for now and is
|
|
9
|
+
* removed in 0.0.7.
|
|
10
|
+
*/
|
|
11
|
+
export interface PanelSeriesData {
|
|
12
|
+
/** The selection id. Unique on the chart. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** What the series is called: button names, the list and the live-region text. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** The series on its own clock. Placed on the bars by time (`align` on `Panels.Root`; exact by default). */
|
|
17
|
+
points?: ReadonlyArray<SeriesPoint>;
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated One value per bar by position, null for none. A length that differs from the bars is shown as a problem
|
|
20
|
+
* in `Panels.List`. Removed in 0.0.7; use `points` (for a study, `pointsFromValues` from the core makes them).
|
|
21
|
+
*/
|
|
22
|
+
values?: ReadonlyArray<number | null>;
|
|
23
|
+
/** A fixed vertical range `[min, max]` for the band the series is in (an oscillator bounded 0 to 100). Ignored in `main`. */
|
|
24
|
+
range?: readonly [number, number];
|
|
25
|
+
/** Horizontal reference lines in the band, for example `[30, 70]`. Ignored in `main`. */
|
|
26
|
+
levels?: readonly number[];
|
|
27
|
+
/** `line` (default), or `histogram`: bars from zero, for a signed series. */
|
|
28
|
+
style?: 'line' | 'histogram';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The context behind Panels, in a module of its own so Chart can read the layout without importing the parts (which
|
|
33
|
+
* import Chart).
|
|
34
|
+
*/
|
|
35
|
+
export interface PanelsContextValue {
|
|
36
|
+
series: readonly PanelSeriesData[];
|
|
37
|
+
layout: PanelLayout;
|
|
38
|
+
move: (seriesId: string, destination: MoveDestination) => void;
|
|
39
|
+
/** Prices of the series sitting in `main`, so the main scale keeps them in view. */
|
|
40
|
+
mainInclude: (visible: { first: number; last: number }, bars: readonly CandleBar[]) => readonly PriceSpan[];
|
|
41
|
+
/** A series on these bars: the value at each bar and the problems with the input. Cached per bars array and series. */
|
|
42
|
+
resolve: (item: PanelSeriesData, bars: readonly CandleBar[]) => ResolvedSeries;
|
|
43
|
+
/** Problems that belong to the set, not one series (an id used twice). */
|
|
44
|
+
problems: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
export const PanelsContext = createContext<PanelsContextValue | null>(null);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createContext, type MutableRefObject } from 'react';
|
|
2
|
+
import type { ChartSelection } from './core/selection.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The contexts behind Selection and Series, in a module of their own so Chart, Drawings and Studies can read them
|
|
6
|
+
* without importing the parts (which import Chart).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** What Drawings.Layer tells the Selection.Layer so a press on a drawing, or while a tool is in use, is not read as a tap on clear chart. */
|
|
10
|
+
export interface SelectionGuard {
|
|
11
|
+
/** True when a press here belongs to a drawing or to a tool in use. */
|
|
12
|
+
owns: (clientX: number, clientY: number, pointerType: string) => boolean;
|
|
13
|
+
/** True while a tool is armed or a drawing is half placed: Escape then cancels that first. */
|
|
14
|
+
busy: () => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SelectionContextValue {
|
|
18
|
+
/** The selection now. */
|
|
19
|
+
selection: ChartSelection;
|
|
20
|
+
/** Change the selection. Setting what is already selected does nothing, so two parts agreeing on a tap report once. */
|
|
21
|
+
select: (next: ChartSelection) => void;
|
|
22
|
+
/** The selection as of the last change, even before the next render. */
|
|
23
|
+
current: () => ChartSelection;
|
|
24
|
+
/** The series a Series.Root mounted, or null. A tap on clear chart selects it. */
|
|
25
|
+
series: { id: string; label: string } | null;
|
|
26
|
+
registerSeries: (series: { id: string; label: string }) => () => void;
|
|
27
|
+
/** A name for the live region; the id is used when none is set. Returns the cleanup. */
|
|
28
|
+
setName: (kind: 'drawing' | 'series' | 'study', id: string, name: string) => () => void;
|
|
29
|
+
guard: MutableRefObject<SelectionGuard | null>;
|
|
30
|
+
}
|
|
31
|
+
export const SelectionContext = createContext<SelectionContextValue | null>(null);
|
|
32
|
+
|
|
33
|
+
export interface SeriesContextValue {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
selected: boolean;
|
|
37
|
+
}
|
|
38
|
+
export const SeriesContext = createContext<SeriesContextValue | null>(null);
|
package/src/tokens.css
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Design tokens for the trader primitives. Import once; override any custom property on a
|
|
3
|
+
* parent element. Parts read these tokens and the data attributes in attributes.ts, never a class name.
|
|
4
|
+
* Every rule is scoped to elements that carry `data-mt` (primitiveAttributes adds it), so importing this file
|
|
5
|
+
* never restyles host markup that happens to use a similar data attribute.
|
|
6
|
+
*
|
|
7
|
+
* Default palette is colour-blind safe: bid is blue, ask is orange (distinguishable under deuteranopia,
|
|
8
|
+
* protanopia and tritanopia), and direction is ALSO carried by a mark (B/A, up/down arrows) so colour is never
|
|
9
|
+
* the only signal (WCAG 1.4.1). Set data-palette="classic" on a parent for green/red.
|
|
10
|
+
*/
|
|
11
|
+
:root {
|
|
12
|
+
/* The smallest touch target: parts and docs raise controls to this on coarse pointers (phones, tablets). Override to taste. */
|
|
13
|
+
--mt-touch-target: 44px;
|
|
14
|
+
/* design-tokens:begin generated from the Matchstick design tokens; do not edit between the markers */
|
|
15
|
+
/* The surface the colour tokens below are tuned for (dark here, white in the light preset). The parts do not paint it; set it on your own background so the two stay together. */
|
|
16
|
+
--mt-surface: #12151c;
|
|
17
|
+
/* Fullscreen.Frame while expanded: the text colour on that background (light here; dark under data-theme='light'). The labels, readout and host text inherit it. */
|
|
18
|
+
--mt-fullscreen-fg: #e8eaef;
|
|
19
|
+
--mt-bid: #4aa3ff;
|
|
20
|
+
--mt-ask: #ff9f43;
|
|
21
|
+
--mt-up: #4aa3ff;
|
|
22
|
+
--mt-down: #ff9f43;
|
|
23
|
+
--mt-flat: #8c93a3;
|
|
24
|
+
--mt-warn: #ffc247;
|
|
25
|
+
--mt-error: #ff6b6b;
|
|
26
|
+
/* Buttons that remove things (Drawings.Remove). A button also says what it does in words, so colour is never the only cue. */
|
|
27
|
+
--mt-destructive: #ff6b6b;
|
|
28
|
+
--mt-synthetic: #b48cff;
|
|
29
|
+
--mt-cursor-a: #4aa3ff;
|
|
30
|
+
--mt-cursor-b: #ff9f43;
|
|
31
|
+
--mt-focus: #ff2a85;
|
|
32
|
+
--mt-font-numeric: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
33
|
+
/* design-tokens:end */
|
|
34
|
+
/* Fullscreen.Frame while expanded: its background (the surface by default; the browser's own full screen would paint black) and, in the in-page fallback, its stacking order. */
|
|
35
|
+
--mt-fullscreen-bg: var(--mt-surface);
|
|
36
|
+
--mt-fullscreen-z: 2147483000;
|
|
37
|
+
/* Marks that carry meaning without colour. Override to change the glyph or text. */
|
|
38
|
+
--mt-mark-bid: 'B';
|
|
39
|
+
--mt-mark-ask: 'A';
|
|
40
|
+
--mt-mark-up: '▲';
|
|
41
|
+
--mt-mark-down: '▼';
|
|
42
|
+
--mt-mark-flat: '–';
|
|
43
|
+
--mt-mark-added: ' +';
|
|
44
|
+
--mt-mark-removed: ' −';
|
|
45
|
+
--mt-mark-modified: ' ±';
|
|
46
|
+
--mt-mark-gap: ' gap';
|
|
47
|
+
--mt-mark-stale: ' stale';
|
|
48
|
+
--mt-mark-truncated: ' truncated';
|
|
49
|
+
--mt-mark-synthetic: ' synthetic';
|
|
50
|
+
/* Drawing state looks. The base token is the passive state; -active is a drawing being drawn or moved; -selected is the picked one.
|
|
51
|
+
-locked applies to a locked drawing and falls back to its state's value. Width is an SVG stroke width, dash is a stroke-dasharray ('none' is solid). */
|
|
52
|
+
--mt-drawing-width: 1.25;
|
|
53
|
+
--mt-drawing-width-active: 2.25;
|
|
54
|
+
--mt-drawing-width-selected: 2.75;
|
|
55
|
+
--mt-drawing-dash: none;
|
|
56
|
+
--mt-drawing-dash-active: 6 4;
|
|
57
|
+
--mt-drawing-dash-selected: none;
|
|
58
|
+
/* The anchor circles of the selected drawing; -dragging is the one under the pointer. */
|
|
59
|
+
--mt-drawing-handle-width: 1.5;
|
|
60
|
+
--mt-drawing-handle-width-dragging: 3;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Light preset: put data-theme='light' on any element (or <html>) over a white-ish surface. Every colour token is rebound
|
|
65
|
+
* so text-like marks reach 4.5:1 and the focus ring and cursor edges reach 3:1 against --mt-surface (table in the README).
|
|
66
|
+
* Keep any data-palette on the same element or inside it; data-palette='classic' has a light variant below.
|
|
67
|
+
*/
|
|
68
|
+
[data-theme='light'] {
|
|
69
|
+
--mt-surface: #ffffff;
|
|
70
|
+
--mt-fullscreen-bg: var(--mt-surface);
|
|
71
|
+
--mt-fullscreen-fg: #14141c;
|
|
72
|
+
--mt-bid: #1f6fd1;
|
|
73
|
+
--mt-ask: #a64f00;
|
|
74
|
+
--mt-up: #1f6fd1;
|
|
75
|
+
--mt-down: #a64f00;
|
|
76
|
+
--mt-flat: #5f5f70;
|
|
77
|
+
--mt-warn: #8a5a00;
|
|
78
|
+
--mt-error: #c0243f;
|
|
79
|
+
--mt-destructive: #c0243f;
|
|
80
|
+
--mt-synthetic: #7a3fd0;
|
|
81
|
+
--mt-cursor-a: #1f6fd1;
|
|
82
|
+
--mt-cursor-b: #a64f00;
|
|
83
|
+
--mt-focus: #a86400;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Custom properties resolve where they are declared, so every token that follows the sides is rebound here. */
|
|
87
|
+
[data-palette='classic'] {
|
|
88
|
+
--mt-bid: #00c87a;
|
|
89
|
+
--mt-ask: #ff4d6a;
|
|
90
|
+
--mt-up: #00c87a;
|
|
91
|
+
--mt-down: #ff4d6a;
|
|
92
|
+
--mt-cursor-a: #00c87a;
|
|
93
|
+
--mt-cursor-b: #ff4d6a;
|
|
94
|
+
}
|
|
95
|
+
[data-theme='light'][data-palette='classic'],
|
|
96
|
+
[data-theme='light'] [data-palette='classic'] {
|
|
97
|
+
--mt-bid: #0a7a4f;
|
|
98
|
+
--mt-ask: #c0243f;
|
|
99
|
+
--mt-up: #0a7a4f;
|
|
100
|
+
--mt-down: #c0243f;
|
|
101
|
+
--mt-cursor-a: #0a7a4f;
|
|
102
|
+
--mt-cursor-b: #c0243f;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* Numeric text: tabular figures keep each digit the same width, but a value that gains a digit (99.99 to 100.00)
|
|
107
|
+
* still grows. Reserve the widest value: set --mt-numeric-width (for example from numericWidth()) on the column.
|
|
108
|
+
*/
|
|
109
|
+
[data-mt-numeric] {
|
|
110
|
+
font-family: var(--mt-font-numeric);
|
|
111
|
+
font-variant-numeric: tabular-nums;
|
|
112
|
+
min-inline-size: var(--mt-numeric-width, 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
[data-mt][data-side='bid'] { color: var(--mt-bid); }
|
|
116
|
+
[data-mt][data-side='ask'] { color: var(--mt-ask); }
|
|
117
|
+
[data-mt][data-side='bid']::before { content: var(--mt-mark-bid); margin-inline-end: 0.35em; font-size: 0.75em; }
|
|
118
|
+
[data-mt][data-side='ask']::before { content: var(--mt-mark-ask); margin-inline-end: 0.35em; font-size: 0.75em; }
|
|
119
|
+
|
|
120
|
+
/* A trade on the tape: the aggressor's side. Blue and orange as the book's sides, with an arrow so it never rests on colour. */
|
|
121
|
+
[data-mt][data-tape-side='buy'] { color: var(--mt-bid); }
|
|
122
|
+
[data-mt][data-tape-side='sell'] { color: var(--mt-ask); }
|
|
123
|
+
[data-mt][data-tape-side='buy']::before { content: var(--mt-mark-up); margin-inline-end: 0.35em; font-size: 0.75em; }
|
|
124
|
+
[data-mt][data-tape-side='sell']::before { content: var(--mt-mark-down); margin-inline-end: 0.35em; font-size: 0.75em; }
|
|
125
|
+
|
|
126
|
+
/* The ladder's size bars: a wash of the side's colour as wide as --mt-bar (0 to 1), growing from the right edge of the cell. */
|
|
127
|
+
[data-mt][data-ladder-bar='bid'] { background: linear-gradient(to left, color-mix(in srgb, var(--mt-bid) 26%, transparent) calc(var(--mt-bar, 0) * 100%), transparent 0); }
|
|
128
|
+
[data-mt][data-ladder-bar='ask'] { background: linear-gradient(to left, color-mix(in srgb, var(--mt-ask) 26%, transparent) calc(var(--mt-bar, 0) * 100%), transparent 0); }
|
|
129
|
+
|
|
130
|
+
[data-mt][data-change='up'] { color: var(--mt-up); }
|
|
131
|
+
[data-mt][data-change='down'] { color: var(--mt-down); }
|
|
132
|
+
[data-mt][data-change='flat'] { color: var(--mt-flat); }
|
|
133
|
+
[data-mt][data-diff='added'] { color: var(--mt-up); }
|
|
134
|
+
[data-mt][data-diff='removed'] { color: var(--mt-down); }
|
|
135
|
+
[data-mt][data-diff='modified'] { color: var(--mt-flat); }
|
|
136
|
+
[data-mt][data-quality='gap'],
|
|
137
|
+
[data-mt][data-quality='stale'],
|
|
138
|
+
[data-mt][data-quality='truncated'] { color: var(--mt-warn); }
|
|
139
|
+
[data-mt][data-verdict='mismatch'] { color: var(--mt-warn); }
|
|
140
|
+
|
|
141
|
+
/* Custom properties inherit, so each primitive first clears the marks it would otherwise take from a parent primitive. */
|
|
142
|
+
[data-mt] {
|
|
143
|
+
--mt-after-change: initial;
|
|
144
|
+
--mt-after-diff: initial;
|
|
145
|
+
--mt-after-quality: initial;
|
|
146
|
+
--mt-after-synthetic: initial;
|
|
147
|
+
}
|
|
148
|
+
[data-mt][data-synthetic] { --mt-after-synthetic: var(--mt-mark-synthetic); }
|
|
149
|
+
|
|
150
|
+
/* Each state sets its own mark; one ::after joins the ones present, so states can coexist without overriding each other. */
|
|
151
|
+
[data-mt][data-change='up'] { --mt-after-change: var(--mt-mark-up); }
|
|
152
|
+
[data-mt][data-change='down'] { --mt-after-change: var(--mt-mark-down); }
|
|
153
|
+
[data-mt][data-change='flat'] { --mt-after-change: var(--mt-mark-flat); }
|
|
154
|
+
[data-mt][data-diff='added'] { --mt-after-diff: var(--mt-mark-added); }
|
|
155
|
+
[data-mt][data-diff='removed'] { --mt-after-diff: var(--mt-mark-removed); }
|
|
156
|
+
[data-mt][data-diff='modified'] { --mt-after-diff: var(--mt-mark-modified); }
|
|
157
|
+
[data-mt][data-quality='gap'] { --mt-after-quality: var(--mt-mark-gap); }
|
|
158
|
+
[data-mt][data-quality='stale'] { --mt-after-quality: var(--mt-mark-stale); }
|
|
159
|
+
[data-mt][data-quality='truncated'] { --mt-after-quality: var(--mt-mark-truncated); }
|
|
160
|
+
[data-mt][data-change]::after,
|
|
161
|
+
[data-mt][data-diff]::after,
|
|
162
|
+
[data-mt][data-quality]:not([data-quality='ok'])::after,
|
|
163
|
+
[data-mt][data-synthetic]::after {
|
|
164
|
+
content: var(--mt-after-change, '') var(--mt-after-diff, '') var(--mt-after-quality, '') var(--mt-after-synthetic, '');
|
|
165
|
+
margin-inline-start: 0.35em;
|
|
166
|
+
font-size: 0.75em;
|
|
167
|
+
}
|
|
168
|
+
/* The marks of a synthetic value follow --mt-synthetic. */
|
|
169
|
+
[data-mt][data-synthetic]::after { color: var(--mt-synthetic); }
|
|
170
|
+
|
|
171
|
+
/* Cursors differ by edge, not only colour: A is marked on the leading edge, B on the trailing edge. */
|
|
172
|
+
[data-mt][data-cursor='a'] { box-shadow: inset 3px 0 0 var(--mt-cursor-a); }
|
|
173
|
+
[data-mt][data-cursor='b'] { box-shadow: inset -3px 0 0 var(--mt-cursor-b); }
|
|
174
|
+
/* Leading and trailing swap with the writing direction. */
|
|
175
|
+
[data-mt][data-cursor='a']:dir(rtl) { box-shadow: inset -3px 0 0 var(--mt-cursor-a); }
|
|
176
|
+
[data-mt][data-cursor='b']:dir(rtl) { box-shadow: inset 3px 0 0 var(--mt-cursor-b); }
|
|
177
|
+
/* Forced colours drop box-shadow, so the edges become borders: A solid, B dashed, so they still differ without colour. */
|
|
178
|
+
@media (forced-colors: active) {
|
|
179
|
+
:where([data-mt][data-cursor='a']) { box-shadow: none; border-inline-start: 3px solid CanvasText; }
|
|
180
|
+
:where([data-mt][data-cursor='b']) { box-shadow: none; border-inline-end: 3px dashed CanvasText; }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
:where([data-mt-focusable], tr[data-diff-row], [data-mt][data-verdict] code[tabindex]):focus-visible { outline: 2px solid var(--mt-focus); outline-offset: -2px; }
|
|
184
|
+
|
|
185
|
+
/* Drawing states, from the tokens above. :where() keeps the specificity at zero, so any rule of yours wins without a fight. */
|
|
186
|
+
:where([data-mt][data-part='drawing'] [data-part='drawing-line'], [data-mt][data-part='drawing'] [data-part='drawing-box'], [data-mt][data-part='drawing'] [data-part='drawing-level'] line, [data-mt][data-part='drawing'] [data-part='drawing-handle']) { stroke: currentColor; fill: none; }
|
|
187
|
+
:where([data-part='crosshair'] [data-part='crosshair-price']) { fill: currentColor; font: 11px var(--mt-font-numeric); }
|
|
188
|
+
:where([data-mt][data-part='drawing'] [data-part='drawing-text'], [data-mt][data-part='drawing'] [data-part='drawing-level'] text, [data-mt][data-part='drawing'] [data-part='drawing-range']) { fill: currentColor; }
|
|
189
|
+
:where([data-mt][data-part='drawing'] :is([data-part='drawing-line'], [data-part='drawing-box'], [data-part='drawing-level'] line)) { stroke-width: var(--mt-drawing-width); stroke-dasharray: var(--mt-drawing-dash); }
|
|
190
|
+
:where([data-mt][data-part='drawing'][data-state='active'] :is([data-part='drawing-line'], [data-part='drawing-box'], [data-part='drawing-level'] line)) { stroke-width: var(--mt-drawing-width-active); stroke-dasharray: var(--mt-drawing-dash-active); }
|
|
191
|
+
:where([data-mt][data-part='drawing'][data-state='selected'] :is([data-part='drawing-line'], [data-part='drawing-box'], [data-part='drawing-level'] line)) { stroke-width: var(--mt-drawing-width-selected); stroke-dasharray: var(--mt-drawing-dash-selected); }
|
|
192
|
+
:where([data-mt][data-part='drawing'][data-locked] :is([data-part='drawing-line'], [data-part='drawing-box'], [data-part='drawing-level'] line)) { stroke-width: var(--mt-drawing-width-locked, var(--mt-drawing-width)); stroke-dasharray: var(--mt-drawing-dash-locked, var(--mt-drawing-dash)); }
|
|
193
|
+
:where([data-mt][data-part='drawing'][data-locked][data-state='selected'] :is([data-part='drawing-line'], [data-part='drawing-box'], [data-part='drawing-level'] line)) { stroke-width: var(--mt-drawing-width-locked, var(--mt-drawing-width-selected)); stroke-dasharray: var(--mt-drawing-dash-locked, var(--mt-drawing-dash-selected)); }
|
|
194
|
+
:where([data-mt][data-part='drawing'] [data-part='drawing-handle']) { stroke-width: var(--mt-drawing-handle-width); }
|
|
195
|
+
:where([data-mt][data-part='drawing'] [data-part='drawing-handle'][data-dragging]) { stroke-width: var(--mt-drawing-handle-width-dragging); }
|
|
196
|
+
|
|
197
|
+
/* Selected series and studies, the same way drawings show it: a heavier stroke, never colour alone. */
|
|
198
|
+
:where([data-part='candles'][data-state='selected'] :is(line, [data-part='body'], [data-part='line'])) { stroke-width: var(--mt-series-width-selected, 2); }
|
|
199
|
+
:where([data-part='candles'][data-state='selected'] [data-part='body']) { stroke: currentColor; }
|
|
200
|
+
:where([data-part='study-line'][data-state='selected']) { stroke-width: var(--mt-study-width-selected, 2.5); }
|
|
201
|
+
:where([data-part='study-band'][data-state='selected']) { stroke: currentColor; stroke-width: 1.5; }
|
|
202
|
+
:where([data-part='study-select'], [data-part='series-select']) { cursor: pointer; }
|
|
203
|
+
|
|
204
|
+
/* Panels: the series lines, their bands and the selected state. Colour is yours; a selected series is heavier, never colour alone. */
|
|
205
|
+
:where([data-part='panel-series']) { stroke: currentColor; stroke-width: var(--mt-study-width, 1.5); }
|
|
206
|
+
:where([data-part='panel-series'][data-style='histogram']) { stroke: none; fill: currentColor; fill-opacity: var(--mt-histogram-opacity, 0.55); }
|
|
207
|
+
:where([data-part='panel-series'][data-style='histogram'][data-state='selected']) { stroke: currentColor; stroke-width: var(--mt-study-width, 1.5); fill-opacity: 0.85; }
|
|
208
|
+
:where([data-part='panel-point']) { stroke: none; }
|
|
209
|
+
:where([data-part='panel-level']) { stroke: var(--mt-grid, currentColor); stroke-opacity: 0.8; stroke-dasharray: 4 3; }
|
|
210
|
+
:where([data-part='panel-problem']), :where([data-part='panel-gaps']), :where([data-part='study-problem']) { font: 12px var(--mt-font-numeric); }
|
|
211
|
+
:where([data-part='panel-series'][data-state='selected']) { stroke-width: var(--mt-study-width-selected, 2.5); }
|
|
212
|
+
:where([data-part='panel-divider'], [data-part='panel-crosshair']) { stroke: var(--mt-grid, currentColor); stroke-opacity: 0.6; }
|
|
213
|
+
:where([data-part='panel-grid'] line) { stroke: var(--mt-grid, currentColor); stroke-opacity: 0.3; }
|
|
214
|
+
:where([data-part='panel-axis'] text, [data-part='panel-label']) { fill: currentColor; font: 11px var(--mt-font-numeric); }
|
|
215
|
+
:where([data-part='panel-select'], [data-part='panel-move-to']) { cursor: pointer; }
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
* Toolbar, Menu and Submenu. Only what makes them work without a stylesheet: where a menu list sits, and the touch target.
|
|
219
|
+
* Looks (colour, border, shadow, spacing) are yours. :where() keeps the specificity at zero, so any rule of yours wins.
|
|
220
|
+
* Nothing opens on hover: a list opens by tap, click or key.
|
|
221
|
+
*/
|
|
222
|
+
:where([data-mt][data-part='menu'], [data-mt][data-part='submenu']) { position: relative; display: inline-block; }
|
|
223
|
+
:where([data-mt][data-part='menu-list'], [data-mt][data-part='submenu-list']) { position: absolute; z-index: 10; min-inline-size: max-content; margin: 0; }
|
|
224
|
+
:where([data-mt][data-part='menu-list']) { inset-block-start: 100%; inset-inline-start: 0; }
|
|
225
|
+
:where([data-mt][data-part='submenu-list']) { inset-block-start: 0; inset-inline-start: 100%; }
|
|
226
|
+
:where([data-mt][data-part='menu-item'], [data-mt][data-part='menu-radio-item'], [data-mt][data-part='submenu-trigger']) { display: block; inline-size: 100%; text-align: start; }
|
|
227
|
+
:where([data-mt][data-part='toolbar-separator'], [data-mt][data-part='menu-separator']) { margin-block: 2px; margin-inline: 4px; }
|
|
228
|
+
:where([data-mt][data-part='toolbar'][aria-orientation='horizontal']) { display: flex; flex-wrap: wrap; align-items: center; }
|
|
229
|
+
:where([data-mt][data-part='toolbar'][aria-orientation='vertical']) { display: flex; flex-direction: column; align-items: stretch; }
|
|
230
|
+
:where([data-mt] [data-part='icon']) { display: inline-flex; align-items: center; justify-content: center; }
|
|
231
|
+
|
|
232
|
+
/* Phones and tablets: every toolbar button, menu trigger and menu item reaches --mt-touch-target in both directions. */
|
|
233
|
+
@media (pointer: coarse) {
|
|
234
|
+
:where([data-mt][data-touch-target]) { min-block-size: var(--mt-touch-target); min-inline-size: var(--mt-touch-target); }
|
|
235
|
+
}
|