@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,256 @@
1
+ /**
2
+ * The matching rules of the symbol search as plain functions. The host owns the instrument list;
3
+ * this owns matching. Ideas, not code, come from the old symbol trie (a character trie, no licence on record): it
4
+ * is replaced here by sorted arrays and binary search, and by no regular expression at all, so no typed character
5
+ * can make `searchSymbols` throw.
6
+ *
7
+ * Order of results: an exact symbol, then a symbol that starts with the text, then an exact alias, then an alias or a
8
+ * name word that starts with the text. When more than one word is typed, each must start a different word of the symbol, name
9
+ * or aliases; those matches rank with the word matches. Inside a group: the shorter symbol, then A to Z, then venue, then list order.
10
+ * Typo tolerance is deliberately not here: in a ticker box a typo can pick the wrong instrument.
11
+ */
12
+ export interface Instrument {
13
+ /** Unique within the list. */
14
+ id: string;
15
+ symbol: string;
16
+ venue: string;
17
+ name?: string;
18
+ aliases?: readonly string[];
19
+ kind?: string;
20
+ /** YYYY-MM-DD, for contracts that expire. */
21
+ expires?: string;
22
+ }
23
+
24
+ export type MatchedOn = 'symbol' | 'alias' | 'name';
25
+ export type MatchHow = 'exact' | 'prefix' | 'word';
26
+
27
+ export interface SymbolMatch {
28
+ instrument: Instrument;
29
+ matchedOn: MatchedOn;
30
+ how: MatchHow;
31
+ /** 0 is best. */
32
+ rank: number;
33
+ }
34
+
35
+ export interface SearchOptions {
36
+ /** At most this many rows; default 20, clamped to 1..200. */
37
+ limit?: number;
38
+ venue?: string;
39
+ kind?: string;
40
+ }
41
+
42
+ interface Entry {
43
+ instrument: Instrument;
44
+ order: number;
45
+ symbolKey: string;
46
+ venueKey: string;
47
+ kindKey: string;
48
+ /** Every word of the symbol, name and aliases, built when a multi-word query first needs it. */
49
+ parts?: Part[];
50
+ }
51
+ interface Part { word: string; on: MatchedOn }
52
+ interface Keyed { key: string; entry: Entry; on: MatchedOn }
53
+
54
+ export interface SymbolIndex {
55
+ /** Records kept. */
56
+ readonly size: number;
57
+ /** Records dropped because id, symbol or venue was not a non-empty string, or the id repeated. */
58
+ readonly skipped: number;
59
+ /** @internal */ readonly entries: readonly Entry[];
60
+ /** @internal */ readonly bySymbol: readonly Keyed[];
61
+ /** @internal */ words: Keyed[] | null;
62
+ /** @internal */ tails: Keyed[] | null;
63
+ }
64
+
65
+ const DEFAULT_LIMIT = 20;
66
+ const MAX_LIMIT = 200;
67
+ const MAX_QUERY = 64;
68
+
69
+ /** Upper case, Unicode-composed, spaces collapsed. Total: it accepts any string, including lone surrogates. */
70
+ export function normalizeSymbolText(text: unknown): string {
71
+ return fold(text).slice(0, MAX_QUERY);
72
+ }
73
+
74
+ function fold(text: unknown): string {
75
+ if (typeof text !== 'string') return '';
76
+ let s = text;
77
+ try { s = s.normalize('NFKC'); } catch { /* keep the text as typed */ }
78
+ return s.toUpperCase().replace(/\s+/g, ' ').trim();
79
+ }
80
+
81
+ /** The words of a value, each cut to the query length: split first, so a word past the 64th character is still found. */
82
+ const wordsOf = (value: unknown): string[] => (text(value) ? fold(value).split(' ').filter(Boolean).map((w) => w.slice(0, MAX_QUERY)) : []);
83
+
84
+ const text = (v: unknown): v is string => typeof v === 'string' && v.trim() !== '';
85
+
86
+ const byKey = (a: Keyed, b: Keyed) => (a.key < b.key ? -1 : a.key > b.key ? 1 : a.entry.order - b.entry.order);
87
+
88
+ /** Indexes the symbols now; names and aliases on the first search that needs them. Never throws. */
89
+ export function buildSymbolIndex(instruments: readonly Instrument[]): SymbolIndex {
90
+ const entries: Entry[] = [];
91
+ const seen = new Set<string>();
92
+ let skipped = 0;
93
+ for (const instrument of instruments ?? []) {
94
+ if (!instrument || !text(instrument.id) || !text(instrument.symbol) || !text(instrument.venue) || seen.has(instrument.id)) { skipped++; continue; }
95
+ seen.add(instrument.id);
96
+ entries.push({ instrument, order: entries.length, symbolKey: normalizeSymbolText(instrument.symbol), venueKey: normalizeSymbolText(instrument.venue), kindKey: normalizeSymbolText(instrument.kind) });
97
+ }
98
+ const bySymbol = entries.map((entry): Keyed => ({ key: entry.symbolKey, entry, on: 'symbol' })).sort(byKey);
99
+ return { size: entries.length, skipped, entries, bySymbol, words: null, tails: null };
100
+ }
101
+
102
+ function wordIndex(index: SymbolIndex): Keyed[] {
103
+ if (index.words) return index.words;
104
+ const words: Keyed[] = [];
105
+ for (const entry of index.entries) {
106
+ const { name, aliases } = entry.instrument;
107
+ for (const w of wordsOf(name)) words.push({ key: w, entry, on: 'name' });
108
+ if (Array.isArray(aliases)) for (const alias of aliases) if (text(alias)) words.push({ key: normalizeSymbolText(alias), entry, on: 'alias' });
109
+ }
110
+ return (index.words = words.sort(byKey));
111
+ }
112
+
113
+ function entryParts(entry: Entry): Part[] {
114
+ if (entry.parts) return entry.parts;
115
+ const { symbol, name, aliases } = entry.instrument;
116
+ const parts: Part[] = wordsOf(symbol).map((word): Part => ({ word, on: 'symbol' })); // a symbol can hold spaces
117
+ for (const w of wordsOf(name)) parts.push({ word: w, on: 'name' });
118
+ if (Array.isArray(aliases)) for (const alias of aliases) for (const w of wordsOf(alias)) parts.push({ word: w, on: 'alias' });
119
+ return (entry.parts = parts);
120
+ }
121
+
122
+ /**
123
+ * The second and later words of symbols and aliases, for queries of more than one word; built on first use. The
124
+ * symbol index and the word index already start at every name word and at the first word of each symbol and alias,
125
+ * so only these are missing, and in most lists there are few of them.
126
+ */
127
+ function tailIndex(index: SymbolIndex): Keyed[] {
128
+ if (index.tails) return index.tails;
129
+ const tails: Keyed[] = [];
130
+ for (const entry of index.entries) {
131
+ const { symbol, aliases } = entry.instrument;
132
+ for (const w of wordsOf(symbol).slice(1)) tails.push({ key: w, entry, on: 'symbol' });
133
+ if (Array.isArray(aliases)) for (const alias of aliases) for (const w of wordsOf(alias).slice(1)) tails.push({ key: w, entry, on: 'alias' });
134
+ }
135
+ return (index.tails = tails.sort(byKey));
136
+ }
137
+
138
+ /** First position whose key is >= `prefix`. */
139
+ function lowerBound(list: readonly Keyed[], prefix: string): number {
140
+ let lo = 0;
141
+ let hi = list.length;
142
+ while (lo < hi) {
143
+ const mid = (lo + hi) >>> 1;
144
+ if (list[mid].key < prefix) lo = mid + 1; else hi = mid;
145
+ }
146
+ return lo;
147
+ }
148
+
149
+ function compare(a: SymbolMatch, b: SymbolMatch): number {
150
+ return a.rank - b.rank
151
+ || a.instrument.symbol.length - b.instrument.symbol.length
152
+ || (a.instrument.symbol < b.instrument.symbol ? -1 : a.instrument.symbol > b.instrument.symbol ? 1 : 0)
153
+ || (a.instrument.venue < b.instrument.venue ? -1 : a.instrument.venue > b.instrument.venue ? 1 : 0);
154
+ }
155
+
156
+ /** Ranked rows for `query`, at most `limit`. An empty or odd query gives no rows; it never throws. */
157
+ export function searchSymbols(index: SymbolIndex, query: string, options: SearchOptions = {}): SymbolMatch[] {
158
+ try {
159
+ const q = normalizeSymbolText(query);
160
+ if (q === '') return [];
161
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Math.floor(Number.isFinite(options.limit) ? (options.limit as number) : DEFAULT_LIMIT)));
162
+ const venue = options.venue ? normalizeSymbolText(options.venue) : null;
163
+ const kind = options.kind ? normalizeSymbolText(options.kind) : null;
164
+ // The best `limit` rows so far, in order. A word index can hold tens of thousands of hits for one letter,
165
+ // so keep a short sorted list instead of collecting and sorting them all.
166
+ const top: Array<{ match: SymbolMatch; order: number }> = [];
167
+ const before = (match: SymbolMatch, order: number, other: { match: SymbolMatch; order: number }) => (compare(match, other.match) || order - other.order) < 0; // last tie-break: list order
168
+ const consider = (hit: Keyed, how: MatchHow, rank: number) => {
169
+ const { instrument, order, venueKey, kindKey } = hit.entry;
170
+ if (venue && venueKey !== venue) return;
171
+ if (kind && kindKey !== kind) return;
172
+ const match: SymbolMatch = { instrument, matchedOn: hit.on, how, rank };
173
+ const at = top.findIndex((t) => t.order === order);
174
+ if (at >= 0) {
175
+ if (top[at].match.rank <= rank) return; // already here with an equal or better match
176
+ top.splice(at, 1);
177
+ } else if (top.length >= limit && before(match, order, top[top.length - 1]) === false) return;
178
+ let i = top.length;
179
+ while (i > 0 && before(match, order, top[i - 1])) i--;
180
+ top.splice(i, 0, { match, order });
181
+ if (top.length > limit) top.pop();
182
+ };
183
+ // Symbols: the sorted run that starts with q; the exact ones sort first inside it.
184
+ for (let i = lowerBound(index.bySymbol, q); i < index.bySymbol.length && index.bySymbol[i].key.startsWith(q); i++) {
185
+ const hit = index.bySymbol[i];
186
+ consider(hit, hit.key === q ? 'exact' : 'prefix', hit.key === q ? 0 : 1);
187
+ }
188
+ // Aliases and name words rank below every symbol match, so a full list of symbol matches needs no further look
189
+ // (and the name index is built on the first search that needs it, not at indexing time).
190
+ if (top.length < limit || top[top.length - 1].match.rank > 1) {
191
+ const words = wordIndex(index);
192
+ for (let i = lowerBound(words, q); i < words.length && words[i].key.startsWith(q); i++) {
193
+ const hit = words[i];
194
+ if (hit.on === 'alias') consider(hit, hit.key === q ? 'exact' : 'prefix', hit.key === q ? 2 : 3);
195
+ else consider(hit, 'word', 3);
196
+ }
197
+ }
198
+ // More than one word typed ("apple inc", "dec 2026"): every word must start a different word of the symbol, name
199
+ // or aliases, so "dec dec" needs two DECs. The longest word picks the candidates; the rest are checked on each.
200
+ // Ranked with the word matches.
201
+ if (q.includes(' ') && (top.length < limit || top[top.length - 1].match.rank > 2)) {
202
+ // Longest first: two typed words that fit the same word are prefixes of each other, so the longer one is the
203
+ // pickier and claims a word first; then a greedy pass finds a fit whenever one exists.
204
+ const tokens = q.split(' ').filter(Boolean).sort((a, b) => b.length - a.length);
205
+ const driver = tokens[0];
206
+ const done = new Set<number>();
207
+ let taken = new Uint8Array(16);
208
+ const check = (entry: Entry) => {
209
+ if (done.has(entry.order)) return;
210
+ done.add(entry.order);
211
+ const words = entryParts(entry);
212
+ if (words.length > taken.length) taken = new Uint8Array(words.length); else taken.fill(0, 0, words.length);
213
+ let onSymbol = false;
214
+ let onName = false;
215
+ for (const t of tokens) {
216
+ let k = 0;
217
+ while (k < words.length && (taken[k] === 1 || !words[k].word.startsWith(t))) k++;
218
+ if (k === words.length) return;
219
+ taken[k] = 1;
220
+ if (words[k].on === 'symbol') onSymbol = true; else if (words[k].on === 'name') onName = true;
221
+ }
222
+ consider({ key: q, entry, on: onSymbol ? 'symbol' : onName ? 'name' : 'alias' }, 'word', 3);
223
+ };
224
+ // Candidates: every entry with a word that starts with the driver (a symbol, a name word, an alias, or a later word of a symbol or alias).
225
+ for (const list of [index.bySymbol, wordIndex(index), tailIndex(index)]) {
226
+ for (let i = lowerBound(list, driver); i < list.length && list[i].key.startsWith(driver); i++) check(list[i].entry);
227
+ }
228
+ }
229
+ return top.map((t) => t.match);
230
+ } catch {
231
+ return []; // the contract of this function: a result list, never an exception
232
+ }
233
+ }
234
+
235
+ /** Resolves the key a user pressed in the search box. */
236
+ export type SymbolKeyAction = 'next' | 'previous' | 'pick' | 'close' | 'clear';
237
+ interface KeyLike { key: string; ctrlKey: boolean; metaKey: boolean; altKey: boolean; isComposing?: boolean }
238
+
239
+ /** ArrowDown/Up move, Enter picks, Escape closes the list and a second Escape clears the text. Not while composing text (IME). */
240
+ export function resolveSymbolKey(event: KeyLike, state: { open: boolean; hasText: boolean }): SymbolKeyAction | null {
241
+ if (event.ctrlKey || event.metaKey || event.altKey || event.isComposing) return null;
242
+ switch (event.key) {
243
+ case 'ArrowDown': return 'next';
244
+ case 'ArrowUp': return 'previous';
245
+ case 'Enter': return state.open ? 'pick' : null;
246
+ case 'Escape': return state.open ? 'close' : state.hasText ? 'clear' : null;
247
+ default: return null;
248
+ }
249
+ }
250
+
251
+ /** The row index a move lands on; the ends stop (no wrap). With no row active, next lands on 0 and previous on the last. */
252
+ export function nextSymbolIndex(count: number, current: number, action: 'next' | 'previous'): number {
253
+ if (count === 0) return -1;
254
+ if (current < 0 || current >= count) return action === 'next' ? 0 : count - 1;
255
+ return Math.min(count - 1, Math.max(0, current + (action === 'next' ? 1 : -1)));
256
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The rules of the trade tape as plain functions. A trade is a time, a price, a size and a side. Price and size arrive and
3
+ * stay exact decimal strings and are printed as given; time is an ISO-8601 UTC instant. A row that breaks a rule is
4
+ * reported with its place in the input and is never drawn. Nothing here repairs a row, reads a clock or touches a page.
5
+ */
6
+
7
+ export type TapeSide = 'buy' | 'sell' | 'unknown';
8
+
9
+ /** One trade as the host records it. `side` is the aggressor when the feed says so; `unknown` when it does not. */
10
+ export interface TapeTrade {
11
+ /** A stable id from the feed, such as a sequence number. Leave it out and the row's place in the list is used, which changes if the host drops old rows. */
12
+ id?: string;
13
+ /** ISO-8601 UTC, for example `2026-01-05T14:30:05.123Z`. Rows come oldest first and never go back in time. */
14
+ t: string;
15
+ /** Exact decimal string, above zero. */
16
+ price: string;
17
+ /** Exact decimal string, above zero. */
18
+ size: string;
19
+ side: TapeSide;
20
+ }
21
+
22
+ /** A trade that passed the rules, with the id it keeps while the host adds rows after it. */
23
+ export interface TapeRow extends TapeTrade {
24
+ /** The trade's `id`, or its place in the input as text. */
25
+ id: string;
26
+ /** `14:30:05.123`: the UTC clock time, as recorded. */
27
+ time: string;
28
+ }
29
+
30
+ export interface TapeIssue {
31
+ /** The row's place in the input. */
32
+ index: number;
33
+ message: string;
34
+ }
35
+
36
+ export interface TapeRead {
37
+ /** Valid rows, oldest first, as given. */
38
+ rows: TapeRow[];
39
+ issues: TapeIssue[];
40
+ }
41
+
42
+ export const TAPE_SIDE_LABEL: Record<TapeSide, string> = { buy: 'Buy', sell: 'Sell', unknown: 'Unknown' };
43
+
44
+ const ISO_UTC = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(\.\d+)?Z$/;
45
+ const POSITIVE_DECIMAL = /^\d+(?:\.\d+)?$/;
46
+
47
+ function isIsoUtc(value: unknown): value is string {
48
+ if (typeof value !== 'string') return false;
49
+ const m = ISO_UTC.exec(value);
50
+ if (!m) return false;
51
+ const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
52
+ return d.getUTCFullYear() === Number(m[1]) && d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
53
+ }
54
+
55
+ function isPositiveDecimal(value: unknown): value is string {
56
+ return typeof value === 'string' && POSITIVE_DECIMAL.test(value) && /[1-9]/.test(value);
57
+ }
58
+
59
+ /** Order two valid ISO-8601 UTC instants at the precision written, so fractions past milliseconds still count. */
60
+ function compareInstants(a: string, b: string): number {
61
+ const [aMain, aFrac = ''] = a.slice(0, -1).split('.');
62
+ const [bMain, bFrac = ''] = b.slice(0, -1).split('.');
63
+ if (aMain !== bMain) return aMain < bMain ? -1 : 1;
64
+ const width = Math.max(aFrac.length, bFrac.length);
65
+ const x = aFrac.padEnd(width, '0');
66
+ const y = bFrac.padEnd(width, '0');
67
+ return x === y ? 0 : x < y ? -1 : 1;
68
+ }
69
+
70
+ /** The clock part of an ISO-8601 UTC instant, fraction kept as recorded: `14:30:05.123`. */
71
+ export const tapeTime = (t: string): string => t.slice(11, -1);
72
+
73
+ /**
74
+ * Check the trades at the boundary. Every problem is returned in one pass. A row earlier than the row before it is
75
+ * reported and left out; rows at the same instant are fine (one order can fill in several prints).
76
+ */
77
+ export function readTape(input: unknown): TapeRead {
78
+ const rows: TapeRow[] = [];
79
+ const issues: TapeIssue[] = [];
80
+ if (!Array.isArray(input)) return { rows, issues: [{ index: -1, message: 'expected a list of trades' }] };
81
+ let last: string | null = null;
82
+ const seen = new Set<string>();
83
+ input.forEach((item: unknown, index) => {
84
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) { issues.push({ index, message: 'expected a trade object' }); return; }
85
+ const { id, t, price, size, side } = item as Record<string, unknown>;
86
+ const problems: string[] = [];
87
+ if (!isIsoUtc(t)) problems.push('t must be an ISO-8601 UTC time ending in Z');
88
+ if (!isPositiveDecimal(price)) problems.push('price must be an exact decimal string above zero');
89
+ if (!isPositiveDecimal(size)) problems.push('size must be an exact decimal string above zero');
90
+ if (side !== 'buy' && side !== 'sell' && side !== 'unknown') problems.push('side must be "buy", "sell" or "unknown"');
91
+ if (id !== undefined && (typeof id !== 'string' || id === '')) problems.push('id must be a non-empty string when given');
92
+ if (problems.length) { issues.push({ index, message: problems.join('; ') }); return; }
93
+ if (last !== null && compareInstants(t as string, last) < 0) { issues.push({ index, message: 'earlier than the row before it' }); return; }
94
+ const rowId = typeof id === 'string' ? id : String(index);
95
+ if (seen.has(rowId)) { issues.push({ index, message: `id "${rowId}" is already used` }); return; }
96
+ seen.add(rowId);
97
+ last = t as string;
98
+ rows.push({ id: rowId, t: t as string, price: price as string, size: size as string, side: side as TapeSide, time: tapeTime(t as string) });
99
+ });
100
+ return { rows, issues };
101
+ }
102
+
103
+ /** Newest first, the order a tape is read in. The input is not changed. */
104
+ export const newestFirst = (rows: readonly TapeRow[]): TapeRow[] => [...rows].reverse();
105
+
106
+ /**
107
+ * While the pointer or focus is on the tape, rows hold their place: the rows shown at that moment stay, in order, and
108
+ * rows that arrived since are counted, not inserted. `held` is newest first, the order the tape shows. Ids that have left the host's window leave the list.
109
+ */
110
+ export function holdTape(held: readonly string[], rows: readonly TapeRow[]): { rows: TapeRow[]; waiting: number } {
111
+ const byId = new Map(rows.map((row) => [row.id, row]));
112
+ const kept = held.flatMap((id) => { const row = byId.get(id); return row ? [row] : []; });
113
+ // Rows come oldest first, so what arrived since is whatever sits after the newest row that was held. A rolling window
114
+ // drops the oldest rows first, so if the newest held row is gone, every row now in the window is newer.
115
+ const newest = held.length ? rows.findIndex((row) => row.id === held[0]) : -1;
116
+ return { rows: kept, waiting: newest >= 0 ? rows.length - 1 - newest : rows.length };
117
+ }
118
+
119
+ /**
120
+ * One sentence for a polite live region: how many trades, how the sides split, the last print. It is a summary of the
121
+ * rows given (oldest first), not a line per trade; the caller decides how often to refresh it.
122
+ */
123
+ export function tapeSummary(rows: readonly TapeRow[]): string {
124
+ if (rows.length === 0) return 'No trades yet.';
125
+ const count = { buy: 0, sell: 0, unknown: 0 };
126
+ for (const row of rows) count[row.side] += 1;
127
+ const last = rows[rows.length - 1];
128
+ const parts = [`${count.buy} buy`, `${count.sell} sell`];
129
+ if (count.unknown) parts.push(`${count.unknown} side unknown`);
130
+ return `${rows.length} ${rows.length === 1 ? 'trade' : 'trades'}: ${parts.join(', ')}. Last: ${last.side === 'unknown' ? 'side unknown,' : last.side} ${last.size} at ${last.price}, ${last.time} UTC.`;
131
+ }
132
+
133
+ export type TapeMove = 'next' | 'prev' | 'first' | 'last';
134
+ export type TapeKeyAction = { type: 'move'; to: TapeMove } | { type: 'select' };
135
+
136
+ interface KeyLike { key: string; ctrlKey: boolean; metaKey: boolean; altKey: boolean }
137
+
138
+ /** Arrow keys move between rows, Home is the newest row, End the oldest; Enter or Space picks the row. Nothing places an order. */
139
+ export function resolveTapeKey(event: KeyLike): TapeKeyAction | null {
140
+ if (event.ctrlKey || event.metaKey || event.altKey) return null;
141
+ switch (event.key) {
142
+ case 'ArrowDown': return { type: 'move', to: 'next' };
143
+ case 'ArrowUp': return { type: 'move', to: 'prev' };
144
+ case 'Home': return { type: 'move', to: 'first' };
145
+ case 'End': return { type: 'move', to: 'last' };
146
+ case 'Enter':
147
+ case ' ': return { type: 'select' };
148
+ default: return null;
149
+ }
150
+ }
151
+
152
+ /** The row index a move lands on; the ends stop. With no current row, next and first land on 0. */
153
+ export function nextTapeIndex(count: number, current: number, to: TapeMove): number {
154
+ if (count === 0) return -1;
155
+ if (to === 'first') return 0;
156
+ if (to === 'last') return count - 1;
157
+ if (current < 0) return to === 'next' ? 0 : count - 1;
158
+ return Math.min(count - 1, Math.max(0, current + (to === 'next' ? 1 : -1)));
159
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * A volume profile as plain functions: how much traded at each price level over a run of bars. Each bar's volume is
3
+ * spread evenly over the price rows its high-low range touches. Nothing here throws: an empty run, a flat price range or
4
+ * zero volume gives a profile with no point of control (`poc` is -1, `vah` and `val` are null).
5
+ */
6
+ import type { CandleBar } from './chart-scale.js';
7
+
8
+ export interface ProfileRow {
9
+ /** Bottom and top price of the row. */
10
+ low: number;
11
+ high: number;
12
+ volume: number;
13
+ /** The part of `volume` from bars that closed up, and from bars that closed down. A bar that closed where it opened counts half each. */
14
+ upVolume: number;
15
+ downVolume: number;
16
+ }
17
+
18
+ export interface VolumeProfileOptions {
19
+ /** First and last bar index of the run, inclusive. */
20
+ first: number;
21
+ last: number;
22
+ /** Number of price rows, 1 to 500. Default 24. */
23
+ rows?: number;
24
+ /** Share of the volume the value area holds, above 0 up to 1. Default 0.7. */
25
+ valueArea?: number;
26
+ }
27
+
28
+ export interface VolumeProfileResult {
29
+ /** Lowest price first. */
30
+ rows: ProfileRow[];
31
+ /** Index of the point of control: the row with the most volume (the lowest one on a tie). -1 when there is no volume. */
32
+ poc: number;
33
+ /** Top of the highest row and bottom of the lowest row in the value area. Null when there is no volume. */
34
+ vah: number | null;
35
+ val: number | null;
36
+ /** The value area as row indexes [low, high]; [-1, -1] when there is none. */
37
+ valueAreaRows: [number, number];
38
+ total: number;
39
+ }
40
+
41
+ const EMPTY: VolumeProfileResult = { rows: [], poc: -1, vah: null, val: null, valueAreaRows: [-1, -1], total: 0 };
42
+
43
+ /**
44
+ * The profile of bars first..last. The value area starts at the point of control and takes whichever neighbouring row
45
+ * has more volume (the one above on a tie), one row at a time, until it holds the `valueArea` share of the total.
46
+ */
47
+ export function volumeProfile(bars: readonly CandleBar[], options: VolumeProfileOptions): VolumeProfileResult {
48
+ const first = Math.max(0, Math.floor(Number.isFinite(options.first) ? options.first : 0));
49
+ const last = Math.min(bars.length - 1, Math.floor(Number.isFinite(options.last) ? options.last : -1));
50
+ const count = Math.min(500, Math.max(1, Math.floor(Number.isFinite(options.rows) ? options.rows! : 24)));
51
+ const share = options.valueArea !== undefined && Number.isFinite(options.valueArea) && options.valueArea > 0 ? Math.min(1, options.valueArea) : 0.7;
52
+
53
+ let min = Infinity;
54
+ let max = -Infinity;
55
+ // The same bars as the allocation below: one with no volume would stretch the rows without adding to them.
56
+ for (let i = first; i <= last; i++) {
57
+ const { high, low, volume } = bars[i];
58
+ if (!Number.isFinite(high) || !Number.isFinite(low) || !Number.isFinite(volume) || volume <= 0) continue;
59
+ if (low < min) min = low;
60
+ if (high > max) max = high;
61
+ }
62
+ if (!(max >= min)) return { ...EMPTY, rows: [], valueAreaRows: [-1, -1] };
63
+
64
+ // A flat range is one row, so the volume still has somewhere to go.
65
+ const flat = max === min;
66
+ const size = flat ? 1 : count;
67
+ const step = flat ? 0 : (max - min) / size;
68
+ const rows: ProfileRow[] = [];
69
+ for (let r = 0; r < size; r++) rows.push({ low: flat ? min : min + r * step, high: flat ? max : r === size - 1 ? max : min + (r + 1) * step, volume: 0, upVolume: 0, downVolume: 0 });
70
+ const rowOf = (price: number) => (flat ? 0 : Math.min(size - 1, Math.max(0, Math.floor((price - min) / step))));
71
+
72
+ let total = 0;
73
+ for (let i = first; i <= last; i++) {
74
+ const bar = bars[i];
75
+ if (!Number.isFinite(bar.high) || !Number.isFinite(bar.low) || !Number.isFinite(bar.volume) || bar.volume <= 0) continue;
76
+ const from = rowOf(bar.low);
77
+ const to = rowOf(bar.high);
78
+ const part = bar.volume / (to - from + 1);
79
+ const up = bar.close > bar.open ? 1 : bar.close < bar.open ? 0 : 0.5;
80
+ for (let r = from; r <= to; r++) {
81
+ rows[r].volume += part;
82
+ rows[r].upVolume += part * up;
83
+ rows[r].downVolume += part * (1 - up);
84
+ }
85
+ total += bar.volume;
86
+ }
87
+ if (total <= 0) return { rows, poc: -1, vah: null, val: null, valueAreaRows: [-1, -1], total: 0 };
88
+
89
+ let poc = 0;
90
+ for (let r = 1; r < size; r++) if (rows[r].volume > rows[poc].volume) poc = r;
91
+
92
+ const target = total * share - total * 1e-9;
93
+ let low = poc;
94
+ let high = poc;
95
+ let held = rows[poc].volume;
96
+ while (held < target && (low > 0 || high < size - 1)) {
97
+ const above = high < size - 1 ? rows[high + 1].volume : -1;
98
+ const below = low > 0 ? rows[low - 1].volume : -1;
99
+ if (above >= below) { high++; held += rows[high].volume; } else { low--; held += rows[low].volume; }
100
+ }
101
+ return { rows, poc, vah: rows[high].high, val: rows[low].low, valueAreaRows: [low, high], total };
102
+ }
103
+
104
+ /** The bar indexes whose timestamps fall in [from, to], as {first, last}; last < first when none do. Bars are in time order. */
105
+ export function indexRangeOfTime(bars: readonly CandleBar[], from: number, to: number): { first: number; last: number } {
106
+ let first = bars.length;
107
+ let last = -1;
108
+ for (let i = 0; i < bars.length; i++) {
109
+ const t = bars[i].timestamp;
110
+ if (t >= from && t <= to) {
111
+ if (first === bars.length) first = i;
112
+ last = i;
113
+ }
114
+ }
115
+ return { first, last };
116
+ }
package/src/index.ts ADDED
@@ -0,0 +1,83 @@
1
+ export { numericWidth, primitiveAttributes } from './attributes.js';
2
+ export type { Change, CursorMark, DiffStatus, PrimitiveAttributes, PrimitiveState, Quality, Side } from './attributes.js';
3
+ export { Notice } from './Notice.js';
4
+ export type { NoticeKind, NoticeProps } from './Notice.js';
5
+ export { useLadderKeys } from './ladder-keys.js';
6
+ export { LADDER_ROW_ATTRIBUTE, nextLadderRow, resolveLadderKey } from './core/ladder-keys.js';
7
+ export type { LadderKeyAction } from './ladder-keys.js';
8
+ export { Transport } from './Transport.js';
9
+ export type { TransportRootProps, TransportSpeedProps } from './Transport.js';
10
+ export { CURSOR_START, Cursor, sameCursor, useCursor } from './Cursor.js';
11
+ export type { CursorBoundary, CursorRootProps, CursorState, CursorValue } from './Cursor.js';
12
+ export { Scrubber, useScrubber } from './Scrubber.js';
13
+ export type { ScrubberMarkProps, ScrubberRootProps, ScrubberThumbProps } from './Scrubber.js';
14
+ export { clampFraction, fractionFromPointer, isActiveScrubPointer, isPrimaryScrubPointer, reduceScrubGesture, resolveScrubKey } from './core/scrubber-gesture.js';
15
+ export type { ScrubGestureEvent, ScrubGestureResult, ScrubKeyAction, ScrubPointer } from './core/scrubber-gesture.js';
16
+ export { DepthProfile } from './DepthProfile.js';
17
+ export type { DepthProfileRootProps } from './DepthProfile.js';
18
+ export { depthGeometry, depthLevelId, nextDepthIndex, resolveDepthKey } from './core/depth-profile.js';
19
+ export type { DepthGeometry, DepthKeyAction, DepthLevel } from './core/depth-profile.js';
20
+ export { DiffTable } from './DiffTable.js';
21
+ export type { DiffTableRootProps } from './DiffTable.js';
22
+ export { DIFF_CHANGE_LABEL, compareExact, diffRowId, holdDiffOrder, nextDiffIndex, nextDiffSort, resolveDiffKey, signedDelta, sortDiffRows } from './core/diff-table.js';
23
+ export type { DiffChange, DiffKeyAction, DiffMove, DiffSort, DiffTableRow } from './core/diff-table.js';
24
+ export { Tape } from './Tape.js';
25
+ export type { TapeRootProps } from './Tape.js';
26
+ export { TAPE_SIDE_LABEL, holdTape, newestFirst, nextTapeIndex, readTape, resolveTapeKey, tapeSummary, tapeTime } from './core/tape.js';
27
+ export type { TapeIssue, TapeKeyAction, TapeMove, TapeRead, TapeRow, TapeSide, TapeTrade } from './core/tape.js';
28
+ export { Ladder } from './Ladder.js';
29
+ export type { LadderRootProps } from './Ladder.js';
30
+ export { buildLadder } from './core/ladder.js';
31
+ export type { LadderBuilt, LadderInput, LadderIssue, LadderLevelIn, LadderRow, LadderSide } from './core/ladder.js';
32
+ export { Receipt } from './Receipt.js';
33
+ export type { ReceiptRootProps } from './Receipt.js';
34
+ export { receiptHash, receiptVerdict, resolveReceiptKey } from './core/receipt-rules.js';
35
+ export type { ReceiptData, ReceiptHashOf, ReceiptVerdict } from './core/receipt-rules.js';
36
+ export { SymbolSearch } from './SymbolSearch.js';
37
+ export type { SymbolSearchRootProps } from './SymbolSearch.js';
38
+ export { buildSymbolIndex, nextSymbolIndex, normalizeSymbolText, resolveSymbolKey, searchSymbols } from './core/symbol-search.js';
39
+ export type { Instrument, MatchHow, MatchedOn, SearchOptions, SymbolIndex, SymbolKeyAction, SymbolMatch } from './core/symbol-search.js';
40
+ export { Chart, useChart } from './Chart.js';
41
+ export { heikinAshi } from './core/candles.js';
42
+ export type { CandleStyle, ChartContext, ChartRootProps, TimeAxisProps } from './Chart.js';
43
+ export { barInterval, chartFrame, clampPriceRange, dataPriceSpan, growViewport, indexOfTime, lastBars, panPriceRange, panViewport, priceTicks, resolveChartKey, tickDecimals, timeOfIndex, zoomPriceRange, zoomViewport } from './core/chart-scale.js';
44
+ export type { CandleBar, ChartFrame, ChartFrameOptions, ChartInclude, PriceSpan, ChartKeyAction, ChartSize, ChartViewport } from './core/chart-scale.js';
45
+ export { Study } from './Studies.js';
46
+ export type { StudyBandProps, StudyLegendProps, StudyLineProps } from './Studies.js';
47
+ export { atr, bollinger, ema, rsi, sma, sourcePrice, studyRange, visibleIndexes, vwap, wma } from './core/studies.js';
48
+ export type { BollingerBands, StudySource, VwapBand, VwapOptions, VwapResult } from './core/studies.js';
49
+ export { VolumeProfile } from './VolumeProfile.js';
50
+ export type { VolumeProfileHistogramProps, VolumeProfileOptionsProps } from './VolumeProfile.js';
51
+ export { indexRangeOfTime, volumeProfile } from './core/volume-profile.js';
52
+ export type { ProfileRow, VolumeProfileOptions, VolumeProfileResult } from './core/volume-profile.js';
53
+ export type { NameProps } from './accessible-name.js';
54
+ export { Toolbar } from './Toolbar.js';
55
+ export type { ToolbarButtonProps, ToolbarRootProps } from './Toolbar.js';
56
+ export { Menu } from './Menu.js';
57
+ export type { MenuItemProps, MenuListProps, MenuRadioItemProps, MenuRootProps, MenuTriggerProps } from './Menu.js';
58
+ export { Submenu } from './Submenu.js';
59
+ export { Drawings, useDrawings } from './Drawings.js';
60
+ export type { DrawingsEditProps, DrawingsListProps, DrawingsLockProps, DrawingsRemoveProps, DrawingsRootProps, DrawingsToolbarProps, DrawingsUndoProps } from './Drawings.js';
61
+ export { ATTACH_LABELS, ATTACH_MODES, DRAWING_TOOLS, DRAWING_TOOL_LABELS, FIB_RATIOS, anchorCount, attachOf, barTime, barsFromFirst, barsFromLast, timeOfBarFromLast, describeAnchor, describeDrawing, drawingGeometry, drawingPriceRanges, editDrawing, hitTest, moveAnchor, moveDrawing, nextDrawingId, nudgeAmount, resolveDrawingKey, snapToBar } from './core/drawings.js';
62
+ export type { AttachMode, Drawing, DrawingAnchor, DrawingGeometry, DrawingHit, DrawingKeyAction, DrawingTool, SnapMode } from './core/drawings.js';
63
+ export { Selection, Series } from './Selection.js';
64
+ export type { SelectionRootProps, SeriesRootProps } from './SelectionParts.js';
65
+ export { describeSelection, sameSelection, selectionAfterClearTap, selectionAfterEscape, selectionAfterTap } from './core/selection.js';
66
+ export type { ChartSelection, SelectionNames } from './core/selection.js';
67
+ export { CsvDrop } from './CsvDrop.js';
68
+ export type { CsvDropLoad, CsvDropRootProps } from './CsvDrop.js';
69
+ export { describeBarsCsv, parseBarsCsv } from './core/csv-bars.js';
70
+ export type { CsvBarsError, CsvBarsNote, CsvBarsOptions, CsvBarsResult, CsvExtraColumn } from './core/csv-bars.js';
71
+ export { Fullscreen, useFullscreen } from './Fullscreen.js';
72
+ export type { FullscreenFrameProps, FullscreenState, FullscreenToggleProps } from './Fullscreen.js';
73
+ export { FULLSCREEN_LABEL, FULLSCREEN_MESSAGE, resolveFullscreenEscape } from './core/fullscreen.js';
74
+ export type { FullscreenEscape } from './core/fullscreen.js';
75
+ export { Panels, usePanels } from './Panels.js';
76
+ export type { PanelsListProps, PanelsMoveProps, PanelsRootProps } from './PanelsParts.js';
77
+ export type { PanelSeriesData } from './panels-context.js';
78
+ export { AXIS_BOTTOM, MAIN_PANEL, describeMove, moveSeries, moveTargets, normalizeLayout, panelBands, panelOf, reconcileLayout } from './core/panels.js';
79
+ export type { MoveDestination, MoveTarget, PanelBand, PanelLayout, PanelSeriesRequest, PanelSpec } from './core/panels.js';
80
+
81
+ // The series contract and the adapters (also under ./core): points with a clock, the boundary check, the report, the CSV adapter.
82
+ export { POINT_KINDS, alignSeries, csvAdapter, duplicateIds, pointsFromValues, report, resolveSeries, seriesSetSchema, toSeriesSet, validateSeriesSet } from './core/index.js';
83
+ export type { AdapterIssue, AdapterResult, AlignMode, AlignOptions, AlignedSeries, AlignedState, CsvAdapterOptions, DataAdapter, DataSeries, FindingKind, PointKind, ResolvedSeries, SeriesCalendar, SeriesFinding, SeriesInput, SeriesIssue, SeriesPoint, SeriesSet, SeriesValidation } from './core/index.js';