@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.
Files changed (216) hide show
  1. package/AGENTS.md +44 -0
  2. package/CHANGELOG.md +19 -0
  3. package/LICENSE +201 -0
  4. package/README.md +636 -0
  5. package/dist/Chart.d.ts +18 -0
  6. package/dist/Chart.js +6 -0
  7. package/dist/ChartParts.d.ts +151 -0
  8. package/dist/ChartParts.js +389 -0
  9. package/dist/CsvDrop.d.ts +8 -0
  10. package/dist/CsvDrop.js +5 -0
  11. package/dist/CsvDropParts.d.ts +33 -0
  12. package/dist/CsvDropParts.js +77 -0
  13. package/dist/Cursor.d.ts +6 -0
  14. package/dist/Cursor.js +5 -0
  15. package/dist/CursorParts.d.ts +51 -0
  16. package/dist/CursorParts.js +40 -0
  17. package/dist/DepthProfile.d.ts +13 -0
  18. package/dist/DepthProfile.js +5 -0
  19. package/dist/DepthProfileParts.d.ts +45 -0
  20. package/dist/DepthProfileParts.js +96 -0
  21. package/dist/DiffTable.d.ts +13 -0
  22. package/dist/DiffTable.js +5 -0
  23. package/dist/DiffTableParts.d.ts +57 -0
  24. package/dist/DiffTableParts.js +103 -0
  25. package/dist/Drawings.d.ts +13 -0
  26. package/dist/Drawings.js +5 -0
  27. package/dist/DrawingsParts.d.ts +194 -0
  28. package/dist/DrawingsParts.js +775 -0
  29. package/dist/Fullscreen.d.ts +7 -0
  30. package/dist/Fullscreen.js +5 -0
  31. package/dist/FullscreenParts.d.ts +54 -0
  32. package/dist/FullscreenParts.js +250 -0
  33. package/dist/Ladder.d.ts +13 -0
  34. package/dist/Ladder.js +5 -0
  35. package/dist/LadderParts.d.ts +49 -0
  36. package/dist/LadderParts.js +56 -0
  37. package/dist/Menu.d.ts +11 -0
  38. package/dist/Menu.js +5 -0
  39. package/dist/MenuParts.d.ts +52 -0
  40. package/dist/MenuParts.js +202 -0
  41. package/dist/Notice.d.ts +18 -0
  42. package/dist/Notice.js +9 -0
  43. package/dist/Panels.d.ts +9 -0
  44. package/dist/Panels.js +5 -0
  45. package/dist/PanelsParts.d.ts +62 -0
  46. package/dist/PanelsParts.js +387 -0
  47. package/dist/Receipt.d.ts +11 -0
  48. package/dist/Receipt.js +5 -0
  49. package/dist/ReceiptParts.d.ts +45 -0
  50. package/dist/ReceiptParts.js +71 -0
  51. package/dist/Scrubber.d.ts +9 -0
  52. package/dist/Scrubber.js +5 -0
  53. package/dist/ScrubberParts.d.ts +74 -0
  54. package/dist/ScrubberParts.js +126 -0
  55. package/dist/Selection.d.ts +14 -0
  56. package/dist/Selection.js +7 -0
  57. package/dist/SelectionParts.d.ts +63 -0
  58. package/dist/SelectionParts.js +208 -0
  59. package/dist/Studies.d.ts +9 -0
  60. package/dist/Studies.js +5 -0
  61. package/dist/StudiesParts.d.ts +45 -0
  62. package/dist/StudiesParts.js +153 -0
  63. package/dist/Submenu.d.ts +7 -0
  64. package/dist/Submenu.js +4 -0
  65. package/dist/SymbolSearch.d.ts +10 -0
  66. package/dist/SymbolSearch.js +5 -0
  67. package/dist/SymbolSearchParts.d.ts +39 -0
  68. package/dist/SymbolSearchParts.js +86 -0
  69. package/dist/Tape.d.ts +15 -0
  70. package/dist/Tape.js +5 -0
  71. package/dist/TapeParts.d.ts +69 -0
  72. package/dist/TapeParts.js +139 -0
  73. package/dist/Toolbar.d.ts +8 -0
  74. package/dist/Toolbar.js +5 -0
  75. package/dist/ToolbarParts.d.ts +61 -0
  76. package/dist/ToolbarParts.js +122 -0
  77. package/dist/Transport.d.ts +15 -0
  78. package/dist/Transport.js +5 -0
  79. package/dist/TransportParts.d.ts +52 -0
  80. package/dist/TransportParts.js +47 -0
  81. package/dist/VolumeProfile.d.ts +7 -0
  82. package/dist/VolumeProfile.js +5 -0
  83. package/dist/VolumeProfileParts.d.ts +37 -0
  84. package/dist/VolumeProfileParts.js +57 -0
  85. package/dist/accessible-name.d.ts +26 -0
  86. package/dist/accessible-name.js +14 -0
  87. package/dist/attributes.d.ts +8 -0
  88. package/dist/attributes.js +9 -0
  89. package/dist/core/adapter.d.ts +38 -0
  90. package/dist/core/adapter.js +64 -0
  91. package/dist/core/attributes.d.ts +36 -0
  92. package/dist/core/attributes.js +22 -0
  93. package/dist/core/candles.d.ts +6 -0
  94. package/dist/core/candles.js +19 -0
  95. package/dist/core/chart-gesture.d.ts +109 -0
  96. package/dist/core/chart-gesture.js +161 -0
  97. package/dist/core/chart-scale.d.ts +141 -0
  98. package/dist/core/chart-scale.js +302 -0
  99. package/dist/core/csv-bars.d.ts +54 -0
  100. package/dist/core/csv-bars.js +325 -0
  101. package/dist/core/depth-profile.d.ts +43 -0
  102. package/dist/core/depth-profile.js +66 -0
  103. package/dist/core/diff-table.d.ts +57 -0
  104. package/dist/core/diff-table.js +77 -0
  105. package/dist/core/drawing-fields.d.ts +11 -0
  106. package/dist/core/drawing-fields.js +15 -0
  107. package/dist/core/drawings.d.ts +194 -0
  108. package/dist/core/drawings.js +403 -0
  109. package/dist/core/fullscreen.d.ts +32 -0
  110. package/dist/core/fullscreen.js +16 -0
  111. package/dist/core/index.d.ts +41 -0
  112. package/dist/core/index.js +23 -0
  113. package/dist/core/ladder-keys.d.ts +35 -0
  114. package/dist/core/ladder-keys.js +43 -0
  115. package/dist/core/ladder.d.ts +65 -0
  116. package/dist/core/ladder.js +93 -0
  117. package/dist/core/panels.d.ts +72 -0
  118. package/dist/core/panels.js +151 -0
  119. package/dist/core/receipt-rules.d.ts +42 -0
  120. package/dist/core/receipt-rules.js +20 -0
  121. package/dist/core/scrubber-gesture.d.ts +66 -0
  122. package/dist/core/scrubber-gesture.js +69 -0
  123. package/dist/core/selection.d.ts +24 -0
  124. package/dist/core/selection.js +33 -0
  125. package/dist/core/series.d.ts +161 -0
  126. package/dist/core/series.js +331 -0
  127. package/dist/core/studies.d.ts +80 -0
  128. package/dist/core/studies.js +237 -0
  129. package/dist/core/symbol-search.d.ts +89 -0
  130. package/dist/core/symbol-search.js +223 -0
  131. package/dist/core/tape.d.ts +77 -0
  132. package/dist/core/tape.js +136 -0
  133. package/dist/core/volume-profile.d.ts +47 -0
  134. package/dist/core/volume-profile.js +86 -0
  135. package/dist/index.d.ts +82 -0
  136. package/dist/index.js +42 -0
  137. package/dist/ladder-keys.d.ts +16 -0
  138. package/dist/ladder-keys.js +86 -0
  139. package/dist/panels-context.d.ts +47 -0
  140. package/dist/panels-context.js +3 -0
  141. package/dist/selection-context.d.ts +41 -0
  142. package/dist/selection-context.js +4 -0
  143. package/examples/csv-to-chart-frame.mjs +17 -0
  144. package/examples/studies.mjs +12 -0
  145. package/llms.txt +16 -0
  146. package/package.json +67 -0
  147. package/src/Chart.tsx +7 -0
  148. package/src/ChartParts.tsx +529 -0
  149. package/src/CsvDrop.tsx +6 -0
  150. package/src/CsvDropParts.tsx +128 -0
  151. package/src/Cursor.tsx +6 -0
  152. package/src/CursorParts.tsx +77 -0
  153. package/src/DepthProfile.tsx +6 -0
  154. package/src/DepthProfileParts.tsx +154 -0
  155. package/src/DiffTable.tsx +6 -0
  156. package/src/DiffTableParts.tsx +160 -0
  157. package/src/Drawings.tsx +6 -0
  158. package/src/DrawingsParts.tsx +1080 -0
  159. package/src/Fullscreen.tsx +6 -0
  160. package/src/FullscreenParts.tsx +318 -0
  161. package/src/Ladder.tsx +6 -0
  162. package/src/LadderParts.tsx +114 -0
  163. package/src/Menu.tsx +6 -0
  164. package/src/MenuParts.tsx +364 -0
  165. package/src/Notice.tsx +27 -0
  166. package/src/Panels.tsx +6 -0
  167. package/src/PanelsParts.tsx +454 -0
  168. package/src/Receipt.tsx +6 -0
  169. package/src/ReceiptParts.tsx +118 -0
  170. package/src/Scrubber.tsx +6 -0
  171. package/src/ScrubberParts.tsx +207 -0
  172. package/src/Selection.tsx +12 -0
  173. package/src/SelectionParts.tsx +251 -0
  174. package/src/Studies.tsx +6 -0
  175. package/src/StudiesParts.tsx +226 -0
  176. package/src/Submenu.tsx +5 -0
  177. package/src/SymbolSearch.tsx +6 -0
  178. package/src/SymbolSearchParts.tsx +166 -0
  179. package/src/Tape.tsx +6 -0
  180. package/src/TapeParts.tsx +208 -0
  181. package/src/Toolbar.tsx +6 -0
  182. package/src/ToolbarParts.tsx +179 -0
  183. package/src/Transport.tsx +6 -0
  184. package/src/TransportParts.tsx +128 -0
  185. package/src/VolumeProfile.tsx +6 -0
  186. package/src/VolumeProfileParts.tsx +106 -0
  187. package/src/accessible-name.tsx +34 -0
  188. package/src/attributes.ts +12 -0
  189. package/src/core/adapter.ts +91 -0
  190. package/src/core/attributes.ts +51 -0
  191. package/src/core/candles.ts +22 -0
  192. package/src/core/chart-gesture.ts +228 -0
  193. package/src/core/chart-scale.ts +368 -0
  194. package/src/core/csv-bars.ts +328 -0
  195. package/src/core/depth-profile.ts +84 -0
  196. package/src/core/diff-table.ts +105 -0
  197. package/src/core/drawing-fields.ts +17 -0
  198. package/src/core/drawings.ts +478 -0
  199. package/src/core/fullscreen.ts +32 -0
  200. package/src/core/index.ts +42 -0
  201. package/src/core/ladder-keys.ts +61 -0
  202. package/src/core/ladder.ts +156 -0
  203. package/src/core/panels.ts +177 -0
  204. package/src/core/receipt-rules.ts +45 -0
  205. package/src/core/scrubber-gesture.ts +102 -0
  206. package/src/core/selection.ts +41 -0
  207. package/src/core/series.ts +374 -0
  208. package/src/core/studies.ts +260 -0
  209. package/src/core/symbol-search.ts +256 -0
  210. package/src/core/tape.ts +159 -0
  211. package/src/core/volume-profile.ts +116 -0
  212. package/src/index.ts +83 -0
  213. package/src/ladder-keys.ts +83 -0
  214. package/src/panels-context.ts +46 -0
  215. package/src/selection-context.ts +38 -0
  216. package/src/tokens.css +235 -0
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useRef, useState } from 'react';
4
+ import { describeBarsCsv, parseBarsCsv } from './core/csv-bars.js';
5
+ const Ctx = createContext(null);
6
+ function useCsvDrop() {
7
+ const ctx = useContext(Ctx);
8
+ if (!ctx)
9
+ throw new Error('CsvDrop parts must be inside CsvDrop.Root');
10
+ return ctx;
11
+ }
12
+ /**
13
+ * A drop area for the user's own CSV file. The file is read in the browser and never leaves the page. Drop a file
14
+ * on the area, or use CsvDrop.Input (the keyboard path: a native file picker). State shows as `data-state`
15
+ * ('idle' | 'reading' | 'loaded' | 'error') and `data-dragging` while a file is over the area.
16
+ */
17
+ function Root({ onLoad, maxBytes = 50 * 1024 * 1024, maxRows, className, children }) {
18
+ const [state, setState] = useState({ kind: 'idle' });
19
+ const [dragging, setDragging] = useState(false);
20
+ const depth = useRef(0);
21
+ // Each read gets a number; a slower earlier read that finishes late is dropped, so the newest file always wins.
22
+ const generation = useRef(0);
23
+ const read = useCallback((file) => {
24
+ const id = ++generation.current;
25
+ if (file.size > maxBytes) {
26
+ setState({ kind: 'error', message: `${file.name} is larger than ${Math.round(maxBytes / 1024 / 1024)} MB, so it wasn't read.` });
27
+ return;
28
+ }
29
+ setState({ kind: 'reading', fileName: file.name });
30
+ file.text().then((text) => {
31
+ if (id !== generation.current)
32
+ return;
33
+ const load = { ...parseBarsCsv(text, { maxRows }), fileName: file.name };
34
+ setState({ kind: 'loaded', load });
35
+ onLoad(load);
36
+ }, () => { if (id === generation.current)
37
+ setState({ kind: 'error', message: `${file.name} couldn't be read.` }); });
38
+ }, [maxBytes, maxRows, onLoad]);
39
+ const hasFile = (e) => Array.from(e.dataTransfer.types).includes('Files');
40
+ return (_jsx(Ctx.Provider, { value: { state, read }, children: _jsx("div", { "data-mt": "", "data-part": "csv-drop", "data-state": state.kind, "data-dragging": dragging ? '' : undefined, className: className, onDragEnter: (e) => { if (!hasFile(e))
41
+ return; e.preventDefault(); depth.current++; setDragging(true); }, onDragOver: (e) => { if (hasFile(e))
42
+ e.preventDefault(); }, onDragLeave: () => { depth.current = Math.max(0, depth.current - 1); if (depth.current === 0)
43
+ setDragging(false); }, onDrop: (e) => {
44
+ if (!hasFile(e))
45
+ return;
46
+ e.preventDefault();
47
+ depth.current = 0;
48
+ setDragging(false);
49
+ const file = e.dataTransfer.files[0];
50
+ if (file)
51
+ read(file);
52
+ }, children: children }) }));
53
+ }
54
+ /** The native file picker, labelled by its children. Accepts .csv, .tsv and .txt. */
55
+ function Input({ className, children }) {
56
+ const { read } = useCsvDrop();
57
+ return (_jsxs("label", { "data-part": "csv-input", className: className, children: [children, _jsx("input", { type: "file", accept: ".csv,.tsv,.txt,text/csv,text/tab-separated-values,text/plain", onChange: (e) => {
58
+ const file = e.target.files?.[0];
59
+ if (file)
60
+ read(file);
61
+ e.target.value = '';
62
+ } })] }));
63
+ }
64
+ /** A polite status line: reading, how many bars loaded, and which rows were skipped and why. */
65
+ function Status({ className, idle }) {
66
+ const { state } = useCsvDrop();
67
+ let text = idle ?? null;
68
+ if (state.kind === 'reading')
69
+ text = `Reading ${state.fileName}…`;
70
+ if (state.kind === 'loaded')
71
+ text = describeBarsCsv(state.load, state.load.fileName);
72
+ if (state.kind === 'error')
73
+ text = state.message;
74
+ const failed = state.kind === 'error' || (state.kind === 'loaded' && state.load.bars.length === 0);
75
+ return (_jsx("p", { role: "status", "aria-live": "polite", "data-part": "csv-status", "data-kind": failed ? 'error' : undefined, className: className, children: text }));
76
+ }
77
+ export { Root, Input, Status };
@@ -0,0 +1,6 @@
1
+ import { Root } from './CursorParts.js';
2
+ export * from './CursorParts.js';
3
+ export declare const Cursor: {
4
+ Root: typeof Root;
5
+ };
6
+ //# sourceMappingURL=Cursor.d.ts.map
package/dist/Cursor.js ADDED
@@ -0,0 +1,5 @@
1
+ import { Root } from './CursorParts.js';
2
+ // The parts live in CursorParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `Cursor.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './CursorParts.js';
5
+ export const Cursor = { Root };
@@ -0,0 +1,51 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Cursor: the one owner of "where in the session are we". Every view (Transport, Scrubber,
4
+ * Ladder, DiffTable) reads this one value instead of keeping its own copy, so linked panels cannot disagree
5
+ * about time. Renders no DOM.
6
+ *
7
+ * The value is the engine's own cursor shape, sequence and timestamp as bigint: an exact integer, never a
8
+ * float. (A wire form with decimal strings belongs at the contract boundary, not in this React state.)
9
+ *
10
+ * Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`), the same three-prop pattern the
11
+ * headless libraries use. A controlled Root never changes the value itself: `setValue` only calls
12
+ * `onValueChange`, and the host decides.
13
+ */
14
+ export type CursorValue = {
15
+ readonly position: 'initial';
16
+ } | {
17
+ readonly position: 'event';
18
+ readonly sequence: bigint;
19
+ readonly timestamp: bigint;
20
+ };
21
+ export type CursorState = 'idle' | 'playing' | 'seeking';
22
+ export type CursorBoundary = 'start' | 'end' | null;
23
+ export declare const CURSOR_START: CursorValue;
24
+ /** True when both are the same place in the session. */
25
+ export declare function sameCursor(a: CursorValue, b: CursorValue): boolean;
26
+ export interface CursorRootProps {
27
+ value?: CursorValue;
28
+ defaultValue?: CursorValue;
29
+ onValueChange?: (value: CursorValue) => void;
30
+ /** What the host is doing with the cursor. Exposed as `data-state` to every view that asks for attributes. */
31
+ state?: CursorState;
32
+ /** Set when the cursor is at the start or end of the session. */
33
+ boundary?: CursorBoundary;
34
+ children?: ReactNode;
35
+ }
36
+ interface CursorContext {
37
+ value: CursorValue;
38
+ setValue: (value: CursorValue) => void;
39
+ state: CursorState;
40
+ boundary: CursorBoundary;
41
+ }
42
+ declare function Root({ value, defaultValue, onValueChange, state, boundary, children }: CursorRootProps): import("react").JSX.Element;
43
+ /** Reads the shared cursor. Throws a plain message when there is no `Cursor.Root` above, rather than guessing a position. */
44
+ export declare function useCursor(): CursorContext & {
45
+ attributes: {
46
+ 'data-state': CursorState;
47
+ 'data-boundary'?: 'start' | 'end';
48
+ };
49
+ };
50
+ export { Root };
51
+ //# sourceMappingURL=CursorParts.d.ts.map
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
4
+ export const CURSOR_START = { position: 'initial' };
5
+ /** True when both are the same place in the session. */
6
+ export function sameCursor(a, b) {
7
+ if (a.position === 'initial' || b.position === 'initial')
8
+ return a.position === b.position;
9
+ return a.sequence === b.sequence && a.timestamp === b.timestamp;
10
+ }
11
+ const Context = createContext(null);
12
+ function Root({ value, defaultValue = CURSOR_START, onValueChange, state = 'idle', boundary = null, children }) {
13
+ const controlled = value !== undefined;
14
+ const [inner, setInner] = useState(defaultValue);
15
+ const current = controlled ? value : inner;
16
+ // The latest callback and mode, read at call time, so `setValue` keeps one identity for the life of the Root.
17
+ const live = useRef({ controlled, onValueChange, current });
18
+ live.current = { controlled, onValueChange, current };
19
+ const setValue = useCallback((next) => {
20
+ const { controlled: isControlled, onValueChange: notify, current: now } = live.current;
21
+ if (sameCursor(now, next))
22
+ return;
23
+ if (!isControlled)
24
+ setInner(next);
25
+ notify?.(next);
26
+ }, []);
27
+ const context = useMemo(() => ({ value: current, setValue, state, boundary }), [current, setValue, state, boundary]);
28
+ return _jsx(Context.Provider, { value: context, children: children });
29
+ }
30
+ /** Reads the shared cursor. Throws a plain message when there is no `Cursor.Root` above, rather than guessing a position. */
31
+ export function useCursor() {
32
+ const context = useContext(Context);
33
+ if (!context)
34
+ throw new Error('useCursor needs a Cursor.Root above it: there is no shared cursor to read.');
35
+ const attributes = context.boundary
36
+ ? { 'data-state': context.state, 'data-boundary': context.boundary }
37
+ : { 'data-state': context.state };
38
+ return { ...context, attributes };
39
+ }
40
+ export { Root };
@@ -0,0 +1,13 @@
1
+ import { Root, Plot, Bids, Asks, Axis, Crosshair, Readout, Table } from './DepthProfileParts.js';
2
+ export * from './DepthProfileParts.js';
3
+ export declare const DepthProfile: {
4
+ Root: typeof Root;
5
+ Plot: typeof Plot;
6
+ Bids: typeof Bids;
7
+ Asks: typeof Asks;
8
+ Axis: typeof Axis;
9
+ Crosshair: typeof Crosshair;
10
+ Readout: typeof Readout;
11
+ Table: typeof Table;
12
+ };
13
+ //# sourceMappingURL=DepthProfile.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { Root, Plot, Bids, Asks, Axis, Crosshair, Readout, Table } from './DepthProfileParts.js';
2
+ // The parts live in DepthProfileParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `DepthProfile.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './DepthProfileParts.js';
5
+ export const DepthProfile = { Root, Plot, Bids, Asks, Axis, Crosshair, Readout, Table };
@@ -0,0 +1,45 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type DepthLevel } from './core/depth-profile.js';
3
+ export interface DepthProfileRootProps {
4
+ /** Best price first on each side. */
5
+ levels: readonly DepthLevel[];
6
+ /** Controlled crosshair: the `side:price` id of the level, or null for none. */
7
+ active?: string | null;
8
+ onActiveChange?: (id: string | null) => void;
9
+ className?: string;
10
+ 'aria-label'?: string;
11
+ children?: ReactNode;
12
+ }
13
+ declare function Root({ levels: given, active, onActiveChange, className, 'aria-label': label, children }: DepthProfileRootProps): import("react").JSX.Element;
14
+ /** The drawing. One tab stop; a tap or click picks the nearest level; ArrowLeft/Right move the crosshair, Home/End jump to the ends, Escape clears it. */
15
+ declare function Plot({ className, label, children }: {
16
+ className?: string;
17
+ label?: string;
18
+ children?: ReactNode;
19
+ }): import("react").JSX.Element;
20
+ declare function Bids({ className }: {
21
+ className?: string;
22
+ }): import("react").JSX.Element;
23
+ declare function Asks({ className }: {
24
+ className?: string;
25
+ }): import("react").JSX.Element;
26
+ /** The lowest and highest price on the plot, as recorded, at its two ends. */
27
+ declare function Axis({ className }: {
28
+ className?: string;
29
+ }): import("react").JSX.Element | null;
30
+ /** A line and a point at the active level; nothing is drawn while there is none. */
31
+ declare function Crosshair({ className }: {
32
+ className?: string;
33
+ }): import("react").JSX.Element | null;
34
+ /** The active level in words, announced politely. The host may pass its own wording. */
35
+ declare function Readout({ className, format }: {
36
+ className?: string;
37
+ format?: (level: DepthLevel) => string;
38
+ }): import("react").JSX.Element;
39
+ /** The same levels as a table: the text equivalent of the plot. Prices run low to high. */
40
+ declare function Table({ className, caption }: {
41
+ className?: string;
42
+ caption?: string;
43
+ }): import("react").JSX.Element;
44
+ export { Root, Plot, Bids, Asks, Axis, Crosshair, Readout, Table };
45
+ //# sourceMappingURL=DepthProfileParts.d.ts.map
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useId, useMemo, useState } from 'react';
4
+ import { DEPTH_HEIGHT, DEPTH_WIDTH, depthGeometry, depthLevelId, nextDepthIndex, resolveDepthKey } from './core/depth-profile.js';
5
+ const Context = createContext(null);
6
+ function useDepth(part) {
7
+ const context = useContext(Context);
8
+ if (!context)
9
+ throw new Error(`DepthProfile.${part} needs a DepthProfile.Root above it.`);
10
+ return context;
11
+ }
12
+ function Root({ levels: given, active, onActiveChange, className, 'aria-label': label = 'Cumulative order-book depth', children }) {
13
+ const levels = useMemo(() => (Array.isArray(given) ? given.filter((level) => level != null) : []), [given]); // no list, or a hole in it, is left out
14
+ const geometry = useMemo(() => depthGeometry(levels), [levels]);
15
+ const [inner, setInner] = useState(null);
16
+ const controlled = active !== undefined;
17
+ const activeId = controlled ? active : inner;
18
+ const index = activeId === null ? -1 : geometry.ordered.findIndex((l) => depthLevelId(l) === activeId);
19
+ const readoutId = useId();
20
+ const setActive = useCallback((next) => {
21
+ const id = next === null ? null : depthLevelId(geometry.ordered[next]);
22
+ if (!controlled)
23
+ setInner(id);
24
+ onActiveChange?.(id);
25
+ }, [geometry, controlled, onActiveChange]);
26
+ const context = useMemo(() => ({ ...geometry, active: index < 0 ? null : index, setActive, readoutId }), [geometry, index, setActive, readoutId]);
27
+ return (_jsx(Context.Provider, { value: context, children: _jsx("figure", { className: className, "aria-label": label, children: children }) }));
28
+ }
29
+ /** The drawing. One tab stop; a tap or click picks the nearest level; ArrowLeft/Right move the crosshair, Home/End jump to the ends, Escape clears it. */
30
+ function Plot({ className, label = 'Bid and ask cumulative depth', children }) {
31
+ const { ordered, active, setActive, readoutId, x } = useDepth('Plot');
32
+ const onKeyDown = (event) => {
33
+ const action = resolveDepthKey(event);
34
+ if (!action)
35
+ return;
36
+ event.preventDefault();
37
+ event.stopPropagation(); // arrows, Home and End are also transport shortcuts on the page
38
+ setActive(nextDepthIndex(ordered, active, action));
39
+ };
40
+ const onPointerDown = (event) => {
41
+ const svg = event.currentTarget;
42
+ const matrix = svg.getScreenCTM();
43
+ if (!ordered.length || !matrix)
44
+ return;
45
+ // Through the inverse screen transform, so the letterboxing of preserveAspectRatio is accounted for.
46
+ const point = svg.createSVGPoint();
47
+ point.x = event.clientX;
48
+ point.y = event.clientY;
49
+ const at = point.matrixTransform(matrix.inverse()).x;
50
+ let nearest = 0;
51
+ for (let i = 1; i < ordered.length; i++) {
52
+ if (Math.abs(x(ordered[i].price) - at) < Math.abs(x(ordered[nearest].price) - at))
53
+ nearest = i;
54
+ }
55
+ setActive(nearest);
56
+ };
57
+ return (_jsx("svg", { className: className, viewBox: `0 0 ${DEPTH_WIDTH} ${DEPTH_HEIGHT}`, role: "group", "aria-label": label, "aria-describedby": readoutId, tabIndex: 0, onKeyDown: onKeyDown, onPointerDown: onPointerDown, children: children }));
58
+ }
59
+ function Bids({ className }) {
60
+ const { bidPoints } = useDepth('Bids');
61
+ return _jsx("polygon", { className: className, "data-part": "depth-bids", "data-side": "bid", points: bidPoints });
62
+ }
63
+ function Asks({ className }) {
64
+ const { askPoints } = useDepth('Asks');
65
+ return _jsx("polygon", { className: className, "data-part": "depth-asks", "data-side": "ask", points: askPoints });
66
+ }
67
+ /** The lowest and highest price on the plot, as recorded, at its two ends. */
68
+ function Axis({ className }) {
69
+ const { ordered } = useDepth('Axis');
70
+ if (ordered.length === 0)
71
+ return null;
72
+ const low = ordered[0].price;
73
+ const high = ordered[ordered.length - 1].price;
74
+ return (_jsxs("g", { className: className, "data-part": "depth-axis", "aria-hidden": "true", children: [_jsx("text", { x: 0, y: DEPTH_HEIGHT - 2, textAnchor: "start", children: low }), _jsx("text", { x: DEPTH_WIDTH, y: DEPTH_HEIGHT - 2, textAnchor: "end", children: high })] }));
75
+ }
76
+ /** A line and a point at the active level; nothing is drawn while there is none. */
77
+ function Crosshair({ className }) {
78
+ const { ordered, active, x, y } = useDepth('Crosshair');
79
+ if (active === null)
80
+ return null;
81
+ const level = ordered[active];
82
+ return (_jsxs("g", { className: className, "data-part": "depth-crosshair", "data-side": level.side, "aria-hidden": "true", children: [_jsx("line", { x1: x(level.price), x2: x(level.price), y1: 0, y2: DEPTH_HEIGHT }), _jsx("circle", { cx: x(level.price), cy: y(level.cumulative), r: 4 })] }));
83
+ }
84
+ /** The active level in words, announced politely. The host may pass its own wording. */
85
+ function Readout({ className, format }) {
86
+ const { ordered, active, readoutId } = useDepth('Readout');
87
+ const level = active === null ? null : ordered[active];
88
+ const text = level === null ? '' : format ? format(level) : `${level.side === 'bid' ? 'Bid' : 'Ask'} ${level.price}, cumulative size ${level.cumulative}`;
89
+ return _jsx("output", { id: readoutId, className: className, "aria-live": "polite", "data-side": level?.side, children: text });
90
+ }
91
+ /** The same levels as a table: the text equivalent of the plot. Prices run low to high. */
92
+ function Table({ className, caption = 'Cumulative depth by price level' }) {
93
+ const { ordered, active } = useDepth('Table');
94
+ return (_jsxs("table", { className: className, children: [_jsx("caption", { children: caption }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Side" }), _jsx("th", { scope: "col", children: "Price" }), _jsx("th", { scope: "col", children: "Cumulative size" })] }) }), _jsx("tbody", { children: ordered.map((level, i) => (_jsxs("tr", { "data-side": level.side, "aria-current": i === active ? 'true' : undefined, children: [_jsx("td", { children: level.side === 'bid' ? 'Bid' : 'Ask' }), _jsx("td", { "data-mt-numeric": true, children: level.price }), _jsx("td", { "data-mt-numeric": true, children: level.cumulative })] }, depthLevelId(level)))) })] }));
95
+ }
96
+ export { Root, Plot, Bids, Asks, Axis, Crosshair, Readout, Table };
@@ -0,0 +1,13 @@
1
+ import { Root, Header, Row, Price, QtyA, QtyB, Delta, ChangeMark } from './DiffTableParts.js';
2
+ export * from './DiffTableParts.js';
3
+ export declare const DiffTable: {
4
+ Root: typeof Root;
5
+ Header: typeof Header;
6
+ Row: typeof Row;
7
+ Price: typeof Price;
8
+ QtyA: typeof QtyA;
9
+ QtyB: typeof QtyB;
10
+ Delta: typeof Delta;
11
+ ChangeMark: typeof ChangeMark;
12
+ };
13
+ //# sourceMappingURL=DiffTable.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { Root, Header, Row, Price, QtyA, QtyB, Delta, ChangeMark } from './DiffTableParts.js';
2
+ // The parts live in DiffTableParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `DiffTable.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './DiffTableParts.js';
5
+ export const DiffTable = { Root, Header, Row, Price, QtyA, QtyB, Delta, ChangeMark };
@@ -0,0 +1,57 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type DiffChange, type DiffSort, type DiffTableRow } from './core/diff-table.js';
3
+ export interface DiffTableRootProps {
4
+ rows: readonly DiffTableRow[];
5
+ /** Controlled sort; leave out for an uncontrolled table that starts as `book`. */
6
+ sort?: DiffSort;
7
+ onSortChange?: (sort: DiffSort) => void;
8
+ /** Enter or Space on a focused row. */
9
+ onSelect?: (row: DiffTableRow) => void;
10
+ /** The `a` or `b` key: focus that cursor control. */
11
+ onFocusCursor?: (which: 'a' | 'b') => void;
12
+ className?: string;
13
+ caption?: string;
14
+ captionClassName?: string;
15
+ /** The parts, given the rows in the order to show them (sorted, and held while the pointer is over the table). */
16
+ children: (rows: DiffTableRow[]) => ReactNode;
17
+ }
18
+ declare function Root({ rows, sort, onSortChange, onSelect, onFocusCursor, className, caption, captionClassName, children }: DiffTableRootProps): import("react").JSX.Element;
19
+ /** The header row. Put the `<th scope="col">` cells inside. */
20
+ declare function Header({ className, children }: {
21
+ className?: string;
22
+ children: ReactNode;
23
+ }): import("react").JSX.Element;
24
+ /** One level. A single tab stop for the table: the active row, or the first. The pointer does not move it. */
25
+ declare function Row({ row, selected, className, children }: {
26
+ row: DiffTableRow;
27
+ selected?: boolean;
28
+ className?: string;
29
+ children: ReactNode;
30
+ }): import("react").JSX.Element;
31
+ /** The row header cell: the price as recorded. Pass children to put a control inside. */
32
+ declare function Price({ row, className, children }: {
33
+ row: DiffTableRow;
34
+ className?: string;
35
+ children?: ReactNode;
36
+ }): import("react").JSX.Element;
37
+ declare function QtyA({ row, className }: {
38
+ row: DiffTableRow;
39
+ className?: string;
40
+ }): import("react").JSX.Element;
41
+ declare function QtyB({ row, className }: {
42
+ row: DiffTableRow;
43
+ className?: string;
44
+ }): import("react").JSX.Element;
45
+ /** qtyB minus qtyA with its sign shown. */
46
+ declare function Delta({ row, className }: {
47
+ row: DiffTableRow;
48
+ className?: string;
49
+ }): import("react").JSX.Element;
50
+ /** The change in words; tokens.css adds the mark (+, −, ±) so it never rests on colour alone. */
51
+ declare function ChangeMark({ change, className, label }: {
52
+ change: DiffChange;
53
+ className?: string;
54
+ label?: string;
55
+ }): import("react").JSX.Element;
56
+ export { Root, Header, Row, Price, QtyA, QtyB, Delta, ChangeMark };
57
+ //# sourceMappingURL=DiffTableParts.d.ts.map
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
4
+ import { primitiveAttributes } from './attributes.js';
5
+ import { DIFF_CHANGE_LABEL, diffRowId, holdDiffOrder, nextDiffIndex, nextDiffSort, resolveDiffKey, signedDelta, sortDiffRows } from './core/diff-table.js';
6
+ const Context = createContext(null);
7
+ function useDiff(part) {
8
+ const context = useContext(Context);
9
+ if (!context)
10
+ throw new Error(`DiffTable.${part} needs a DiffTable.Root above it.`);
11
+ return context;
12
+ }
13
+ function Root({ rows, sort, onSortChange, onSelect, onFocusCursor, className, caption, captionClassName, children }) {
14
+ const [innerSort, setInnerSort] = useState('book');
15
+ const controlled = sort !== undefined;
16
+ const current = controlled ? sort : innerSort;
17
+ const [held, setHeld] = useState(null);
18
+ const [activeId, setActiveId] = useState(null);
19
+ const table = useRef(null);
20
+ const sorted = useMemo(() => sortDiffRows(rows, current), [rows, current]);
21
+ const shown = useMemo(() => (held ? holdDiffOrder(held, sorted) : sorted), [held, sorted]);
22
+ const firstId = shown.length ? diffRowId(shown[0]) : null;
23
+ const changeSort = useCallback(() => {
24
+ const next = nextDiffSort(current);
25
+ if (!controlled)
26
+ setInnerSort(next);
27
+ setHeld(null); // asking for a new order is the one time the rows may move under the pointer
28
+ onSortChange?.(next);
29
+ }, [current, controlled, onSortChange]);
30
+ const focusRow = (id) => {
31
+ setActiveId(id);
32
+ for (const tr of table.current?.querySelectorAll('tbody tr[data-diff-row]') ?? []) {
33
+ if (tr.dataset.diffRow === id) {
34
+ tr.focus();
35
+ return;
36
+ }
37
+ }
38
+ };
39
+ const onKeyDown = (event) => {
40
+ const action = resolveDiffKey(event.nativeEvent);
41
+ if (!action)
42
+ return;
43
+ const onRow = event.target.closest('tr[data-diff-row]');
44
+ if (action.type === 'select') {
45
+ if (event.target !== onRow || !onRow)
46
+ return; // Enter and Space on the pick button are the button's own
47
+ const row = shown.find((r) => diffRowId(r) === onRow.dataset.diffRow);
48
+ if (!row || !onSelect)
49
+ return;
50
+ event.preventDefault();
51
+ onSelect(row);
52
+ return;
53
+ }
54
+ if (action.type === 'move' && !onRow)
55
+ return;
56
+ event.preventDefault();
57
+ event.stopPropagation(); // these letters and arrows are also page shortcuts
58
+ if (action.type === 'move') {
59
+ const index = shown.findIndex((r) => diffRowId(r) === onRow?.dataset.diffRow);
60
+ const to = nextDiffIndex(shown.length, index, action.to);
61
+ if (to >= 0)
62
+ focusRow(diffRowId(shown[to]));
63
+ }
64
+ else if (action.type === 'cursor')
65
+ onFocusCursor?.(action.which);
66
+ else
67
+ changeSort();
68
+ };
69
+ const context = useMemo(() => ({ activeId, firstId, setActive: setActiveId }), [activeId, firstId]);
70
+ return (_jsx(Context.Provider, { value: context, children: _jsxs("table", { ref: table, className: className, "data-mt": "", "data-sorted": current, onPointerEnter: () => setHeld(sorted.map(diffRowId)), onPointerLeave: () => setHeld(null), onKeyDown: onKeyDown, children: [caption && _jsx("caption", { className: captionClassName, children: caption }), children(shown)] }) }));
71
+ }
72
+ /** The header row. Put the `<th scope="col">` cells inside. */
73
+ function Header({ className, children }) {
74
+ return _jsx("thead", { children: _jsx("tr", { className: className, children: children }) });
75
+ }
76
+ /** One level. A single tab stop for the table: the active row, or the first. The pointer does not move it. */
77
+ function Row({ row, selected, className, children }) {
78
+ const { activeId, firstId, setActive } = useDiff('Row');
79
+ const id = diffRowId(row);
80
+ const stop = activeId === null ? id === firstId : id === activeId;
81
+ const { 'data-mt': _scope, ...state } = primitiveAttributes({ side: row.side });
82
+ return (_jsx("tr", { className: className, ...state, "data-diff": row.change === 'unchanged' ? undefined : row.change, "data-diff-row": id, "aria-selected": selected === undefined ? undefined : selected, tabIndex: stop ? 0 : -1, onFocus: () => setActive(id), children: children }));
83
+ }
84
+ /** The row header cell: the price as recorded. Pass children to put a control inside. */
85
+ function Price({ row, className, children }) {
86
+ return _jsx("th", { scope: "row", className: className, "data-mt-numeric": "", children: children ?? row.price });
87
+ }
88
+ function QtyA({ row, className }) {
89
+ return _jsx("td", { className: className, "data-mt-numeric": "", children: row.qtyA });
90
+ }
91
+ function QtyB({ row, className }) {
92
+ return _jsx("td", { className: className, "data-mt-numeric": "", children: row.qtyB });
93
+ }
94
+ /** qtyB minus qtyA with its sign shown. */
95
+ function Delta({ row, className }) {
96
+ return _jsx("td", { className: className, "data-mt-numeric": "", children: signedDelta(row.delta) });
97
+ }
98
+ /** The change in words; tokens.css adds the mark (+, −, ±) so it never rests on colour alone. */
99
+ function ChangeMark({ change, className, label }) {
100
+ const attributes = primitiveAttributes(change === 'unchanged' ? {} : { diff: change });
101
+ return _jsx("span", { className: className, ...attributes, children: label ?? DIFF_CHANGE_LABEL[change] });
102
+ }
103
+ export { Root, Header, Row, Price, QtyA, QtyB, Delta, ChangeMark };
@@ -0,0 +1,13 @@
1
+ import { Root, Layer, Toolbar, List, Remove, Undo, Lock, Edit } from './DrawingsParts.js';
2
+ export * from './DrawingsParts.js';
3
+ export declare const Drawings: {
4
+ Root: typeof Root;
5
+ Layer: typeof Layer;
6
+ Toolbar: typeof Toolbar;
7
+ List: typeof List;
8
+ Remove: typeof Remove;
9
+ Undo: typeof Undo;
10
+ Lock: typeof Lock;
11
+ Edit: typeof Edit;
12
+ };
13
+ //# sourceMappingURL=Drawings.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { Root, Layer, Toolbar, List, Remove, Undo, Lock, Edit } from './DrawingsParts.js';
2
+ // The parts live in DrawingsParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `Drawings.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './DrawingsParts.js';
5
+ export const Drawings = { Root, Layer, Toolbar, List, Remove, Undo, Lock, Edit };