@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,387 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { buttonFace } from './accessible-name.js';
5
+ import { useChart } from './ChartParts.js';
6
+ import { chartFrame, priceTicks, tickDecimals } from './core/chart-scale.js';
7
+ import { AXIS_BOTTOM, describeMove, MAIN_PANEL, moveSeries, moveTargets, normalizeLayout, panelBands, panelOf, reconcileLayout } from './core/panels.js';
8
+ import { duplicateIds, resolveSeries } from './core/series.js';
9
+ import { PanelsContext } from './panels-context.js';
10
+ import { SelectionContext } from './selection-context.js';
11
+ const HIDDEN = { position: 'absolute', width: 1, height: 1, margin: -1, padding: 0, overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap', border: 0 };
12
+ /** The layout the `panel` fields of the series ask for: `main` first, then each named panel in the order first seen. */
13
+ function initialLayout(series, named = []) {
14
+ const panels = [{ id: MAIN_PANEL, label: 'Main' }];
15
+ // A band named in `panels` that no series asks for is not made: a band with no series is removed.
16
+ for (const n of named)
17
+ if (n.id !== MAIN_PANEL && series.some((s) => s.panel === n.id))
18
+ panels.push(n);
19
+ const placement = {};
20
+ for (const s of series) {
21
+ if (!s.panel || s.panel === MAIN_PANEL)
22
+ continue;
23
+ if (!panels.some((p) => p.id === s.panel))
24
+ panels.push({ id: s.panel, label: s.panel });
25
+ placement[s.id] = s.panel;
26
+ }
27
+ return { panels, placement };
28
+ }
29
+ const finite = (v) => v !== null && v !== undefined && Number.isFinite(v);
30
+ /** Holds the layout, says each move in a polite live region and tells Chart how tall the first band is. Wrap the Chart in it. */
31
+ function Root({ series: given, panels: named, layout, onLayoutChange, align, children }) {
32
+ // A list that is not a list, or an entry that is not a series, is left out and named in the problems, not thrown at render.
33
+ const series = useMemo(() => (Array.isArray(given) ? given.filter((s) => s !== null && typeof s === 'object' && typeof s.id === 'string') : []), [given]);
34
+ const unusable = given === undefined ? 0 : Array.isArray(given) ? given.length - series.length : -1;
35
+ const [inner, setInner] = useState(() => initialLayout(series, named));
36
+ // Series that come or go after the first render: a new one starts where it asks, a gone one leaves its panel (an empty panel goes). Done while rendering, so there is no frame with the series in the wrong band.
37
+ const [seen, setSeen] = useState(() => series.map((s) => s.id));
38
+ if (layout === undefined && (seen.length !== series.length || series.some((s, i) => s.id !== seen[i]))) {
39
+ setSeen(series.map((s) => s.id));
40
+ setInner((was) => reconcileLayout(was, series, seen, named));
41
+ }
42
+ const current = useMemo(() => normalizeLayout(layout !== undefined ? layout : inner), [layout, inner]);
43
+ const latest = useRef(current);
44
+ latest.current = current;
45
+ const labels = useRef(new Map());
46
+ labels.current = new Map(series.map((s) => [s.id, s.label]));
47
+ const [status, setStatus] = useState('');
48
+ const pending = useRef(null);
49
+ const announce = (now) => {
50
+ const p = pending.current;
51
+ if (!p || panelOf(now, p.id) !== p.panel)
52
+ return;
53
+ pending.current = null;
54
+ const where = now.panels.find((x) => x.id === p.panel);
55
+ setStatus(describeMove(labels.current.get(p.id) ?? p.id, where?.label ?? 'a panel'));
56
+ };
57
+ const move = useCallback((seriesId, destination) => {
58
+ const was = latest.current;
59
+ const next = moveSeries(was, seriesId, destination);
60
+ if (next === was)
61
+ return;
62
+ latest.current = next;
63
+ if (layout === undefined)
64
+ setInner(next);
65
+ onLayoutChange?.(next);
66
+ // A controlled host may decline the move; it is said only once the layout it gives back has the series there.
67
+ pending.current = { id: seriesId, panel: panelOf(next, seriesId) };
68
+ if (layout === undefined)
69
+ announce(next);
70
+ }, [layout, onLayoutChange]);
71
+ useEffect(() => { if (layout !== undefined)
72
+ announce(current); }, [layout, current]);
73
+ // Series placed on the bars once per bars array and series object; a crosshair move re-renders without redoing it.
74
+ const mode = align?.mode;
75
+ const tolerance = align?.toleranceMs;
76
+ // One entry per bars array, rebuilt when its length changes (a host may append bars to the same array).
77
+ const cache = useMemo(() => ({ byBars: new WeakMap() }), [mode, tolerance]);
78
+ const resolve = useCallback((item, bars) => {
79
+ let entry = cache.byBars.get(bars);
80
+ if (!entry || entry.len !== bars.length) {
81
+ entry = { len: bars.length, times: bars.map((b) => b.timestamp), made: new WeakMap() };
82
+ cache.byBars.set(bars, entry);
83
+ }
84
+ let hit = entry.made.get(item);
85
+ if (!hit) {
86
+ hit = resolveSeries(entry.times, item, { mode, toleranceMs: tolerance });
87
+ entry.made.set(item, hit);
88
+ }
89
+ return hit;
90
+ }, [cache, mode, tolerance]);
91
+ const problems = useMemo(() => {
92
+ const out = duplicateIds(series).map((id) => `The series id "${id}" is used more than once. Ids must be unique on a chart.`);
93
+ if (unusable < 0)
94
+ out.push('The series given is not a list, so no series is drawn.');
95
+ else if (unusable > 0)
96
+ out.push(`${unusable} ${unusable === 1 ? 'entry' : 'entries'} in the series list ${unusable === 1 ? 'is' : 'are'} not a series (an object with a string id) and ${unusable === 1 ? 'was' : 'were'} left out.`);
97
+ return out;
98
+ }, [series, unusable]);
99
+ const mainInclude = useCallback((visible, bars) => {
100
+ const out = [];
101
+ for (const s of series) {
102
+ if (panelOf(current, s.id) !== MAIN_PANEL)
103
+ continue;
104
+ const values = resolve(s, bars).values;
105
+ let low = Infinity;
106
+ let high = -Infinity;
107
+ for (let i = visible.first; i <= visible.last; i++) {
108
+ const v = values[i];
109
+ if (finite(v)) {
110
+ if (v < low)
111
+ low = v;
112
+ if (v > high)
113
+ high = v;
114
+ }
115
+ }
116
+ if (low <= high)
117
+ out.push(s.style === 'histogram' ? [Math.min(low, 0), Math.max(high, 0)] : [low, high]); // a histogram's bars start at zero
118
+ }
119
+ return out;
120
+ }, [series, current, resolve]);
121
+ // A series of this Root that is no longer given cannot stay selected. Only ids this Root has had count, so a selection
122
+ // of the chart's own bar series (a Series.Root, registered after the first render) is never touched here.
123
+ const selection = useContext(SelectionContext);
124
+ const selectedId = selection?.selection?.kind === 'series' ? selection.selection.id : null;
125
+ const had = useRef(new Set());
126
+ useEffect(() => {
127
+ const ids = new Set(series.map((s) => s.id));
128
+ if (selection && selectedId !== null && had.current.has(selectedId) && !ids.has(selectedId)) {
129
+ queueMicrotask(() => {
130
+ const now = selection.current();
131
+ if (now && now.kind === 'series' && now.id === selectedId)
132
+ selection.select(null);
133
+ });
134
+ }
135
+ had.current = new Set([...had.current, ...ids]);
136
+ }, [selection, selectedId, series]);
137
+ const value = useMemo(() => ({ series, layout: current, move, mainInclude, resolve, problems }), [series, current, move, mainInclude, resolve, problems]);
138
+ return (_jsxs(PanelsContext.Provider, { value: value, children: [children, _jsx("span", { "data-part": "panels-status", role: "status", "aria-live": "polite", style: HIDDEN, children: status })] }));
139
+ }
140
+ function usePanels(part) {
141
+ const context = useContext(PanelsContext);
142
+ if (!context)
143
+ throw new Error(`Panels.${part} needs a Panels.Root above it.`);
144
+ return context;
145
+ }
146
+ const NO_ITEMS = [];
147
+ /** Words for a bar with no number, from the reason the series gives. */
148
+ const NO_VALUE_WORDS = {
149
+ missing: 'missing (the feed has nothing here)',
150
+ 'market-closed': 'market closed',
151
+ 'not-applicable': 'not applicable',
152
+ halted: 'halted',
153
+ unknown: 'unknown (the source does not say why)',
154
+ 'no-point': 'no point at this bar',
155
+ };
156
+ const noValueWords = (state) => (state && NO_VALUE_WORDS[state]) || 'no value';
157
+ /**
158
+ * One sentence on where a series has no value: how many bars, by reason, and when it stops for good. Empty when every
159
+ * bar has a value, or none does (the problem note says that).
160
+ */
161
+ function gapSummary(values, states, bars) {
162
+ if (!states || bars.length === 0)
163
+ return '';
164
+ let gaps = 0;
165
+ let lastValue = -1;
166
+ const by = {};
167
+ for (let i = 0; i < bars.length; i++) {
168
+ if (finite(values[i])) {
169
+ lastValue = i;
170
+ continue;
171
+ }
172
+ gaps++;
173
+ const k = noValueWords(states[i]);
174
+ by[k] = (by[k] ?? 0) + 1;
175
+ }
176
+ if (gaps === 0 || lastValue < 0)
177
+ return '';
178
+ const parts = Object.entries(by).map(([k, n]) => `${n} ${k}`).join(', ');
179
+ const stops = lastValue < bars.length - 1 ? ` No value after ${new Date(bars[lastValue].timestamp).toISOString()}.` : '';
180
+ return `${gaps} of ${bars.length} bars have no value (${parts}).${stops}`;
181
+ }
182
+ const gapCache = new WeakMap();
183
+ /** The gap sentence, kept per resolved series so a crosshair move does not rescan every bar. */
184
+ function gapsOf(resolved, bars) {
185
+ const hit = gapCache.get(resolved);
186
+ if (hit && hit.len === bars.length)
187
+ return hit.text;
188
+ const text = gapSummary(resolved.values, resolved.states, bars);
189
+ gapCache.set(resolved, { len: bars.length, text });
190
+ return text;
191
+ }
192
+ /** Wide enough for a finger: the transparent hit stroke is --mt-touch-target (44px) across. */
193
+ const HIT_WIDTH = 44;
194
+ const num = (n) => n.toFixed(2);
195
+ /** One path per run of consecutive values. */
196
+ function pathOf(frame, values) {
197
+ let d = '';
198
+ let run = [];
199
+ const flush = () => {
200
+ if (run.length >= 2)
201
+ d += run.map((i, k) => `${k === 0 ? 'M' : 'L'}${num(frame.x(i))},${num(frame.y(values[i]))}`).join('');
202
+ run = [];
203
+ };
204
+ for (let i = frame.first; i <= frame.last; i++) {
205
+ if (finite(values[i]))
206
+ run.push(i);
207
+ else
208
+ flush();
209
+ }
210
+ flush();
211
+ return d;
212
+ }
213
+ /**
214
+ * The bars a band's price scale is taken from: one per bar in view, spanning the values of the series in it (NaN where
215
+ * none), plus zero for a histogram and any reference levels. Only the window is filled, so a long history costs
216
+ * nothing; chartFrame reads nothing outside it.
217
+ */
218
+ function scaleBars(bars, viewport, inBand) {
219
+ const out = new Array(bars.length);
220
+ const from = Math.max(0, Math.floor(viewport.start));
221
+ const to = Math.min(bars.length - 1, Math.ceil(viewport.end) - 1);
222
+ const extras = [];
223
+ for (const { item } of inBand) {
224
+ if (item.style === 'histogram')
225
+ extras.push(0);
226
+ for (const l of Array.isArray(item.levels) ? item.levels : [])
227
+ if (Number.isFinite(l))
228
+ extras.push(l);
229
+ }
230
+ for (let i = from; i <= to; i++) {
231
+ let low = Infinity;
232
+ let high = -Infinity;
233
+ for (const { values } of inBand) {
234
+ const v = values[i];
235
+ if (finite(v)) {
236
+ if (v < low)
237
+ low = v;
238
+ if (v > high)
239
+ high = v;
240
+ }
241
+ }
242
+ const has = low <= high;
243
+ if (has)
244
+ for (const e of extras) {
245
+ if (e < low)
246
+ low = e;
247
+ if (e > high)
248
+ high = e;
249
+ }
250
+ out[i] = { timestamp: bars[i].timestamp, open: has ? low : NaN, high: has ? high : NaN, low: has ? low : NaN, close: has ? high : NaN, volume: 0 };
251
+ }
252
+ return out;
253
+ }
254
+ /** The fixed range a band asks for: the widest of the `range`s of its series, or null to follow the data. */
255
+ function fixedRange(inBand) {
256
+ let low = Infinity;
257
+ let high = -Infinity;
258
+ for (const { item } of inBand) {
259
+ const r = item.range;
260
+ if (r && Number.isFinite(r[0]) && Number.isFinite(r[1]) && r[0] < r[1]) {
261
+ if (r[0] < low)
262
+ low = r[0];
263
+ if (r[1] > high)
264
+ high = r[1];
265
+ }
266
+ }
267
+ return low < high ? [low, high] : null;
268
+ }
269
+ function useSeriesSelection(id, label) {
270
+ const selection = useContext(SelectionContext);
271
+ const setName = selection?.setName;
272
+ useEffect(() => { if (setName)
273
+ setName('series', id, label); }, [setName, id, label]);
274
+ const sel = selection?.selection;
275
+ return { selectable: !!selection, selected: !!sel && sel.kind === 'series' && sel.id === id };
276
+ }
277
+ /** Runs of one bar with a value on both sides empty: a line cannot show them, so they are drawn as dots. */
278
+ function loneIndexes(frame, values) {
279
+ const out = [];
280
+ for (let i = frame.first; i <= frame.last && out.length < 300; i++) {
281
+ if (finite(values[i]) && !finite(values[i - 1]) && !finite(values[i + 1]))
282
+ out.push(i);
283
+ }
284
+ return out;
285
+ }
286
+ /** A bar from zero to the value, for each bar in view. */
287
+ function histogramPath(frame, values) {
288
+ const half = Math.max(0.5, frame.barWidth * 0.35);
289
+ const zero = frame.y(0);
290
+ let d = '';
291
+ for (let i = frame.first; i <= frame.last; i++) {
292
+ const v = values[i];
293
+ if (finite(v))
294
+ d += `M${num(frame.x(i) - half)},${num(zero)}V${num(frame.y(v))}h${num(half * 2)}V${num(zero)}Z`;
295
+ }
296
+ return d;
297
+ }
298
+ function SeriesPaths({ item, values, frame, clip }) {
299
+ const { selectable, selected } = useSeriesSelection(item.id, item.label);
300
+ const histogram = item.style === 'histogram';
301
+ const d = histogram ? histogramPath(frame, values) : pathOf(frame, values);
302
+ const lone = histogram ? [] : loneIndexes(frame, values);
303
+ if (!d && lone.length === 0)
304
+ return null;
305
+ return (_jsxs(_Fragment, { children: [d && _jsx("path", { "data-part": "panel-series", "data-series": item.id, "data-style": histogram ? 'histogram' : undefined, "data-state": selected ? 'selected' : undefined, clipPath: clip, d: d, fill: histogram ? 'currentColor' : 'none', "aria-hidden": "true" }), lone.map((i) => _jsx("circle", { "data-part": "panel-point", "data-series": item.id, "data-state": selected ? 'selected' : undefined, clipPath: clip, cx: num(frame.x(i)), cy: num(frame.y(values[i])), r: 3, fill: "currentColor", "aria-hidden": "true" }, i)), selectable && _jsx("path", { "data-part": "series-hit", "data-select-id": item.id, clipPath: clip, d: d + lone.map((i) => `M${num(frame.x(i))},${num(frame.y(values[i]))}h0`).join(''), fill: "none", stroke: "transparent", strokeWidth: HIT_WIDTH, strokeLinecap: "round", strokeLinejoin: "round", pointerEvents: "stroke", "aria-hidden": "true" })] }));
306
+ }
307
+ function Band({ spec, top, height, items, bars }) {
308
+ const { frame: main, viewport, active, readoutId } = useChart('Panels.Layer');
309
+ const { resolve } = usePanels('Layer');
310
+ const placed = useMemo(() => items.map((item) => ({ item, values: resolve(item, bars).values })), [items, bars, resolve]);
311
+ const frame = useMemo(() => chartFrame(scaleBars(bars, viewport, placed), viewport, { width: main.width, height }, { volumeShare: 0, axisBottom: 0, priceRange: fixedRange(placed) }), [bars, bars.length, placed, viewport, main.width, height]);
312
+ const levels = useMemo(() => [...new Set(items.flatMap((it) => (Array.isArray(it.levels) ? it.levels : []).filter(Number.isFinite)))], [items]);
313
+ const ticks = priceTicks(frame.priceMin, frame.priceMax, 3);
314
+ const places = tickDecimals(ticks);
315
+ const clip = `${readoutId}-panel-${spec.id}`;
316
+ return (_jsxs("g", { "data-part": "panel", "data-panel": spec.id, transform: `translate(0,${top})`, children: [_jsx("defs", { children: _jsx("clipPath", { id: clip, children: _jsx("rect", { x: 0, y: 0, width: frame.plotWidth, height: height }) }) }), _jsx("line", { "data-part": "panel-divider", x1: 0, x2: main.width, y1: 0, y2: 0, "aria-hidden": "true" }), _jsx("g", { "data-part": "panel-grid", "aria-hidden": "true", clipPath: `url(#${clip})`, children: ticks.map((t) => _jsx("line", { x1: 0, x2: frame.plotWidth, y1: frame.y(t), y2: frame.y(t) }, t)) }), _jsx("g", { "data-part": "panel-axis", "aria-hidden": "true", style: { pointerEvents: 'none' }, children: ticks.map((t) => _jsx("text", { x: frame.plotWidth + 6, y: frame.y(t) + 4, textAnchor: "start", children: t.toFixed(places) }, t)) }), levels.map((l) => frame.y(l) >= 0 && frame.y(l) <= height && _jsx("line", { "data-part": "panel-level", x1: 0, x2: frame.plotWidth, y1: frame.y(l), y2: frame.y(l), "aria-hidden": "true" }, `level-${l}`)), _jsx("text", { "data-part": "panel-label", x: 6, y: 14, "aria-hidden": "true", style: { pointerEvents: 'none' }, children: spec.label }), active !== null && _jsx("line", { "data-part": "panel-crosshair", x1: frame.x(active), x2: frame.x(active), y1: 0, y2: height, "aria-hidden": "true", style: { pointerEvents: 'none' } }), placed.map(({ item, values }) => _jsx(SeriesPaths, { item: item, values: values, frame: frame, clip: `url(#${clip})` }, item.id))] }));
317
+ }
318
+ /**
319
+ * Draws the series in their bands. Put it inside `Chart.Plot`, after the chart's own parts. The series in `main` use the
320
+ * chart's price scale (and keep themselves in view); every other band has its own scale, grid, price labels and
321
+ * a divider. With a Selection.Root above, a tap on a series line selects it. A band is never shorter than 40, so give the
322
+ * chart enough height for the bands you use.
323
+ */
324
+ function Layer() {
325
+ const { series, layout, resolve } = usePanels('Layer');
326
+ const { bars, frame, totalHeight, readoutId } = useChart('Panels.Layer');
327
+ const bands = useMemo(() => panelBands(layout.panels, totalHeight - AXIS_BOTTOM), [layout.panels, totalHeight]);
328
+ // One array per panel, kept between renders (a crosshair move re-renders but changes neither the series nor the layout).
329
+ const byPanel = useMemo(() => {
330
+ const map = new Map();
331
+ for (const p of layout.panels)
332
+ map.set(p.id, []);
333
+ for (const s of series)
334
+ map.get(panelOf(layout, s.id))?.push(s);
335
+ return map;
336
+ }, [series, layout]);
337
+ const inPanel = (id) => byPanel.get(id) ?? NO_ITEMS;
338
+ return (_jsxs("g", { "data-part": "panels", "aria-hidden": "true", children: [_jsx("g", { "data-part": "panel", "data-panel": MAIN_PANEL, clipPath: `url(#${readoutId}-price)`, children: inPanel(MAIN_PANEL).map((item) => _jsx(SeriesPaths, { item: item, values: resolve(item, bars).values, frame: frame, clip: `url(#${readoutId}-price)` }, item.id)) }), layout.panels.slice(1).map((spec, i) => {
339
+ const band = bands[i + 1];
340
+ return band ? _jsx(Band, { spec: spec, top: band.top, height: band.height, items: inPanel(spec.id), bars: bars }, spec.id) : null;
341
+ })] }));
342
+ }
343
+ /**
344
+ * The series as text: each one's name, its panel, and its value at the crosshair bar (the last bar in view when there is
345
+ * no crosshair), so the numbers are not only a picture. With a Selection.Root above, each row has a button that selects
346
+ * the series. Put it outside the SVG, inside `Chart.Root`.
347
+ */
348
+ function List({ className, format }) {
349
+ const { series, layout, resolve, problems: setProblems } = usePanels('List');
350
+ const { active, frame, decimals, bars } = useChart('Panels.List');
351
+ const selection = useContext(SelectionContext);
352
+ const at = active ?? frame.last;
353
+ const f = format ?? ((n) => n.toFixed(decimals));
354
+ if (series.length === 0 && setProblems.length === 0)
355
+ return null;
356
+ return (_jsxs(_Fragment, { children: [setProblems.map((p) => _jsx("p", { "data-part": "panel-problem", role: "note", children: p }, p)), _jsx("ul", { className: className, "data-part": "panel-list", "aria-label": "Series", children: series.map((s) => {
357
+ const here = layout.panels.find((p) => p.id === panelOf(layout, s.id));
358
+ const resolved = resolve(s, bars);
359
+ const v = resolved.values[at];
360
+ const selected = !!selection?.selection && selection.selection.kind === 'series' && selection.selection.id === s.id;
361
+ const face = buttonFace(s.label, {});
362
+ return (_jsxs("li", { "data-part": "panel-list-item", "data-series": s.id, "data-state": selected ? 'selected' : undefined, children: [selection
363
+ ? _jsx("button", { type: "button", "data-mt": "", "data-touch-target": "", "data-part": "panel-select", "data-state": selected ? 'selected' : undefined, "aria-pressed": selected, "aria-label": face['aria-label'], onClick: () => selection.select(selected ? null : { kind: 'series', id: s.id }), children: face.content })
364
+ : _jsx("span", { "data-part": "panel-name", children: s.label }), _jsxs("span", { "data-part": "panel-where", children: [" ", here?.label ?? 'Main'] }), _jsxs("span", { "data-part": "panel-value", "data-mt-numeric": "", children: [" ", finite(v) ? f(v) : noValueWords(resolved.states?.[at])] }), gapsOf(resolved, bars) && _jsxs("span", { "data-part": "panel-gaps", children: [" ", gapsOf(resolved, bars)] }), resolved.problems.map((p) => _jsxs("span", { "data-part": "panel-problem", role: "note", children: [" ", p] }, p))] }, s.id));
365
+ }) })] }));
366
+ }
367
+ /**
368
+ * The way to move a series to another panel without dragging: buttons for where it can go, relative to where it is
369
+ * (up, down, a new panel above, a new panel below). It shows while a series of the Root is selected (or for `seriesId`)
370
+ * and says each move in the live region. After a move the focus goes to the group, so a keyboard or screen reader user
371
+ * is not left on a button that is gone.
372
+ */
373
+ function Move({ className, seriesId }) {
374
+ const { series, layout, move } = usePanels('Move');
375
+ const selection = useContext(SelectionContext);
376
+ const id = seriesId ?? (selection?.selection?.kind === 'series' ? selection.selection.id : undefined);
377
+ const item = id === undefined ? undefined : series.find((s) => s.id === id);
378
+ const group = useRef(null);
379
+ const [moved, setMoved] = useState(0);
380
+ useEffect(() => { if (moved > 0)
381
+ group.current?.focus(); }, [moved]);
382
+ if (!item)
383
+ return null;
384
+ const targets = moveTargets(layout, item.id);
385
+ return (_jsx("div", { ref: group, className: className, role: "group", tabIndex: -1, "aria-label": `Move ${item.label}`, "data-mt": "", "data-part": "panel-move", "data-series": item.id, children: targets.map((t) => (_jsx("button", { type: "button", "data-mt": "", "data-touch-target": "", "data-part": "panel-move-to", onClick: () => { move(item.id, t.destination); setMoved((n) => n + 1); }, children: t.label }, t.destination.kind === 'panel' ? `to:${t.destination.panelId}` : `new:${t.destination.side}`))) }));
386
+ }
387
+ export { Root, Layer, List, Move, usePanels };
@@ -0,0 +1,11 @@
1
+ import { Root, Field, Hash, CopyButton, ShareNote, Verdict } from './ReceiptParts.js';
2
+ export * from './ReceiptParts.js';
3
+ export declare const Receipt: {
4
+ Root: typeof Root;
5
+ Field: typeof Field;
6
+ Hash: typeof Hash;
7
+ CopyButton: typeof CopyButton;
8
+ ShareNote: typeof ShareNote;
9
+ Verdict: typeof Verdict;
10
+ };
11
+ //# sourceMappingURL=Receipt.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { Root, Field, Hash, CopyButton, ShareNote, Verdict } from './ReceiptParts.js';
2
+ // The parts live in ReceiptParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `Receipt.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './ReceiptParts.js';
5
+ export const Receipt = { Root, Field, Hash, CopyButton, ShareNote, Verdict };
@@ -0,0 +1,45 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ReceiptData, type ReceiptHashOf } from './core/receipt-rules.js';
3
+ export interface ReceiptRootProps {
4
+ receipt: ReceiptData;
5
+ /** The whole receipt as text, for the Copy receipt button. The host owns the wording. */
6
+ receiptText: string;
7
+ /** Writes text to the clipboard; the default is `navigator.clipboard.writeText`. Rejecting means it could not copy. */
8
+ onCopy?: (text: string) => Promise<void>;
9
+ className?: string;
10
+ 'aria-label'?: string;
11
+ children?: ReactNode;
12
+ }
13
+ declare function Root({ receipt, receiptText, onCopy, className, 'aria-label': label, children }: ReceiptRootProps): import("react").JSX.Element;
14
+ /** One labelled fact: a `<dt>` and a `<dd>` for a `<dl>`. */
15
+ declare function Field({ label, children }: {
16
+ label: string;
17
+ children: ReactNode;
18
+ }): import("react").JSX.Element;
19
+ /** A digest as recorded, in a focusable `<code>` so a keyboard user can land on it and press Enter or `y`. */
20
+ declare function Hash({ of, className }: {
21
+ of: ReceiptHashOf;
22
+ className?: string;
23
+ }): import("react").JSX.Element;
24
+ /**
25
+ * A button that copies the whole receipt (or just the result hash), then says whether it worked. Only the button that
26
+ * copied shows the status; a copy by Enter or y shows under the hash button, or the receipt button when there is none.
27
+ */
28
+ declare function CopyButton({ what, className, statusClassName, children }: {
29
+ what?: 'receipt' | 'hash';
30
+ className?: string;
31
+ statusClassName?: string;
32
+ children?: ReactNode;
33
+ }): import("react").JSX.Element;
34
+ /** The line that must travel with a copy of non-synthetic data; nothing when there is none. */
35
+ declare function ShareNote({ className }: {
36
+ className?: string;
37
+ }): import("react").JSX.Element | null;
38
+ /** Whether replaying from the start gave the same result, in words. Tokens.css colours a mismatch. */
39
+ declare function Verdict({ className, matchText, mismatchText }: {
40
+ className?: string;
41
+ matchText?: string;
42
+ mismatchText?: string;
43
+ }): import("react").JSX.Element;
44
+ export { Root, Field, Hash, CopyButton, ShareNote, Verdict };
45
+ //# sourceMappingURL=ReceiptParts.d.ts.map
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
4
+ import { Notice } from './Notice.js';
5
+ import { receiptHash, receiptVerdict, resolveReceiptKey } from './core/receipt-rules.js';
6
+ const Context = createContext(null);
7
+ function useReceipt(part) {
8
+ const context = useContext(Context);
9
+ if (!context)
10
+ throw new Error(`Receipt.${part} needs a Receipt.Root above it.`);
11
+ return context;
12
+ }
13
+ function Root({ receipt, receiptText, onCopy, className, 'aria-label': label = 'Evidence receipt', children }) {
14
+ const [status, setStatus] = useState(null);
15
+ const [hashButtons, setHashButtons] = useState(0);
16
+ const addHashButton = useCallback(() => {
17
+ setHashButtons((n) => n + 1);
18
+ return () => setHashButtons((n) => n - 1);
19
+ }, []);
20
+ const copy = useCallback(async (text, what, from) => {
21
+ try {
22
+ await (onCopy ? onCopy(text) : navigator.clipboard.writeText(text));
23
+ setStatus({ text: `${what} copied.`, ok: true, from });
24
+ }
25
+ catch {
26
+ setStatus({ text: 'Could not copy here. Select the text and copy it.', ok: false, from });
27
+ }
28
+ }, [onCopy]);
29
+ const context = useMemo(() => ({ receipt, receiptText, copy, status, hashButtons, addHashButton }), [receipt, receiptText, copy, status, hashButtons, addHashButton]);
30
+ const onKeyDown = (event) => {
31
+ if (resolveReceiptKey(event.nativeEvent, event.target) !== 'copy-hash')
32
+ return;
33
+ event.preventDefault();
34
+ event.stopPropagation(); // Enter and y are not for the page's shortcuts while a receipt is focused
35
+ void copy(receipt.resultHash, 'Result hash', 'key');
36
+ };
37
+ return (_jsx(Context.Provider, { value: context, children: _jsx("section", { className: className, "aria-label": label, "data-synthetic": receipt.synthetic ? '' : undefined, "data-verdict": receiptVerdict(receipt), onKeyDown: onKeyDown, children: children }) }));
38
+ }
39
+ /** One labelled fact: a `<dt>` and a `<dd>` for a `<dl>`. */
40
+ function Field({ label, children }) {
41
+ return (_jsxs(_Fragment, { children: [_jsx("dt", { children: label }), _jsx("dd", { children: children })] }));
42
+ }
43
+ /** A digest as recorded, in a focusable `<code>` so a keyboard user can land on it and press Enter or `y`. */
44
+ function Hash({ of, className }) {
45
+ const { receipt } = useReceipt('Hash');
46
+ return _jsx("code", { className: className, "data-mt-numeric": "", tabIndex: 0, children: receiptHash(receipt, of) });
47
+ }
48
+ /**
49
+ * A button that copies the whole receipt (or just the result hash), then says whether it worked. Only the button that
50
+ * copied shows the status; a copy by Enter or y shows under the hash button, or the receipt button when there is none.
51
+ */
52
+ function CopyButton({ what = 'receipt', className, statusClassName, children }) {
53
+ const { receipt, receiptText, copy, status, hashButtons, addHashButton } = useReceipt('CopyButton');
54
+ useEffect(() => (what === 'hash' ? addHashButton() : undefined), [what, addHashButton]);
55
+ const shown = status !== null && (status.from === what || (status.from === 'key' && (what === 'hash' || hashButtons === 0))) ? status : null;
56
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: className, onClick: () => void (what === 'hash' ? copy(receipt.resultHash, 'Result hash', 'hash') : copy(receiptText, 'Receipt', 'receipt')), children: children ?? (what === 'hash' ? 'Copy result hash' : 'Copy receipt') }), _jsx(Notice, { as: "span", kind: shown === null ? 'info' : shown.ok ? 'ok' : 'error', className: statusClassName, children: shown?.text ?? '' })] }));
57
+ }
58
+ /** The line that must travel with a copy of non-synthetic data; nothing when there is none. */
59
+ function ShareNote({ className }) {
60
+ const { receipt } = useReceipt('ShareNote');
61
+ if (!receipt.shareNote)
62
+ return null;
63
+ return _jsx("p", { className: className, children: receipt.shareNote });
64
+ }
65
+ /** Whether replaying from the start gave the same result, in words. Tokens.css colours a mismatch. */
66
+ function Verdict({ className, matchText = 'Same two states and the same result hash.', mismatchText = 'Did not reproduce the same result.' }) {
67
+ const { receipt } = useReceipt('Verdict');
68
+ const verdict = receiptVerdict(receipt);
69
+ return _jsx("span", { className: className, "data-mt": "", "data-verdict": verdict, children: verdict === 'match' ? matchText : mismatchText });
70
+ }
71
+ export { Root, Field, Hash, CopyButton, ShareNote, Verdict };
@@ -0,0 +1,9 @@
1
+ import { Root, Track, Thumb, Mark } from './ScrubberParts.js';
2
+ export * from './ScrubberParts.js';
3
+ export declare const Scrubber: {
4
+ Root: typeof Root;
5
+ Track: typeof Track;
6
+ Thumb: typeof Thumb;
7
+ Mark: typeof Mark;
8
+ };
9
+ //# sourceMappingURL=Scrubber.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { Root, Track, Thumb, Mark } from './ScrubberParts.js';
2
+ // The parts live in ScrubberParts.js (a client module). The namespace object is built here, outside it, so a server
3
+ // component can write `Scrubber.Root`: a property of an object exported from a client module is not reachable there.
4
+ export * from './ScrubberParts.js';
5
+ export const Scrubber = { Root, Track, Thumb, Mark };
@@ -0,0 +1,74 @@
1
+ import { type KeyboardEvent, type PointerEvent, type ReactNode } from 'react';
2
+ /**
3
+ * Scrubber as parts: Root, Track, Thumb, Mark. Headless: the parts render the elements the evidence
4
+ * ruler always did, add `data-state` / `data-disabled`, and work in fractions of the track. The host owns what a
5
+ * fraction means (a place in its session), the copy, and the look.
6
+ *
7
+ * Root renders no DOM. It owns the preview while a pointer is down, so `useScrubber()` gives a readout the live
8
+ * fraction; nothing is committed until release, and then exactly once through `onCommit`.
9
+ */
10
+ interface ScrubberContext {
11
+ /** Where the thumb is drawn: the preview while dragging, else the committed fraction. */
12
+ fraction: number;
13
+ committedFraction: number;
14
+ dragging: boolean;
15
+ loading: boolean;
16
+ trackHandlers: {
17
+ onPointerDown: (event: PointerEvent<HTMLElement>) => void;
18
+ onPointerMove: (event: PointerEvent<HTMLElement>) => void;
19
+ onPointerUp: (event: PointerEvent<HTMLElement>) => void;
20
+ onPointerCancel: (event: PointerEvent<HTMLElement>) => void;
21
+ };
22
+ onThumbKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
23
+ }
24
+ /** The live scrub state, for a readout that must follow the preview. Throws a plain message outside a `Scrubber.Root`. */
25
+ export declare function useScrubber(): Pick<ScrubberContext, 'fraction' | 'committedFraction' | 'dragging' | 'loading'>;
26
+ export interface ScrubberRootProps {
27
+ /** Where the host's cursor sits on the track, 0 to 1. */
28
+ fraction: number;
29
+ /** Another navigation is running: a new scrub does not start, keys do nothing, and a release does not commit. */
30
+ loading?: boolean;
31
+ onBeginScrub?: () => void;
32
+ onEndScrub?: () => void;
33
+ /** A release (or Home, End, PageUp, PageDown) resolved to this fraction. Called once per gesture. */
34
+ onCommit: (fraction: number) => void;
35
+ /** An arrow key: step one event in this direction. The host decides what "one event" is. */
36
+ onStep: (direction: -1 | 1) => void;
37
+ children?: ReactNode;
38
+ }
39
+ declare function Root({ fraction: committedInput, loading, onBeginScrub, onEndScrub, onCommit, onStep, children }: ScrubberRootProps): import("react").JSX.Element;
40
+ /** The pressable area. Pointer handlers live here; marks and the thumb go inside it. */
41
+ declare function Track({ className, children }: {
42
+ className?: string;
43
+ children?: ReactNode;
44
+ }): import("react").JSX.Element;
45
+ export interface ScrubberThumbProps {
46
+ className?: string;
47
+ /** Accessible name. */
48
+ label?: string;
49
+ /** What the position means in words (the host's readout); announced with the value. */
50
+ valueText?: string;
51
+ /** Ids of elements that describe the thumb, such as the first few gap marks. */
52
+ describedBy?: string;
53
+ }
54
+ /** The one tab stop: `role="slider"`, positioned at the live fraction. Arrows step, Home/End/PageUp/PageDown jump. */
55
+ declare function Thumb({ className, label, valueText, describedBy }: ScrubberThumbProps): import("react").JSX.Element;
56
+ export interface ScrubberMarkProps {
57
+ /** Left edge on the track, 0 to 1. */
58
+ at: number;
59
+ /** Width on the track, 0 to 1; leave out for a point. */
60
+ span?: number;
61
+ /** Accessible name; the mark is an image, not a control, and never a tab stop. */
62
+ label: string;
63
+ /** What kind of mark: lets a host style gaps and checkpoints apart. */
64
+ kind?: string;
65
+ title?: string;
66
+ /** `-1` makes the mark focusable by pointer but never a tab stop; leave out for a plain image. */
67
+ tabIndex?: -1;
68
+ id?: string;
69
+ className?: string;
70
+ }
71
+ /** A position on the track that informs but cannot be activated (a declared gap, a verified checkpoint). */
72
+ declare function Mark({ at, span, label, kind, title, tabIndex, id, className }: ScrubberMarkProps): import("react").JSX.Element;
73
+ export { Root, Track, Thumb, Mark };
74
+ //# sourceMappingURL=ScrubberParts.d.ts.map