@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,10 @@
1
+ import { Root, Input, List, Option, Count } from './SymbolSearchParts.js';
2
+ export * from './SymbolSearchParts.js';
3
+ export declare const SymbolSearch: {
4
+ Root: typeof Root;
5
+ Input: typeof Input;
6
+ List: typeof List;
7
+ Option: typeof Option;
8
+ Count: typeof Count;
9
+ };
10
+ //# sourceMappingURL=SymbolSearch.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { Root, Input, List, Option, Count } from './SymbolSearchParts.js';
2
+ // The parts live in SymbolSearchParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `SymbolSearch.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './SymbolSearchParts.js';
5
+ export const SymbolSearch = { Root, Input, List, Option, Count };
@@ -0,0 +1,39 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type Instrument, type SymbolMatch } from './core/symbol-search.js';
3
+ export interface SymbolSearchRootProps {
4
+ instruments: readonly Instrument[];
5
+ /** Rows shown, 1 to 200; default 20. */
6
+ limit?: number;
7
+ venue?: string;
8
+ kind?: string;
9
+ /** True when the instrument list is synthetic; sets `data-synthetic` on the root. */
10
+ synthetic?: boolean;
11
+ onPick?: (instrument: Instrument) => void;
12
+ className?: string;
13
+ children?: ReactNode;
14
+ }
15
+ declare function Root({ instruments, limit, venue, kind, synthetic, onPick, className, children }: SymbolSearchRootProps): import("react").JSX.Element;
16
+ /** The text box. One tab stop; it owns the keys. */
17
+ declare function Input({ className, label, placeholder }: {
18
+ className?: string;
19
+ label?: string;
20
+ placeholder?: string;
21
+ }): import("react").JSX.Element;
22
+ /** The result list, given the rows to draw. Hidden while closed or empty. */
23
+ declare function List({ className, children }: {
24
+ className?: string;
25
+ children: (rows: SymbolMatch[]) => ReactNode;
26
+ }): import("react").JSX.Element;
27
+ /** One row. `data-matched` says why it is here; `data-venue` keeps the same symbol on two venues apart. */
28
+ declare function Option({ row, index, className, children }: {
29
+ row: SymbolMatch;
30
+ index: number;
31
+ className?: string;
32
+ children?: ReactNode;
33
+ }): import("react").JSX.Element;
34
+ /** The count, announced politely: "3 results", "No instrument matches". Nothing while the box is empty. */
35
+ declare function Count({ className }: {
36
+ className?: string;
37
+ }): import("react").JSX.Element;
38
+ export { Root, Input, List, Option, Count };
39
+ //# sourceMappingURL=SymbolSearchParts.d.ts.map
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useId, useMemo, useState } from 'react';
4
+ import { buildSymbolIndex, nextSymbolIndex, resolveSymbolKey, searchSymbols } from './core/symbol-search.js';
5
+ const Context = createContext(null);
6
+ function useSearch(part) {
7
+ const context = useContext(Context);
8
+ if (!context)
9
+ throw new Error(`SymbolSearch.${part} needs a SymbolSearch.Root above it.`);
10
+ return context;
11
+ }
12
+ function Root({ instruments, limit = 20, venue, kind, synthetic, onPick, className, children }) {
13
+ const index = useMemo(() => buildSymbolIndex(instruments), [instruments]);
14
+ const [query, setQueryState] = useState('');
15
+ const [open, setOpen] = useState(false);
16
+ const [active, setActive] = useState(-1);
17
+ const id = useId();
18
+ const shown = Math.min(200, Math.max(1, Math.floor(limit) || 20));
19
+ const found = useMemo(() => searchSymbols(index, query, { limit: shown + 1, venue, kind }), [index, query, shown, venue, kind]);
20
+ const rows = found.slice(0, shown);
21
+ const more = found.length > shown;
22
+ const setQuery = useCallback((text) => {
23
+ setQueryState(text);
24
+ setOpen(text.trim() !== '');
25
+ setActive(text.trim() === '' ? -1 : 0); // the best match is highlighted, so Enter alone takes it
26
+ }, []);
27
+ const pick = useCallback((i) => {
28
+ const row = rows[i];
29
+ if (!row)
30
+ return;
31
+ setQueryState(row.instrument.symbol);
32
+ setOpen(false);
33
+ setActive(-1);
34
+ onPick?.(row.instrument);
35
+ }, [rows, onPick]);
36
+ const context = useMemo(() => ({
37
+ query, setQuery, open, setOpen, rows, more, active: active >= rows.length ? rows.length - 1 : active, setActive, pick,
38
+ listId: `${id}-list`, countId: `${id}-count`, optionId: (i) => `${id}-opt-${i}`,
39
+ }), [query, setQuery, open, rows, more, active, pick, id]);
40
+ return (_jsx(Context.Provider, { value: context, children: _jsx("div", { className: className, "data-synthetic": synthetic ? '' : undefined, children: children }) }));
41
+ }
42
+ /** The text box. One tab stop; it owns the keys. */
43
+ function Input({ className, label = 'Search instruments', placeholder }) {
44
+ const { query, setQuery, open, setOpen, rows, active, setActive, pick, listId, countId, optionId } = useSearch('Input');
45
+ const onKeyDown = (event) => {
46
+ const action = resolveSymbolKey(event.nativeEvent, { open, hasText: query !== '' });
47
+ if (!action)
48
+ return;
49
+ if (action === 'pick' && rows.length === 0)
50
+ return;
51
+ event.preventDefault();
52
+ event.stopPropagation(); // these keys are not the page's transport shortcuts while the box has focus
53
+ if (action === 'next' || action === 'previous') {
54
+ if (!open && query.trim() !== '')
55
+ setOpen(true);
56
+ setActive(nextSymbolIndex(rows.length, active, action));
57
+ }
58
+ else if (action === 'pick')
59
+ pick(Math.max(0, active));
60
+ else if (action === 'close')
61
+ setOpen(false);
62
+ else
63
+ setQuery('');
64
+ };
65
+ return (_jsx("input", { className: className, type: "text", role: "combobox", "aria-label": label, "aria-autocomplete": "list", "aria-expanded": open && rows.length > 0, "aria-controls": listId, "aria-describedby": countId, "aria-activedescendant": open && active >= 0 && active < rows.length ? optionId(active) : undefined, autoComplete: "off", autoCapitalize: "characters", spellCheck: false, placeholder: placeholder, value: query, onChange: (event) => setQuery(event.target.value), onBlur: () => setOpen(false), onKeyDown: onKeyDown }));
66
+ }
67
+ /** The result list, given the rows to draw. Hidden while closed or empty. */
68
+ function List({ className, children }) {
69
+ const { rows, open, listId } = useSearch('List');
70
+ return (_jsx("ul", { id: listId, className: className, role: "listbox", "aria-label": "Matching instruments", hidden: !open || rows.length === 0, children: open ? children(rows) : null }));
71
+ }
72
+ /** One row. `data-matched` says why it is here; `data-venue` keeps the same symbol on two venues apart. */
73
+ function Option({ row, index, className, children }) {
74
+ const { active, pick, optionId } = useSearch('Option');
75
+ const { instrument } = row;
76
+ return (_jsx("li", { id: optionId(index), className: className, role: "option", "aria-selected": index === active, "data-matched": row.matchedOn, "data-venue": instrument.venue, onMouseDown: (event) => event.preventDefault(), onClick: () => pick(index), children: children ?? (_jsxs(_Fragment, { children: [_jsx("span", { "data-mt-numeric": "", children: instrument.symbol }), " ", _jsx("span", { children: instrument.venue }), instrument.name && _jsxs(_Fragment, { children: [" ", _jsx("span", { children: instrument.name })] }), row.matchedOn !== 'symbol' && _jsxs(_Fragment, { children: [" ", _jsxs("span", { children: ["(matched ", row.matchedOn, ")"] })] })] })) }));
77
+ }
78
+ /** The count, announced politely: "3 results", "No instrument matches". Nothing while the box is empty. */
79
+ function Count({ className }) {
80
+ const { query, rows, more, countId } = useSearch('Count');
81
+ const text = query.trim() === '' ? ''
82
+ : rows.length === 0 ? `No instrument matches “${query.trim()}”.`
83
+ : `${rows.length}${more ? '+' : ''} ${rows.length === 1 && !more ? 'result' : 'results'}${more ? ', first rows shown' : ''}.`;
84
+ return _jsx("output", { id: countId, className: className, "aria-live": "polite", children: text });
85
+ }
86
+ export { Root, Input, List, Option, Count };
package/dist/Tape.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { Root, Table, Header, Row, Time, Price, Size, Side, Summary, Status } from './TapeParts.js';
2
+ export * from './TapeParts.js';
3
+ export declare const Tape: {
4
+ Root: typeof Root;
5
+ Table: typeof Table;
6
+ Header: typeof Header;
7
+ Row: typeof Row;
8
+ Time: typeof Time;
9
+ Price: typeof Price;
10
+ Size: typeof Size;
11
+ Side: typeof Side;
12
+ Summary: typeof Summary;
13
+ Status: typeof Status;
14
+ };
15
+ //# sourceMappingURL=Tape.d.ts.map
package/dist/Tape.js ADDED
@@ -0,0 +1,5 @@
1
+ import { Root, Table, Header, Row, Time, Price, Size, Side, Summary, Status } from './TapeParts.js';
2
+ // The parts live in TapeParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `Tape.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './TapeParts.js';
5
+ export const Tape = { Root, Table, Header, Row, Time, Price, Size, Side, Summary, Status };
@@ -0,0 +1,69 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type TapeIssue, type TapeRow, type TapeTrade } from './core/tape.js';
3
+ export interface TapeRootProps {
4
+ /** The trades, oldest first. Anything that breaks a rule is reported by `Tape.Status` and left out. */
5
+ trades: readonly TapeTrade[];
6
+ /** How many of the newest rows to show. Default 50. */
7
+ limit?: number;
8
+ /** How often the summary line may change, in milliseconds. Default 5000. */
9
+ summaryEveryMs?: number;
10
+ /** Enter or Space on a focused row. */
11
+ onSelect?: (row: TapeRow) => void;
12
+ className?: string;
13
+ /** The parts, given the rows to show (newest first, held while the pointer or focus is on the tape). */
14
+ children: (rows: TapeRow[]) => ReactNode;
15
+ }
16
+ declare function Root({ trades, limit, summaryEveryMs, onSelect, className, children }: TapeRootProps): import("react").JSX.Element;
17
+ /** The table. Give it a caption through `caption`; the rows go inside `tbody`. */
18
+ declare function Table({ caption, className, captionClassName, children }: {
19
+ caption: string;
20
+ className?: string;
21
+ captionClassName?: string;
22
+ children: ReactNode;
23
+ }): import("react").JSX.Element;
24
+ /** The header row. Put the `<th scope="col">` cells inside. */
25
+ declare function Header({ className, children }: {
26
+ className?: string;
27
+ children: ReactNode;
28
+ }): import("react").JSX.Element;
29
+ /** One trade. A single tab stop for the tape: the active row, or the newest. Holding the pointer does not move it. */
30
+ declare function Row({ row, selected, className, children }: {
31
+ row: TapeRow;
32
+ selected?: boolean;
33
+ className?: string;
34
+ children: ReactNode;
35
+ }): import("react").JSX.Element;
36
+ /** The time as recorded, UTC. */
37
+ declare function Time({ row, className }: {
38
+ row: TapeRow;
39
+ className?: string;
40
+ }): import("react").JSX.Element;
41
+ declare function Price({ row, className }: {
42
+ row: TapeRow;
43
+ className?: string;
44
+ }): import("react").JSX.Element;
45
+ declare function Size({ row, className }: {
46
+ row: TapeRow;
47
+ className?: string;
48
+ }): import("react").JSX.Element;
49
+ /** The side in words; tokens.css adds the mark so it never rests on colour alone. An unknown side has no colour. */
50
+ declare function Side({ row, className, label }: {
51
+ row: TapeRow;
52
+ className?: string;
53
+ label?: string;
54
+ }): import("react").JSX.Element;
55
+ /** The polite live line: a summary of the tape on a timer. Render it once, outside the table. */
56
+ declare function Summary({ className }: {
57
+ className?: string;
58
+ }): import("react").JSX.Element;
59
+ /** What the tape is doing that the rows do not say: paused with N new waiting, and rows left out for breaking a rule. */
60
+ declare function Status({ className, children }: {
61
+ className?: string;
62
+ children?: (state: {
63
+ paused: boolean;
64
+ waiting: number;
65
+ issues: TapeIssue[];
66
+ }) => ReactNode;
67
+ }): import("react").JSX.Element | null;
68
+ export { Root, Table, Header, Row, Time, Price, Size, Side, Summary, Status };
69
+ //# sourceMappingURL=TapeParts.d.ts.map
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { TAPE_SIDE_LABEL, holdTape, newestFirst, nextTapeIndex, readTape, resolveTapeKey, tapeSummary } from './core/tape.js';
5
+ const Context = createContext(null);
6
+ function useTape(part) {
7
+ const context = useContext(Context);
8
+ if (!context)
9
+ throw new Error(`Tape.${part} needs a Tape.Root above it.`);
10
+ return context;
11
+ }
12
+ function Root({ trades, limit = 50, summaryEveryMs = 5000, onSelect, className, children }) {
13
+ const read = useMemo(() => readTape(trades), [trades]);
14
+ const [pointer, setPointer] = useState(false);
15
+ const [focus, setFocus] = useState(false);
16
+ const [held, setHeld] = useState(null);
17
+ const [activeId, setActiveId] = useState(null);
18
+ const root = useRef(null);
19
+ const live = useMemo(() => newestFirst(read.rows).slice(0, Math.max(0, limit)), [read.rows, limit]);
20
+ const paused = pointer || focus;
21
+ const holding = paused && held ? holdTape(held, read.rows) : null;
22
+ const shown = holding ? holding.rows : live;
23
+ const waiting = holding ? holding.waiting : 0;
24
+ const summary = useMemo(() => tapeSummary(read.rows), [read.rows]);
25
+ const firstId = shown.length ? shown[0].id : null;
26
+ // If the row last focused has left the shown rows, the tab stop goes back to the newest row.
27
+ const stopId = activeId !== null && shown.some((r) => r.id === activeId) ? activeId : null;
28
+ const freeze = () => setHeld((current) => current ?? live.map((r) => r.id));
29
+ const release = (next) => { if (!next)
30
+ setHeld(null); };
31
+ const focusRow = (id) => {
32
+ setActiveId(id);
33
+ for (const tr of root.current?.querySelectorAll('tr[data-tape-row]') ?? []) {
34
+ if (tr.dataset.tapeRow === id) {
35
+ tr.focus();
36
+ return;
37
+ }
38
+ }
39
+ };
40
+ const onKeyDown = (event) => {
41
+ const action = resolveTapeKey(event.nativeEvent);
42
+ if (!action)
43
+ return;
44
+ const onRow = event.target.closest('tr[data-tape-row]');
45
+ if (!onRow)
46
+ return;
47
+ if (action.type === 'select') {
48
+ if (event.target !== onRow)
49
+ return; // a control inside the row keeps its own Enter and Space
50
+ const row = shown.find((r) => r.id === onRow.dataset.tapeRow);
51
+ if (!row || !onSelect)
52
+ return;
53
+ event.preventDefault();
54
+ onSelect(row);
55
+ return;
56
+ }
57
+ event.preventDefault();
58
+ event.stopPropagation(); // these keys are also page shortcuts
59
+ const index = shown.findIndex((r) => r.id === onRow.dataset.tapeRow);
60
+ const to = nextTapeIndex(shown.length, index, action.to);
61
+ if (to >= 0)
62
+ focusRow(shown[to].id);
63
+ };
64
+ const onBlur = (event) => {
65
+ if (event.currentTarget.contains(event.relatedTarget))
66
+ return;
67
+ setFocus(false);
68
+ release(pointer);
69
+ };
70
+ // A focused row that the host's window drops leaves the page without a blur event, so let go of the hold here.
71
+ useEffect(() => {
72
+ if (focus && root.current && !root.current.contains(document.activeElement)) {
73
+ setFocus(false);
74
+ if (!pointer)
75
+ setHeld(null);
76
+ }
77
+ }, [shown, focus, pointer]);
78
+ // The summary changes on a timer, and not at all while someone is reading rows.
79
+ const latest = useRef(summary);
80
+ latest.current = summary;
81
+ const pausedRef = useRef(paused);
82
+ pausedRef.current = paused;
83
+ const [announced, setAnnounced] = useState(summary);
84
+ useEffect(() => {
85
+ const id = setInterval(() => { if (!pausedRef.current)
86
+ setAnnounced(latest.current); }, Math.max(1000, summaryEveryMs));
87
+ return () => clearInterval(id);
88
+ }, [summaryEveryMs]);
89
+ const context = useMemo(() => ({ issues: read.issues, paused, waiting, summary: announced, activeId: stopId, firstId, setActive: setActiveId }), [read.issues, paused, waiting, announced, stopId, firstId]);
90
+ return (_jsx(Context.Provider, { value: context, children: _jsx("div", { ref: root, className: className, "data-mt": "", "data-paused": paused ? '' : undefined, onPointerEnter: () => { setPointer(true); freeze(); }, onPointerLeave: () => { setPointer(false); release(focus); }, onFocus: () => { setFocus(true); freeze(); }, onBlur: onBlur, onKeyDown: onKeyDown, children: children(shown) }) }));
91
+ }
92
+ /** The table. Give it a caption through `caption`; the rows go inside `tbody`. */
93
+ function Table({ caption, className, captionClassName, children }) {
94
+ return (_jsxs("table", { className: className, children: [_jsx("caption", { className: captionClassName, children: caption }), children] }));
95
+ }
96
+ /** The header row. Put the `<th scope="col">` cells inside. */
97
+ function Header({ className, children }) {
98
+ return _jsx("thead", { children: _jsx("tr", { className: className, children: children }) });
99
+ }
100
+ /** One trade. A single tab stop for the tape: the active row, or the newest. Holding the pointer does not move it. */
101
+ function Row({ row, selected, className, children }) {
102
+ const { activeId, firstId, setActive } = useTape('Row');
103
+ const stop = activeId === null ? row.id === firstId : row.id === activeId;
104
+ return (_jsx("tr", { className: className, "data-tape-row": row.id, "aria-selected": selected === undefined ? undefined : selected, tabIndex: stop ? 0 : -1, onFocus: () => setActive(row.id), children: children }));
105
+ }
106
+ /** The time as recorded, UTC. */
107
+ function Time({ row, className }) {
108
+ return _jsx("th", { scope: "row", className: className, "data-mt-numeric": "", children: _jsx("time", { dateTime: row.t, children: row.time }) });
109
+ }
110
+ function Price({ row, className }) {
111
+ return _jsx("td", { className: className, "data-mt-numeric": "", children: row.price });
112
+ }
113
+ function Size({ row, className }) {
114
+ return _jsx("td", { className: className, "data-mt-numeric": "", children: row.size });
115
+ }
116
+ /** The side in words; tokens.css adds the mark so it never rests on colour alone. An unknown side has no colour. */
117
+ function Side({ row, className, label }) {
118
+ return _jsx("td", { className: className, "data-mt": "", "data-tape-side": row.side === 'unknown' ? undefined : row.side, title: row.side === 'unknown' ? 'The feed did not say which side was the aggressor' : undefined, children: label ?? TAPE_SIDE_LABEL[row.side] });
119
+ }
120
+ /** The polite live line: a summary of the tape on a timer. Render it once, outside the table. */
121
+ function Summary({ className }) {
122
+ const { summary } = useTape('Summary');
123
+ return _jsx("p", { className: className, role: "status", "aria-live": "polite", "aria-atomic": "true", children: summary });
124
+ }
125
+ /** What the tape is doing that the rows do not say: paused with N new waiting, and rows left out for breaking a rule. */
126
+ function Status({ className, children }) {
127
+ const { paused, waiting, issues } = useTape('Status');
128
+ if (children)
129
+ return _jsx("div", { className: className, children: children({ paused, waiting, issues }) });
130
+ const parts = [];
131
+ if (paused)
132
+ parts.push(waiting > 0 ? `Paused. ${waiting} new ${waiting === 1 ? 'trade' : 'trades'} waiting.` : 'Paused while you read.');
133
+ if (issues.length)
134
+ parts.push(`${issues.length} ${issues.length === 1 ? 'row' : 'rows'} left out: ${issues.slice(0, 3).map((i) => `row ${i.index + 1}, ${i.message}`).join('; ')}${issues.length > 3 ? '; …' : ''}.`);
135
+ if (!parts.length)
136
+ return null;
137
+ return _jsx("p", { className: className, "data-paused": paused ? '' : undefined, children: parts.join(' ') });
138
+ }
139
+ export { Root, Table, Header, Row, Time, Price, Size, Side, Summary, Status };
@@ -0,0 +1,8 @@
1
+ import { Root, Button, Separator } from './ToolbarParts.js';
2
+ export * from './ToolbarParts.js';
3
+ export declare const Toolbar: {
4
+ Root: typeof Root;
5
+ Button: typeof Button;
6
+ Separator: typeof Separator;
7
+ };
8
+ //# sourceMappingURL=Toolbar.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { Root, Button, Separator } from './ToolbarParts.js';
2
+ // The parts live in ToolbarParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `Toolbar.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './ToolbarParts.js';
5
+ export const Toolbar = { Root, Button, Separator };
@@ -0,0 +1,61 @@
1
+ import { type ButtonHTMLAttributes, type ReactNode, type RefObject } from 'react';
2
+ import { type NameProps } from './accessible-name.js';
3
+ /**
4
+ * A toolbar as parts: Root, Button and Separator. Headless: parts add `data-part` and `data-mt`; the host gives them looks.
5
+ *
6
+ * The toolbar is ONE tab stop (roving tabindex). Inside it the arrow keys along its orientation move between the items,
7
+ * Home and End jump to the ends, and Escape sends focus to `returnFocusTo` (the chart, for a drawing toolbar). An item
8
+ * is a Toolbar.Button (a plain button, or a toggle with `pressed`) or a Menu.Trigger.
9
+ */
10
+ type Orientation = 'horizontal' | 'vertical';
11
+ interface ToolbarContext {
12
+ orientation: Orientation;
13
+ stop: string | null;
14
+ setStop: (id: string) => void;
15
+ register: (id: string, element: HTMLElement) => void;
16
+ unregister: (id: string) => void;
17
+ /** Ask the root to re-check which item is the tab stop (an item became enabled or disabled). */
18
+ refresh: () => void;
19
+ }
20
+ /** The toolbar a part sits in, or null outside one. Menu.Trigger uses it to join the roving tab stop. */
21
+ export declare function useToolbarContext(): ToolbarContext | null;
22
+ /** Makes `element` an item of the toolbar above it, if any; returns the tabIndex and focus handler to put on it. */
23
+ export declare function useToolbarItem(element: RefObject<HTMLElement | null>, disabled?: boolean): {
24
+ tabIndex: 0 | -1 | undefined;
25
+ onFocus: (() => void) | undefined;
26
+ };
27
+ type DataAttributes = {
28
+ [attribute: `data-${string}`]: string | number | boolean | undefined;
29
+ };
30
+ export interface ToolbarRootProps extends DataAttributes {
31
+ className?: string;
32
+ /** Names the toolbar. Default "Toolbar"; give it the real name ("Drawing tools"). */
33
+ 'aria-label'?: string;
34
+ /** Which arrow keys move between items. Default 'horizontal' (Left and Right); 'vertical' uses Up and Down. */
35
+ orientation?: Orientation;
36
+ /**
37
+ * Where Escape sends focus: an element ref, or a function that returns the element (it is read when Escape is pressed,
38
+ * so the chart need not exist yet). Without it Escape does nothing here.
39
+ */
40
+ returnFocusTo?: RefObject<HTMLElement | SVGElement | null> | (() => HTMLElement | SVGElement | null);
41
+ children?: ReactNode;
42
+ }
43
+ /** role="toolbar" with one tab stop. Put Toolbar.Button and Menu.Trigger inside it, in any wrapper. */
44
+ declare function Root({ className, 'aria-label': label, orientation, returnFocusTo, children, ...rest }: ToolbarRootProps): import("react").JSX.Element;
45
+ export interface ToolbarButtonProps extends NameProps, DataAttributes, Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
46
+ /** Makes the button a toggle: `aria-pressed` is this value. Leave it out for a plain button. */
47
+ pressed?: boolean;
48
+ }
49
+ /**
50
+ * A button in the toolbar. Visible words are `children`, else `label`. With only an icon, set `hideLabel` and `label`:
51
+ * the label becomes the accessible name. A button with no words at all gets no name, and warns in the console.
52
+ */
53
+ declare function Button({ pressed, icon, label, hideLabel, children, className, onClick, ...rest }: ToolbarButtonProps): import("react").JSX.Element;
54
+ /** A thin divider between groups of items. */
55
+ declare function Separator({ className }: {
56
+ className?: string;
57
+ }): import("react").JSX.Element;
58
+ /** Warns once, in the console, when a control has no accessible name (no text and no aria-label). */
59
+ export declare function useNameCheck(ref: RefObject<HTMLElement | null>, part: string): void;
60
+ export { Root, Button, Separator };
61
+ //# sourceMappingURL=ToolbarParts.d.ts.map
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
4
+ import { buttonFace } from './accessible-name.js';
5
+ const Context = createContext(null);
6
+ /** The toolbar a part sits in, or null outside one. Menu.Trigger uses it to join the roving tab stop. */
7
+ export function useToolbarContext() {
8
+ return useContext(Context);
9
+ }
10
+ /** Makes `element` an item of the toolbar above it, if any; returns the tabIndex and focus handler to put on it. */
11
+ export function useToolbarItem(element, disabled) {
12
+ const toolbar = useContext(Context);
13
+ const id = useId();
14
+ const register = toolbar?.register;
15
+ const unregister = toolbar?.unregister;
16
+ useEffect(() => {
17
+ if (!register || !unregister || !element.current)
18
+ return;
19
+ register(id, element.current);
20
+ return () => unregister(id);
21
+ }, [register, unregister, id, element]);
22
+ // A disabled item cannot be the tab stop: tell the root when that changes, since the root may not re-render on its own.
23
+ const refresh = toolbar?.refresh;
24
+ useEffect(() => { refresh?.(); }, [refresh, disabled]);
25
+ const setStop = toolbar?.setStop;
26
+ const onFocus = useCallback(() => setStop?.(id), [setStop, id]);
27
+ if (!toolbar)
28
+ return { tabIndex: undefined, onFocus: undefined };
29
+ return { tabIndex: toolbar.stop === id ? 0 : -1, onFocus };
30
+ }
31
+ /** role="toolbar" with one tab stop. Put Toolbar.Button and Menu.Trigger inside it, in any wrapper. */
32
+ function Root({ className, 'aria-label': label = 'Toolbar', orientation = 'horizontal', returnFocusTo, children, ...rest }) {
33
+ const items = useRef(new Map());
34
+ const [stop, setStop] = useState(null);
35
+ const [, setVersion] = useState(0);
36
+ const register = useCallback((id, element) => { items.current.set(id, element); setVersion((v) => v + 1); }, []);
37
+ const unregister = useCallback((id) => { items.current.delete(id); setVersion((v) => v + 1); }, []);
38
+ /** The enabled items in document order. */
39
+ const ordered = () => [...items.current.entries()]
40
+ .filter(([, el]) => el.isConnected && !el.disabled)
41
+ .sort(([, a], [, b]) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
42
+ // One item must always be the tab stop: when none is (first render, or the one that was removed), a pressed item is the
43
+ // stop, else the first. This runs after every render because an item can change `pressed` or `disabled` without telling us.
44
+ useEffect(() => {
45
+ const list = ordered();
46
+ if (list.length === 0 || (stop !== null && list.some(([id]) => id === stop)))
47
+ return;
48
+ const pressed = list.find(([, el]) => el.getAttribute('aria-pressed') === 'true');
49
+ setStop((pressed ?? list[0])[0]);
50
+ });
51
+ const onKeyDown = (event) => {
52
+ if (event.altKey || event.ctrlKey || event.metaKey)
53
+ return;
54
+ const list = ordered();
55
+ const at = list.findIndex(([, el]) => el === event.target);
56
+ if (at < 0)
57
+ return; // a key inside an open menu belongs to the menu
58
+ if (event.key === 'Escape') {
59
+ const target = typeof returnFocusTo === 'function' ? returnFocusTo() : returnFocusTo?.current;
60
+ if (!target)
61
+ return;
62
+ event.preventDefault();
63
+ event.stopPropagation();
64
+ target.focus();
65
+ return;
66
+ }
67
+ // A horizontal toolbar mirrors in right-to-left text, as Menu does: the arrow that points ahead visually moves ahead.
68
+ const rtl = orientation !== 'vertical' && getComputedStyle(event.currentTarget).direction === 'rtl';
69
+ const forward = orientation === 'vertical' ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight';
70
+ const back = orientation === 'vertical' ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft';
71
+ let next;
72
+ switch (event.key) {
73
+ case forward:
74
+ next = (at + 1) % list.length;
75
+ break;
76
+ case back:
77
+ next = (at - 1 + list.length) % list.length;
78
+ break;
79
+ case 'Home':
80
+ next = 0;
81
+ break;
82
+ case 'End':
83
+ next = list.length - 1;
84
+ break;
85
+ default: return;
86
+ }
87
+ event.preventDefault();
88
+ event.stopPropagation(); // arrows, Home and End are also transport shortcuts on a replay page
89
+ setStop(list[next][0]);
90
+ list[next][1].focus();
91
+ };
92
+ const refresh = useCallback(() => setVersion((v) => v + 1), []);
93
+ const context = useMemo(() => ({ orientation, stop, setStop, register, unregister, refresh }), [orientation, stop, register, unregister, refresh]);
94
+ return (_jsx(Context.Provider, { value: context, children: _jsx("div", { "data-mt": "", "data-part": "toolbar", ...rest, className: className, role: "toolbar", "aria-label": label, "aria-orientation": orientation, onKeyDown: onKeyDown, children: children }) }));
95
+ }
96
+ /**
97
+ * A button in the toolbar. Visible words are `children`, else `label`. With only an icon, set `hideLabel` and `label`:
98
+ * the label becomes the accessible name. A button with no words at all gets no name, and warns in the console.
99
+ */
100
+ function Button({ pressed, icon, label, hideLabel, children, className, onClick, ...rest }) {
101
+ const ref = useRef(null);
102
+ const item = useToolbarItem(ref, rest.disabled);
103
+ const face = buttonFace(undefined, { icon, label, hideLabel, children, ariaLabel: rest['aria-label'] });
104
+ useNameCheck(ref, 'Toolbar.Button');
105
+ return (_jsx("button", { "data-mt": "", "data-part": "toolbar-button", "data-touch-target": "", ...rest, ref: ref, type: "button", className: className, "aria-label": face['aria-label'], "aria-pressed": pressed, tabIndex: item.tabIndex, onFocus: (event) => { item.onFocus?.(); rest.onFocus?.(event); }, onClick: onClick, children: face.content }));
106
+ }
107
+ /** A thin divider between groups of items. */
108
+ function Separator({ className }) {
109
+ return _jsx("div", { className: className, "data-mt": "", "data-part": "toolbar-separator", role: "separator" });
110
+ }
111
+ /** Warns once, in the console, when a control has no accessible name (no text and no aria-label). */
112
+ export function useNameCheck(ref, part) {
113
+ useEffect(() => {
114
+ const el = ref.current;
115
+ if (!el)
116
+ return;
117
+ const named = (el.textContent ?? '').trim() !== '' || (el.getAttribute('aria-label') ?? '').trim() !== '' || el.hasAttribute('aria-labelledby');
118
+ if (!named)
119
+ console.warn(`${part} has no accessible name: give it children, or a label with hideLabel when it shows only an icon.`);
120
+ }, [ref, part]);
121
+ }
122
+ export { Root, Button, Separator };
@@ -0,0 +1,15 @@
1
+ import { Root, Group, Play, Speed, Readout, Boundary } from './TransportParts.js';
2
+ export * from './TransportParts.js';
3
+ export declare const Transport: {
4
+ Root: typeof Root;
5
+ Group: typeof Group;
6
+ Play: typeof Play;
7
+ Previous: import("./TransportParts.js").StepButton;
8
+ Next: import("./TransportParts.js").StepButton;
9
+ Start: import("./TransportParts.js").StepButton;
10
+ End: import("./TransportParts.js").StepButton;
11
+ Speed: typeof Speed;
12
+ Readout: typeof Readout;
13
+ Boundary: typeof Boundary;
14
+ };
15
+ //# sourceMappingURL=Transport.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { Root, Group, Play, Previous, Next, Start, End, Speed, Readout, Boundary } from './TransportParts.js';
2
+ // The parts live in TransportParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `Transport.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './TransportParts.js';
5
+ export const Transport = { Root, Group, Play, Previous, Next, Start, End, Speed, Readout, Boundary };