@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
package/README.md ADDED
@@ -0,0 +1,636 @@
1
+ # Matchstick Primitives
2
+
3
+ Free building blocks for trading screens, fed by Matchstick replay or by your own data.
4
+
5
+ ![A tape and a price ladder moving together on one made-up feed](https://raw.githubusercontent.com/matchstick-trading/primitives/main/assets/primitives-live.gif)
6
+
7
+ - Try it live: https://ui.matchstick.trading/docs/live (made-up data, no sign-in).
8
+ - Issues are open for feedback: https://github.com/matchstick-trading/primitives/issues
9
+ - Licence: Apache-2.0 (see `LICENSE`).
10
+
11
+ Headless, keyboard-first React parts for trading screens, with a plain-TypeScript core under them. Charts, studies,
12
+ drawings, a volume profile, a replay transport and scrubber, a cursor, a depth profile, a diff table, a symbol search,
13
+ a receipt and a notice. Developer preview, version 0.0.10: names and parts can change.
14
+
15
+ Every demo and example uses made-up (synthetic) data. No market data ships in this package, and the parts place no
16
+ orders: there is no order entry, account or trading feature here.
17
+
18
+ ## Status
19
+
20
+ - Developer preview. Names, parts and data shapes can change, and there is no promise of updates or support.
21
+ - Install: `npm install @matchstick-trading/primitives` (React 18 or 19 is a peer dependency).
22
+ - Licence: Apache-2.0. The source in `src/` ships with the package and you may use it under the terms in `LICENSE`.
23
+ The licence covers this package only, not Matchstick Replay or any other Matchstick product.
24
+
25
+ ## First chart
26
+
27
+ The parts use React state and context. The package already starts its interactive modules with `'use client'`, so you can import and compose them (`Chart.Root`, `Chart.Plot`, ...) in a Next.js App Router server component; props must be serialisable (pass plain data, and put event handlers and render functions in your own client component).
28
+
29
+ The snippets below use synthetic data.
30
+
31
+ ```tsx
32
+ import { Chart } from '@matchstick-trading/primitives';
33
+ import '@matchstick-trading/primitives/tokens.css';
34
+
35
+ // Made-up one-minute bars. Replace with your own list of { timestamp (ms), open, high, low, close, volume }.
36
+ export const bars = Array.from({ length: 60 }, (_, i) => {
37
+ const base = 100 + Math.sin(i / 6) * 2 + i * 0.03;
38
+ return { timestamp: Date.UTC(2026, 0, 7, 19, i), open: base, high: base + 0.4, low: base - 0.4, close: base + (i % 2 ? 0.2 : -0.2), volume: 100 + (i % 7) * 20 };
39
+ });
40
+
41
+ export function FirstChart() {
42
+ return (
43
+ <Chart.Root bars={bars} synthetic aria-label="Synthetic one-minute bars">
44
+ <Chart.Plot>
45
+ <Chart.Grid />
46
+ <Chart.Volume />
47
+ <Chart.Candles style="candle" />
48
+ <Chart.PriceAxis />
49
+ <Chart.TimeAxis />
50
+ <Chart.Crosshair />
51
+ </Chart.Plot>
52
+ <Chart.Readout />
53
+ </Chart.Root>
54
+ );
55
+ }
56
+ ```
57
+
58
+ In a TypeScript project, importing `tokens.css` needs the bundler's asset types (`"types": ["vite/client"]` in
59
+ `tsconfig.json` for Vite), or `tsc` reports TS2882. The chart is 420 px tall by default and as wide as its container;
60
+ set `height` (and optionally `width`) on `Chart.Root` to change that, rather than sizing a wrapper around it.
61
+
62
+ The chart draws SVG and leaves stroke and fill to you, so add this once (it styles `data-part` and `data-change`, not class names):
63
+
64
+ ```css
65
+ [data-part='grid'] line { stroke: #8884; }
66
+ [data-part='candles'] line { stroke: currentColor; }
67
+ [data-part='candles'] rect { fill: currentColor; }
68
+ [data-part='candles'] [data-change='up'] { color: var(--mt-up); }
69
+ [data-part='candles'] [data-change='down'] { color: var(--mt-down); }
70
+ [data-part='candles'] [data-change='flat'] { color: var(--mt-flat); }
71
+ [data-part='candles'] [data-change='up'] rect { fill: none; stroke: currentColor; } /* up bodies hollow, down solid: direction never rests on colour alone */
72
+ [data-part='volume'] [data-change='up'] { fill: color-mix(in srgb, var(--mt-up) 45%, transparent); }
73
+ [data-part='volume'] [data-change='down'], [data-part='volume'] [data-change='flat'] { fill: color-mix(in srgb, var(--mt-down) 45%, transparent); }
74
+ [data-part='price-axis'] text, [data-part='time-axis'] text { fill: currentColor; font: 11px var(--mt-font-numeric); }
75
+ [data-part='crosshair'] line { stroke: currentColor; stroke-dasharray: 3 3; }
76
+ ```
77
+
78
+ The crosshair's price label (`[data-part='crosshair-price']`) already takes the text colour at 11px from `tokens.css`; override it the same way as the axis labels if you want.
79
+
80
+ ### Add a scrubber
81
+
82
+ `Scrubber` is a track you position with CSS: it has no size of its own, so give the track `position: relative` and a
83
+ height, or it is invisible. Marks and the thumb are placed with `left` as a percentage of the track.
84
+
85
+ ```css
86
+ [data-part='scrubber-track'] { position: relative; height: 24px; background: #8882; }
87
+ [data-part='scrubber-mark'] { position: absolute; top: 0; bottom: 0; background: #ff980055; }
88
+ [data-part='scrubber-thumb'] { position: absolute; top: 0; width: 12px; height: 24px; margin-left: -6px; background: currentColor; }
89
+ ```
90
+
91
+ `fraction` is 0 to 1 along the track; the host maps it to whatever it scrubs (for example a bar index) and decides what
92
+ one `onStep` event means.
93
+
94
+ ```tsx
95
+ import { useState } from 'react';
96
+ import { Scrubber } from '@matchstick-trading/primitives';
97
+
98
+ export function ReplayScrubber() {
99
+ const [fraction, setFraction] = useState(0.3);
100
+ return (
101
+ <Scrubber.Root
102
+ fraction={fraction}
103
+ onCommit={setFraction} // once, on release
104
+ onStep={(direction) => setFraction((f) => Math.min(1, Math.max(0, f + direction * 0.05)))} // arrow keys: -1 or 1
105
+ >
106
+ <Scrubber.Track>
107
+ <Scrubber.Mark at={0.5} span={0.1} label="Gap" kind="gap" />
108
+ <Scrubber.Thumb label="Replay position" />
109
+ </Scrubber.Track>
110
+ </Scrubber.Root>
111
+ );
112
+ }
113
+ ```
114
+
115
+ ### Add a volume profile
116
+
117
+ `VolumeProfile.Histogram` goes inside `Chart.Plot` and draws SVG with no fill, so it shows as black blocks until you style it. Put it before the candles so they stay on top:
118
+
119
+ ```css
120
+ [data-part='profile-row'] [data-change='up'] { fill: color-mix(in srgb, var(--mt-up) 35%, transparent); }
121
+ [data-part='profile-row'] [data-change='down'] { fill: color-mix(in srgb, var(--mt-down) 35%, transparent); }
122
+ [data-part='profile-row'][data-poc] rect { stroke: currentColor; }
123
+ [data-part='poc'], [data-part='vah'], [data-part='val'] { stroke: currentColor; stroke-dasharray: 2 3; }
124
+ ```
125
+
126
+ `VolumeProfile.Table` is the same profile as text and also needs a `Chart.Root` above it: place it inside `Chart.Root` after `Chart.Plot` (it is an HTML table, not SVG). Outside `Chart.Root` it throws "Chart VolumeProfile.Table needs a Chart.Root above it."
127
+
128
+ ### Add a depth profile
129
+
130
+ `DepthProfile` draws SVG with no fill or stroke, like the chart. Give it `levels` (each `{ side: 'bid' | 'ask', price, cumulative }`, with price and cumulative as strings) and add this once:
131
+
132
+ ```css
133
+ [data-part='depth-bids'] { fill: #1f77b455; stroke: #1f77b4; }
134
+ [data-part='depth-asks'] { fill: #ff7f0e55; stroke: #ff7f0e; }
135
+ [data-part='depth-axis'] text { fill: currentColor; font: 11px var(--mt-font-numeric); }
136
+ [data-part='depth-crosshair'] line { stroke: currentColor; stroke-dasharray: 3 3; }
137
+ [data-part='depth-crosshair'] circle { fill: currentColor; }
138
+ figure table td, figure table th { padding: 0 0.75rem 0 0; text-align: left; } /* the depth table's cells */
139
+ ```
140
+
141
+ Side is carried by the table and the readout words as well as by colour.
142
+
143
+ `DepthProfile` is a set of parts, not one component, so build it from them:
144
+
145
+ ```tsx
146
+ import { DepthProfile, type DepthLevel } from '@matchstick-trading/primitives';
147
+
148
+ // Best price first on each side: the best bid is the highest, the best ask the lowest.
149
+ const levels: DepthLevel[] = [
150
+ { side: 'bid', price: '99.9', cumulative: '5' },
151
+ { side: 'bid', price: '99.8', cumulative: '12' },
152
+ { side: 'ask', price: '100.1', cumulative: '7' },
153
+ { side: 'ask', price: '100.2', cumulative: '12' },
154
+ ];
155
+
156
+ export function Depth() {
157
+ return (
158
+ <DepthProfile.Root levels={levels}>
159
+ <DepthProfile.Plot>
160
+ <DepthProfile.Bids />
161
+ <DepthProfile.Asks />
162
+ <DepthProfile.Axis />
163
+ <DepthProfile.Crosshair />
164
+ </DepthProfile.Plot>
165
+ <DepthProfile.Readout />
166
+ <DepthProfile.Table />
167
+ </DepthProfile.Root>
168
+ );
169
+ }
170
+ ```
171
+
172
+ Typing the list as `DepthLevel[]` keeps `side` from widening to `string`. Arrow keys on the plot move the crosshair.
173
+
174
+ ### Add replay controls and a notice
175
+
176
+ `Transport` buttons render empty: put the label (or an icon with a name) inside each one. The host owns the handlers.
177
+
178
+ ```tsx
179
+ import { useState } from 'react';
180
+ import { Transport, Notice } from '@matchstick-trading/primitives';
181
+
182
+ export function Controls() {
183
+ const [playing, setPlaying] = useState(false);
184
+ const [speed, setSpeed] = useState(1);
185
+ const [bar, setBar] = useState(12);
186
+ return (
187
+ <>
188
+ <Transport.Root playing={playing} aria-label="Replay controls">
189
+ <Transport.Group>
190
+ <Transport.Start onClick={() => setBar(0)}>Start</Transport.Start>
191
+ <Transport.Previous onClick={() => setBar((b) => Math.max(0, b - 1))}>Back</Transport.Previous>
192
+ <Transport.Play onClick={() => setPlaying((p) => !p)}>{playing ? 'Pause' : 'Play'}</Transport.Play>
193
+ <Transport.Next onClick={() => setBar((b) => b + 1)}>Forward</Transport.Next>
194
+ <Transport.End onClick={() => setBar(59)}>End</Transport.End>
195
+ </Transport.Group>
196
+ <Transport.Speed speed={speed} options={[0.5, 1, 2, 4]} onSpeedChange={setSpeed} format={(x) => `${x}x`} />
197
+ <Transport.Readout>Bar {bar + 1} of 60</Transport.Readout>
198
+ </Transport.Root>
199
+ <Notice kind="gap" role="status">Two minutes of data are missing here.</Notice>
200
+ </>
201
+ );
202
+ }
203
+ ```
204
+
205
+ `Transport` and `Notice` have almost no look of their own. `Notice` sets `data-kind` (`gap`, `stale`, `truncated`, `unsupported`, `rights`, `info`, `ok`, `error`) and nothing styles it, so every kind looks the same until you add this kind of rule:
206
+
207
+ ```css
208
+ [data-kind='error'] { color: #b00020; }
209
+ [data-kind='gap'], [data-kind='stale'], [data-kind='truncated'] { color: #8a5a00; }
210
+ [data-kind='ok'] { color: #1b6e3c; }
211
+ nav[aria-label] > * + * { margin-inline-start: 0.5rem; } /* space the transport controls */
212
+ ```
213
+
214
+ ### Give a series a clock (`points`)
215
+
216
+ A series is a list of `{ t, kind, v? }` points, not a bare array: `t` is an ISO-8601 UTC string, `kind` is `value`, `missing`, `market-closed`, `not-applicable`, `halted` or `unknown`, and a `value` point has a finite `v` (zero is a value; NaN and infinities are refused). `Panels.Root` series and `Study.Line` take `points` and line them up with the bars by time, so a gap in the data is a gap on the chart, named in the list. `values` (an array with one number or null per bar) still works for now and will be removed in a later version; `pointsFromValues(bars, values)` converts one (it takes the bars, or their times in ms; a null or NaN becomes `not-applicable` unless you pass another gap kind).
217
+
218
+ ```ts
219
+ import { csvAdapter, toSeriesSet, validateSeriesSet, report } from '@matchstick-trading/primitives/core';
220
+
221
+ const read = csvAdapter.read(text, { utcOffsetMinutes: 0 }); // bars, extra numeric columns as series, errors, plain-language notes
222
+ const set = toSeriesSet(read);
223
+ const problems = validateSeriesSet(set); // every issue in one pass
224
+ const findings = report(set); // feed-stopped, no-point, gap-kind-differs, unclassified
225
+ ```
226
+
227
+ An empty or `NA` cell in an extra column becomes `unknown`, never zero. A time with no zone is read at the fixed offset you give (`utcOffsetMinutes`, UTC when you give none) and the notes say so (they say nothing when every time carries a zone). A column with a cell that is not a number or a no-value word is left out of the series and named in the notes. `Panels.List` says how many bars a series has no value on and why, and where it stops; there is no time-zone database. Each series entry in `Panels.Root` also takes `range: [low, high]` to fix its band's scale (a value outside it is cut off at the band's edge), `levels` for reference lines, and `style: 'histogram'`. They are fields of the series, not of the `panels` definitions.
228
+
229
+ ### Add a study (an indicator line)
230
+
231
+ A study is two things: a function from `@matchstick-trading/primitives/core` that turns bars into numbers (`ema`, `sma`, `wma`, `bollinger`, `vwap`, `rsi`, `atr`), and a part that draws the numbers (`Study.Line` for a line, `Study.Band` for a band between two series). `Study.Root` goes inside `Chart.Root` and wraps both the `Chart.Plot` that holds the lines and the `Study.Legend`, which lists the names and values and sits outside the SVG:
232
+
233
+ Two things the types will not say until the first build. `vwap`'s `bands` is a list of multipliers, not a count or a flag: `vwap(bars, { bands: [1] })` gives one band (`.bands[0].upper` and `.lower`), `[1, 2]` gives two. `studyRange` returns one `[low, high]` span, but `Chart.Root`'s `include` returns a list of spans, so wrap it: `include={(visible) => [studyRange(visible, band.upper, band.lower)]}` (keep the callback stable with `useCallback`).
234
+
235
+ ```tsx
236
+ import { Chart, Study, ema, pointsFromValues } from '@matchstick-trading/primitives';
237
+
238
+ import { bars } from './FirstChart'; // the `bars` list exported by the first chart above, saved as FirstChart.tsx
239
+ const ema10 = pointsFromValues(bars, ema(bars, 10)); // compute once, outside the component, or in useMemo
240
+
241
+ export function ChartWithEma() {
242
+ return (
243
+ <Chart.Root bars={bars} synthetic aria-label="Synthetic bars with an EMA">
244
+ <Study.Root>
245
+ <Study.Legend />
246
+ <Chart.Plot>
247
+ <Chart.Candles />
248
+ <Study.Line points={ema10} name="EMA 10" />
249
+ <Chart.PriceAxis />
250
+ <Chart.TimeAxis />
251
+ </Chart.Plot>
252
+ </Study.Root>
253
+ </Chart.Root>
254
+ );
255
+ }
256
+ ```
257
+
258
+ Like the candles, the study parts draw SVG with no stroke or fill, so add this too:
259
+
260
+ ```css
261
+ [data-part='study-line'] { stroke: #7a5cff; stroke-width: 1.25; fill: none; }
262
+ [data-part='study-band'] { fill: #7a5cff22; stroke: none; }
263
+ [data-part='study-legend'] { list-style: none; margin: 0; padding: 0; font: 12px var(--mt-font-numeric); }
264
+ ```
265
+
266
+ `rsi` is not a price overlay (its values are 0 to 100), so do not draw it on the price pane; this version has no separate indicator pane.
267
+
268
+ ### Add drawings
269
+
270
+ Drawings need a `Drawings.Root` around the chart and the toolbar, and a `Drawings.Layer` inside `Chart.Plot`. The Layer is what turns a click on
271
+ the chart into a drawing: without it a pressed tool does nothing. On touch, a vertical swipe on the chart scrolls the page while no tool is armed and nothing is selected (a tap on a drawing selects it); once a tool is armed or a drawing is selected the plot keeps the gesture (`touch-action: none`) so drawing and dragging work, so scroll the page from outside the chart then.
272
+
273
+ ```tsx
274
+ import { Chart, Drawings } from '@matchstick-trading/primitives';
275
+ import '@matchstick-trading/primitives/tokens.css';
276
+ import { bars } from './FirstChart';
277
+
278
+ export function DrawingChart() {
279
+ return (
280
+ <Drawings.Root>
281
+ <Drawings.Toolbar tools={['trend-line', 'horizontal-line', 'rectangle']} />
282
+ <Chart.Root bars={bars} synthetic aria-label="Synthetic one-minute bars">
283
+ <Chart.Plot>
284
+ <Chart.Grid />
285
+ <Chart.Candles style="candle" />
286
+ <Chart.PriceAxis />
287
+ <Chart.TimeAxis />
288
+ <Chart.Crosshair />
289
+ <Drawings.Layer />
290
+ </Chart.Plot>
291
+ </Chart.Root>
292
+ <Drawings.Undo />
293
+ <Drawings.List />
294
+ </Drawings.Root>
295
+ );
296
+ }
297
+ ```
298
+
299
+ Add the chart CSS from "First chart". `tokens.css` strokes the drawings in `currentColor` with no fill, so they show in the text colour; set
300
+ `color` on `[data-part='drawing']` to change it. Press a tool, then click twice on the chart (or press Enter twice with the chart focused).
301
+
302
+ The parts draw no styling of their own. The chart, study, depth and scrubber parts set `data-part` and state attributes
303
+ (`data-change`, `data-state`, `data-selected`, `data-synthetic`), and `tokens.css` plus your own CSS read those; the
304
+ replay controls take a `className`. Colours are colour-blind safe by
305
+ default, and direction is also carried by a mark, never by colour alone.
306
+
307
+ ### Select the series and studies
308
+
309
+ One thing is selected on a chart at a time: a drawing, the bar series or a study. `Selection.Root` holds that one value, `{ kind: 'drawing' | 'series' | 'study', id }` or `null`, and says each change in a polite live region. It is opt-in: without a `Selection.Root` above them, `Drawings` and `Study` behave exactly as before.
310
+
311
+ ```tsx
312
+ import { useMemo, useState } from 'react';
313
+ import { Chart, Selection, Series, Study, ema, pointsFromValues, type ChartSelection } from '@matchstick-trading/primitives';
314
+ import { bars } from './FirstChart';
315
+
316
+ export function SelectableChart() {
317
+ const [selection, setSelection] = useState<ChartSelection>(null);
318
+ const ema10 = useMemo(() => pointsFromValues(bars, ema(bars, 10)), []);
319
+ const [interval, setInterval] = useState('1m');
320
+ return (
321
+ <Selection.Root selection={selection} onSelectionChange={setSelection}>
322
+ <Series.Root id="main" label="Synthetic bars">
323
+ <Series.Select />
324
+ <Chart.Root bars={bars} synthetic aria-label="Synthetic bars">
325
+ <Study.Root>
326
+ <Study.Legend />
327
+ <Chart.Plot>
328
+ <Chart.Candles />
329
+ <Study.Line id="ema10" points={ema10} name="EMA 10" />
330
+ <Selection.Layer />
331
+ </Chart.Plot>
332
+ </Study.Root>
333
+ </Chart.Root>
334
+ <Series.Options>
335
+ {/* The options are yours: the parts ship no fields. */}
336
+ <label>Interval <select value={interval} onChange={(event) => setInterval(event.target.value)}><option>1m</option><option>5m</option></select></label>
337
+ </Series.Options>
338
+ </Series.Root>
339
+ </Selection.Root>
340
+ );
341
+ }
342
+ ```
343
+
344
+ | Part | What it does |
345
+ |---|---|
346
+ | `Selection.Root` | Holds the selection (`selection` and `onSelectionChange` to control it) and announces each change in a visually hidden `data-part='selection-status'` live region. |
347
+ | `Selection.Layer` | Goes inside `Chart.Plot`, after `Drawings.Layer` if there is one. Reads taps and Escape on the chart. Draws nothing. |
348
+ | `Series.Root` | `{ id, label }`. Says a bar series exists, so a tap on clear chart can select it. Without one, a tap on clear chart selects nothing. |
349
+ | `Series.Select` | A button, `aria-pressed`, named by the series label (or `label` or children). The keyboard and touch way to do what a tap on clear chart does; press again to put it away. |
350
+ | `Series.Options` | `data-part='series-options'`, `role='group'` with an `aria-label` (default: the label and "options"). Renders its children only while the series is selected. |
351
+ | `Study.Line`, `Study.Band` with `id` | Selectable with a `Selection.Root` above: a tap on the line or band (a transparent `data-part='study-hit'` stroke 44 px wide, the `--mt-touch-target` size, so a finger can hit it) and a `data-part='study-select'` button in each `Study.Legend` item. Give the study a `name`: the button is named by it. |
352
+
353
+ The rules, one tap or key, one effect:
354
+
355
+ - Tap on a drawing selects it. Tap on a study line or band that has an `id` selects that study.
356
+ - Tap on clear chart with something selected only puts the selection away.
357
+ - Tap on clear chart with nothing selected selects the series, when a `Series.Root` is mounted. With none, nothing happens, as before.
358
+ - Escape (chart focused) clears whatever is selected. A half-placed drawing or an armed drawing tool takes the first Escape.
359
+ - A drag is the chart panning, and never selects or deselects. A second finger (a pinch) does not count as a tap.
360
+ - Inside a `Selection.Root`, `Drawings.Root` reads and writes the same selection (a selected drawing is `{ kind: 'drawing' }`) and its own `selected` prop is ignored; `onSelectedChange` still fires.
361
+
362
+ The selected series, study line, study band and legend item carry `data-state='selected'` (the candles group for the series). `tokens.css` draws the selected series and study with a heavier stroke, so the state is not colour alone; `aria-pressed` and the live region say it in words. Series options are yours to fill: put your own controls in `Series.Options` (an interval list, an instrument picker) and decide what a change means. Show the panel where it does not push the chart down, or a second tap lands somewhere else.
363
+
364
+ The selection is not tied to a symbol or a list. Drawing ids are only unique within one list (`nextDrawingId` starts every list at `drawing-1`), so after a symbol
365
+ switch a `{ kind: 'drawing', id }` selection can point at the other list's drawing with the same id. Clear it yourself when you swap `drawings`
366
+ (`setSelection(null)`), the same way you replace the list. The same goes for a `Series.Root` or study `id` that changes with the symbol.
367
+
368
+ Study `id`s are unique on a chart: two studies with the same id are selected together. `Series.Options` and `Series.Select` throw if there is no `Series.Root` above them, and `Series.Root` throws without a `Selection.Root`. The pure rules (`selectionAfterTap`, `selectionAfterClearTap`, `selectionAfterEscape`, `describeSelection`) are in `@matchstick-trading/primitives/core`.
369
+
370
+ ## Panels and several series
371
+
372
+ A chart can have more than one band, each with its own price scale, on one shared time axis. The chart's own bars and volume stay in the first band, `main`. Other series (a value per bar, `null` for none, like a study) are given to `Panels.Root`, which places each one in a band and lets a person move it to another. It is opt-in: without a `Panels.Root`, the chart draws exactly as it did.
373
+
374
+ ```tsx
375
+ import { useMemo } from 'react';
376
+ import { Chart, Panels, Selection, ema, pointsFromValues, rsi } from '@matchstick-trading/primitives';
377
+ import { bars } from './FirstChart';
378
+
379
+ const panels = [{ id: 'rsi', label: 'RSI panel' }];
380
+
381
+ export function PanelChart() {
382
+ const series = useMemo(() => [
383
+ { id: 'ema50', label: 'EMA 50', points: pointsFromValues(bars, ema(bars, 50)) }, // starts in main, on the price scale
384
+ { id: 'rsi14', label: 'RSI 14', points: pointsFromValues(bars, rsi(bars, 14)), panel: 'rsi' }, // starts in a band of its own
385
+ ], []);
386
+ return (
387
+ <Selection.Root>
388
+ <Panels.Root series={series} panels={panels}>
389
+ <Chart.Root bars={bars} height={520} synthetic aria-label="Synthetic bars with an EMA and an RSI panel">
390
+ <Panels.List />
391
+ <Chart.Plot>
392
+ <Chart.Grid />
393
+ <Chart.Candles />
394
+ <Panels.Layer />
395
+ <Chart.PriceAxis />
396
+ <Chart.TimeAxis />
397
+ <Selection.Layer />
398
+ </Chart.Plot>
399
+ </Chart.Root>
400
+ <Panels.Move />
401
+ </Panels.Root>
402
+ </Selection.Root>
403
+ );
404
+ }
405
+ ```
406
+
407
+ | Part | What it does |
408
+ |---|---|
409
+ | `Panels.Root` | Holds the layout (controlled with `layout` and `onLayoutChange`, or its own) and says each move in a polite live region (`data-part='panels-status'`). Takes `series` and, optionally, `panels` (`{ id, label, weight }`) for the bands the series start in. |
410
+ | `Panels.Layer` | Inside `Chart.Plot`, after the chart's own parts. Draws each band (`data-part='panel'`, `data-panel`): a divider, a grid, price labels, a label and a crosshair line for every band but `main`, and the series (`data-part='panel-series'`, `data-series`). A series in `main` uses the chart's price scale and keeps itself in view. |
411
+ | `Panels.List` | Outside the SVG, inside `Chart.Root`. Each series, its band and its value at the crosshair bar (the last bar in view without one). With a `Selection.Root`, each row has a button that selects the series (`aria-pressed`). |
412
+ | `Panels.Move` | Shows while a series of the Root is selected (or for `seriesId`): buttons for where it can go, relative to where it is (`Move up to Main`, `Move down to RSI panel`, `Move to a new panel above`, `Move to a new panel below`). No drag is needed. After a move the focus goes to the group (`data-part='panel-move'`). |
413
+
414
+ A tap on a series line selects it (`{ kind: 'series', id }`, one selection at a time, shown with a heavier stroke and `data-state='selected'`). A tap on clear chart below the first band selects nothing. A band is never shorter than 40, the split is fixed (`main` gets three shares and every other band one), and the time labels sit under the last band, so give `Chart.Root` enough `height`. A band left with no series is removed, and `main` is always first. With the Root's own layout, a series added later starts in the band it names and one taken away leaves its band (an empty band goes); a placement a person made is kept. With a controlled `layout` (`PanelLayout`: `{ panels: { id, label, weight? }[], placement: Record<seriesId, panelId> }`; `main` is always first and is added if you leave it out, and its label is not drawn on the chart), keeping it in step with `series` is yours (`reconcileLayout` is in the core), and a move is announced only after the layout you give back puts the series there. With a `layout`, a series' `panel` and the `panels` prop are not read; `PanelLayout` and the rules are exported from the package root and from `/core`. The layout handed to `onLayoutChange` always lists `main` first, and a new band gets the id `panel-N` and the label `Panel N` (a new band below `main` goes directly under it); a band that was emptied and made again is a new band with a new name. Series with very different ranges in one band share its scale, so the smaller one flattens. To use panels in full screen, put `Panels.Root` and the chart in a child component of the `Fullscreen.Frame`, where `useFullscreen` can set the height. The panel styles (`panel-series`, divider, grid, axis, label) are in `tokens.css`, which `FirstChart` imports. Only the series you give can move: the chart's bars, volume and drawings stay in `main`, and the price range controls (`Chart.Reset`, `Chart.Autoscale`, a drag on the price labels) belong to `main`. Dragging a series between bands is not built in; the Move buttons are the way. A series that is no longer given is deselected. The pure rules (`panelBands`, `moveTargets`, `moveSeries`, `normalizeLayout`) are in `@matchstick-trading/primitives/core`.
415
+
416
+ ## The parts
417
+
418
+ | Import | What it is |
419
+ |---|---|
420
+ | `Chart` | Root, Plot, Grid, Volume, Candles (candle, hollow, ohlc, line, area, heikin-ashi), PriceAxis, TimeAxis, Crosshair, Readout, Reset, Autoscale. Pan, zoom and axis keys built in. |
421
+ | `Study` | EMA, SMA, WMA, Bollinger, RSI, ATR and VWAP lines and bands, and a legend. |
422
+ | `Selection`, `Series` | One selection (drawing, series or study) for the chart: `Selection.Root`, `Selection.Layer`, `Series.Root`, `Series.Select`, `Series.Options`, and an `id` on `Study.Line` and `Study.Band`. |
423
+ | `Panels` | Several series in bands with their own price scales, and a menu to move a series between bands: `Panels.Root`, `Panels.Layer`, `Panels.List`, `Panels.Move`. |
424
+ | `VolumeProfile` | Histogram and options over the visible bars. |
425
+ | `Drawings` | `Root`, `Layer` (the drawing surface, inside `Chart.Plot`), `Toolbar`, `List`, `Remove`, `Undo`, `Lock`, `Edit`. Trend line, ray, extended line, horizontal and vertical lines, rectangle, Fibonacci retracement, price range, text note; Toolbar, List, Remove with undo, Lock, Edit (typed price, time, bar and attach mode). Enter places a point at the crosshair. |
426
+ | `Toolbar`, `Menu`, `Submenu` | A toolbar that is one tab stop (arrow keys, Home, End, Escape back to the chart); a button that opens a menu; a menu item that opens another. Tap or key, never hover. |
427
+ | `Fullscreen` | `Frame` (a wrapper; `data-fullscreen` while expanded) and `Toggle` (a button with `aria-pressed`). The browser's full screen where it has one, a full-page fallback where it does not. |
428
+ | `Transport`, `Scrubber`, `Cursor` | Replay controls, a scrub bar and a cursor you can step through events. |
429
+ | `DepthProfile`, `DiffTable`, `Tape`, `Ladder`, `Receipt` | A depth view, a sortable table that shows what changed, the latest trades newest first (rows hold still while you read), a price ladder with size bars and a spread row, and a verifiable receipt. |
430
+ | `SymbolSearch`, `CsvDrop` | A search over a list you supply, and a drop zone that reads a CSV of bars. |
431
+ | `Notice` | A status message with a kind. |
432
+
433
+ Types are in `dist/index.d.ts` with the doc comments for every part and prop.
434
+
435
+ ### Every control has a name
436
+
437
+ A button, menu item or tool names its action in words, even when an icon is all that shows. The words are the visible text, or,
438
+ with `hideLabel`, the `aria-label`. `icon` is a slot you fill with your own graphic: it is hidden from assistive technology
439
+ (`aria-hidden`, `data-part="icon"`), and **no icons ship in this package**. A control with neither words nor a label has no name and
440
+ logs a warning to the console. This holds for `Toolbar.Button`, `Menu.Trigger`, `Menu.Item`, `Menu.RadioItem`, `Submenu.Trigger`,
441
+ the drawing tools and `Drawings.Lock`, `Remove`, `Undo`, `Edit`, and `Chart.Reset` and `Chart.Autoscale`.
442
+
443
+ ```tsx
444
+ import { Chart, Drawings, Toolbar } from '@matchstick-trading/primitives';
445
+
446
+ // Your own icons; here a plain character stands in for one.
447
+ const ICONS = { 'trend-line': '/', 'horizontal-line': '-' };
448
+
449
+ export function Controls() {
450
+ return (
451
+ <Drawings.Root>
452
+ {/* Only the icons show; each button is still named "Trend line", "Horizontal line" and so on. */}
453
+ <Drawings.Toolbar tools={['trend-line', 'horizontal-line']} icon={ICONS} hideLabels />
454
+ {/* A rename: labels change the words and the accessible name. */}
455
+ <Drawings.Toolbar aria-label="Lines" tools={['trend-line']} labels={{ 'trend-line': 'Diagonal' }} />
456
+ <Drawings.Lock icon="L" label="Lock drawing" hideLabel />
457
+ <Toolbar.Root aria-label="View">
458
+ <Toolbar.Button icon="+" label="Zoom in" hideLabel />
459
+ </Toolbar.Root>
460
+ <Chart.Root bars={[]}>
461
+ <Chart.Reset icon="R" label="Reset view" hideLabel />
462
+ </Chart.Root>
463
+ </Drawings.Root>
464
+ );
465
+ }
466
+ ```
467
+
468
+ ### Toolbar, Menu and Submenu
469
+
470
+ `Toolbar.Root` is `role="toolbar"` with one tab stop: Left and Right (Up and Down with `orientation="vertical"`) move between its
471
+ items, Home and End jump, Escape sends focus to `returnFocusTo` (a ref or a function that returns the element). Its items are
472
+ `Toolbar.Button` (`pressed` makes it a toggle) and `Menu.Trigger`. A `Menu` is a trigger plus a `Menu.List` of `Menu.Item` and
473
+ `Menu.RadioItem`; a `Submenu` goes inside a list. Everything opens by tap or click; nothing opens on hover. Inside a list: ArrowUp,
474
+ ArrowDown, Home, End, typeahead, Enter or Space to pick, Escape to close and return to the trigger, ArrowRight and ArrowLeft to open and
475
+ close a submenu. On a coarse pointer every button and item is at least `--mt-touch-target` (44px). The list is absolutely positioned
476
+ against the menu, so an ancestor that clips overflow will clip it.
477
+
478
+ ### Full screen
479
+
480
+ `Fullscreen.Frame` is a wrapper div: put the chart, its controls and the `Fullscreen.Toggle` inside it. The toggle expands the frame to the whole
481
+ screen and back. It works everywhere: where the browser has the Fullscreen API (desktop browsers, iPad Safari through its `webkit` prefix) the
482
+ frame goes full screen natively; where it does not (iPhone Safari), or the browser refuses, the frame instead fills the page itself (fixed over
483
+ everything, inside the safe areas, painted with `--mt-fullscreen-bg`, text in `--mt-fullscreen-fg`: light by default, dark under `data-theme='light'`), the page behind it becomes `inert` so focus and taps stay in the frame, and
484
+ the page stops scrolling. All of that is put back when you leave, even if the frame unmounts while expanded. Nothing is hidden when full screen is
485
+ unsupported. Leaving through the browser's own UI is followed, and focus goes back to the toggle.
486
+
487
+ ```tsx
488
+ import { Chart, Fullscreen, useFullscreen } from '@matchstick-trading/primitives';
489
+ import { bars } from './FirstChart'; // the `bars` list exported by the first chart above, saved as FirstChart.tsx
490
+
491
+ function TallChart() {
492
+ const { fullscreen } = useFullscreen(); // inside the Frame: lets the chart use the extra height
493
+ return (
494
+ <Chart.Root bars={bars} height={fullscreen ? 640 : 420}>
495
+ <Chart.Plot>
496
+ <Chart.Candles style="candle" />
497
+ <Chart.Crosshair />
498
+ </Chart.Plot>
499
+ <Chart.Readout />
500
+ </Chart.Root>
501
+ );
502
+ }
503
+
504
+ export function ChartWithFullScreen() {
505
+ return (
506
+ <Fullscreen.Frame>
507
+ <Fullscreen.Toggle /> {/* "Full screen" / "Exit full screen"; label and exitLabel rename, icon and hideLabel as on any button */}
508
+ <TallChart />
509
+ </Fullscreen.Frame>
510
+ );
511
+ }
512
+ ```
513
+
514
+ | `data-part` | What it is |
515
+ |---|---|
516
+ | `fullscreen-frame` | The wrapper. `data-fullscreen` is present while expanded; `data-mode` is `native` or `fallback`. |
517
+ | `fullscreen-toggle` | The button. `aria-pressed` is true while expanded; `data-touch-target` gives it `--mt-touch-target` (44px) on a coarse pointer. |
518
+ | `fullscreen-status` | A visually hidden live region: "Full screen" on entering, "Exited full screen" on leaving. |
519
+
520
+ `Fullscreen.Frame` keeps its own state; pass `fullscreen` and `onFullscreenChange` to control it. A browser may refuse native full screen unless a
521
+ tap or key asked for it, so a `fullscreen` prop set from code alone can end up in the in-page fallback. Put the toggle inside the frame: the page
522
+ behind an in-page frame is inert, and a toggle outside it could not be reached. The in-page fallback is `position: fixed`, so an ancestor with a
523
+ `transform`, `filter` or `contain` would trap it; keep the frame out of one.
524
+
525
+ Escape leaves full screen, in this order. A part that handles Escape itself goes first: an open menu closes, the `Drawings.Edit` form closes,
526
+ the toolbar sends focus to its `returnFocusTo`. Then, with a drawing tool armed or a drawing half placed, the first Escape disarms or cancels it.
527
+ Then, with focus on the chart, Escape clears what the chart has marked: the crosshair bar, or a selection of any kind (a drawing, the series, a
528
+ study). With none of those, or with focus elsewhere in the frame, it leaves full screen. The frame reads these from the parts' own attributes
529
+ (`data-part='crosshair'`, `data-selected`, `data-state='selected'`, `data-draft`, `aria-pressed` on the armed tool), so a host that restyles the
530
+ parts keeps them. Native full screen has the browser's own Escape too.
531
+
532
+ ### Drawing from the keyboard
533
+
534
+ With a tool pressed and the chart focused:
535
+
536
+ | Key | What it does |
537
+ |---|---|
538
+ | Arrow keys, Home, End | Move the crosshair bar by bar; a half-placed drawing follows it. |
539
+ | Enter | Place a point at the crosshair: the crosshair bar, at the crosshair price (from the keyboard, that bar's close). A one-point tool is done; a two-point tool takes a second Enter at a different place. |
540
+ | Escape | Drop a half-placed drawing, then put the tool away. |
541
+ | Ctrl or Cmd + Z | Put back the last removal while `Drawings.Undo` still offers it. |
542
+
543
+ Each step is spoken in the polite live region of `Drawings.Undo`, in words: "Trend line: first point at 100.40, 19:03." and "Trend line:
544
+ second point at 101.20, 19:07. Added Trend line from 100.40 at 19:03 to 101.20 at 19:07." The same region says "Updated ..." after
545
+ `Drawings.Edit` applies. After the last row of `Drawings.List` is removed, focus goes to the Undo button when it is showing, else to the
546
+ empty list, so it never falls to the top of the page.
547
+
548
+ On a coarse pointer every drawing control is at least `--mt-touch-target` (44px) in both directions: `Drawings.Undo`, `Drawings.Remove`, `Drawings.Lock`,
549
+ the `Drawings.Edit` toggle with its inputs, Apply and Cancel, and the three buttons in each `Drawings.List` row. Opening the Edit form scrolls it into view
550
+ (no smooth scroll under `prefers-reduced-motion`). A pending Undo is dropped when the host hands `drawings` back as a list the Root did not make
551
+ (for example a new empty array after a symbol switch); a copy of the same drawings in the same order keeps it. The copy must hold the same drawing objects: clones (immer, `structuredClone`, a server round-trip)
552
+ and a derived empty list (`drawings.filter(...)` after the last one is removed) count as a different list, so Undo is not offered. That trade is deliberate: it
553
+ cannot tell an emptied list from a switch to a symbol with no drawings, and a missing Undo is safer than a drawing restored onto the wrong symbol.
554
+
555
+ ## The core, without React
556
+
557
+ `@matchstick-trading/primitives/core` is plain TypeScript with no imports outside itself: chart geometry and scales,
558
+ the studies (`ema`, `sma`, `rsi`, `atr`, `bollinger`, `vwap`), the volume profile, the drawing model, the CSV reader
559
+ (`parseBarsCsv`), the key maps and the chart's pointer and wheel gestures (`reduceChartGesture`, a pure reducer from an event to a viewport or price-range change). Use it from any framework or from Node.
560
+
561
+ ## Your own data
562
+
563
+ Give `Chart.Root` a list of bars (a bar with a missing or non-finite time or price is left out rather than drawn, so filter such bars yourself before computing a study, or its values will not line up), or read a CSV with `parseBarsCsv` (or the `CsvDrop` part) and pass the result. Data
564
+ from a third party stays theirs and is not advice. Do not put real market data into anything you share.
565
+
566
+ ## Tokens
567
+
568
+ Import `@matchstick-trading/primitives/tokens.css` once and override any custom property on a parent element. Every token:
569
+
570
+ - Layout and type: `--mt-touch-target` (44px), `--mt-font-numeric`.
571
+ - Surface: `--mt-surface`, the background the colour tokens are tuned for. The parts do not paint it. While a `Fullscreen.Frame` is expanded it paints `--mt-fullscreen-bg` (the surface by default) and sets its text colour to `--mt-fullscreen-fg` (`#e8eaef`, or `#14141c` under `data-theme='light'`), so axis labels, the readout and your own text stay readable.
572
+ - Colour: `--mt-bid`, `--mt-ask`, `--mt-up`, `--mt-down`, `--mt-flat`, `--mt-warn`, `--mt-error`, `--mt-destructive`, `--mt-synthetic`, `--mt-cursor-a`, `--mt-cursor-b`, `--mt-focus`.
573
+ - Defaults and where they come from: the dark values follow the Matchstick design tokens, written into `src/tokens.css`. Changed from 0.0.6: `--mt-focus` is pink `#ff2a85` (was amber `#ffc247`), `--mt-surface` is `#12151c` (was `#12121a`), `--mt-error` and `--mt-destructive` are `#ff6b6b` (were `#ff6b81`), `--mt-flat` is `#8c93a3` (was `#9090a0`), `--mt-fullscreen-fg` is `#e8eaef` (was `#e8e8f0`), and `--mt-font-numeric` is `"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace` (was `'SF Mono', 'Cascadia Code', 'Fira Code', monospace`; the package does not ship the font, so without it you get the system monospace). Bid and ask stay blue and orange. The light preset and `data-palette='classic'` are unchanged.
574
+ - Marks (text that carries meaning without colour): `--mt-mark-bid`, `--mt-mark-ask`, `--mt-mark-up`, `--mt-mark-down`, `--mt-mark-flat`, `--mt-mark-added`, `--mt-mark-removed`, `--mt-mark-modified`, `--mt-mark-gap`, `--mt-mark-stale`, `--mt-mark-truncated`, `--mt-mark-synthetic`.
575
+ - Drawing states: `--mt-drawing-width`, `--mt-drawing-width-active`, `--mt-drawing-width-selected`, `--mt-drawing-dash`, `--mt-drawing-dash-active`, `--mt-drawing-dash-selected`, `--mt-drawing-width-locked`, `--mt-drawing-dash-locked`, `--mt-drawing-handle-width`, `--mt-drawing-handle-width-dragging`.
576
+ - Other: `--mt-numeric-width` (set it on a column; see `numericWidth()`).
577
+
578
+ ### Drawing state looks
579
+
580
+ A drawing is `data-state="passive"`, `"active"` (being drawn or moved) or `"selected"`, and may carry `data-locked`. The base token
581
+ (`--mt-drawing-width`, `--mt-drawing-dash`) is the passive state. The default is the look the docs site always had: passive is thin and
582
+ solid, active is thicker and dashed (`6 4`), selected is the thickest and solid. Change it without writing a selector:
583
+
584
+ ```css
585
+ :root { --mt-drawing-dash-selected: 2 3; --mt-drawing-width-selected: 3.5; } /* a dotted, heavy selection */
586
+ :root { --mt-drawing-dash-active: none; } /* solid while moving */
587
+ :root { --mt-drawing-dash-locked: 8 3; } /* locked drawings dashed; unset falls back to their state */
588
+ ```
589
+
590
+ Width is an SVG stroke width, dash is a `stroke-dasharray` (`none` is solid). The rules sit in `tokens.css` behind `:where()`, so a rule of yours
591
+ always wins. The tokens apply to lines, rectangles and the level lines of a Fibonacci retracement. Colour is still yours: set `color` on `[data-part='drawing']` (the lines use `currentColor`).
592
+
593
+ ### Light preset
594
+
595
+ `tokens.css` is tuned for a dark surface. For a light one, put `data-theme="light"` on any element over a white surface; nothing else changes:
596
+
597
+ ```html
598
+ <div data-theme="light" style="background: var(--mt-surface)">
599
+ <!-- Chart, DepthProfile, Notice, ... -->
600
+ </div>
601
+ ```
602
+
603
+ `data-palette="classic"` has a light variant (green and red) when used on or inside the same element. The package does not follow
604
+ `prefers-color-scheme` on its own; to do that, set `data-theme` from your own media query or script. Contrast was computed with the WCAG 2 relative-luminance
605
+ formula: text-like tokens need 4.5:1, non-text marks (cursor edges) and the focus ring need 3:1.
606
+
607
+ | Token | Light | Dark | Light on #ffffff | Dark on #12151c | Minimum |
608
+ |---|---|---|---|---|---|
609
+ | `--mt-bid` | `#1f6fd1` | `#4aa3ff` | 4.94 | 6.94 | 4.5 |
610
+ | `--mt-ask` | `#a64f00` | `#ff9f43` | 5.62 | 8.95 | 4.5 |
611
+ | `--mt-up` | `#1f6fd1` | `#4aa3ff` | 4.94 | 6.94 | 4.5 |
612
+ | `--mt-down` | `#a64f00` | `#ff9f43` | 5.62 | 8.95 | 4.5 |
613
+ | `--mt-flat` | `#5f5f70` | `#8c93a3` | 6.25 | 5.93 | 4.5 |
614
+ | `--mt-warn` | `#8a5a00` | `#ffc247` | 5.93 | 11.36 | 4.5 |
615
+ | `--mt-error` | `#c0243f` | `#ff6b6b` | 5.89 | 6.58 | 4.5 |
616
+ | `--mt-destructive` | `#c0243f` | `#ff6b6b` | 5.89 | 6.58 | 4.5 |
617
+ | `--mt-synthetic` | `#7a3fd0` | `#b48cff` | 6.09 | 7.08 | 4.5 |
618
+ | `--mt-cursor-a` | `#1f6fd1` | `#4aa3ff` | 4.94 | 6.94 | 3 |
619
+ | `--mt-cursor-b` | `#a64f00` | `#ff9f43` | 5.62 | 8.95 | 3 |
620
+ | `--mt-focus` | `#a86400` | `#ff2a85` | 4.68 | 5.15 | 3 |
621
+ | classic `bid/up/cursor-a` | `#0a7a4f` | `#00c87a` | 5.37 | 8.30 | 4.5 |
622
+ | classic `ask/down/cursor-b` | `#c0243f` | `#ff4d6a` | 5.89 | 5.67 | 4.5 |
623
+
624
+ The colours that failed on white in 0.0.2 (synthetic 2.58, bid 2.63, ask 2.04, warn 1.61, error 2.74, flat 3.14, focus 1.61) now pass in the light preset.
625
+
626
+ ## What is in this package
627
+
628
+ - `README.md`, `AGENTS.md`, `llms.txt`: this file, how an agent should use the package, and a short index.
629
+ - `dist/`: compiled JavaScript and `.d.ts` declarations for `.` and `./core`.
630
+ - `src/tokens.css`, imported as `@matchstick-trading/primitives/tokens.css`: the design tokens (`--mt-*`), including `--mt-touch-target`, the light preset and the drawing state tokens (see Tokens).
631
+ - `examples/`: small runnable files with synthetic data (`node examples/studies.mjs`).
632
+
633
+ The TypeScript source in `src/` (without its tests) ships too, under the same licence.
634
+
635
+ More: https://ui.matchstick.trading/docs/getting-started · supported scope and known limits:
636
+ https://ui.matchstick.trading/docs/supported · issues: https://github.com/matchstick-trading/primitives/issues