@trdrs/quickcharts 1.2.0 → 2.0.0

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 (85) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +742 -180
  3. package/dist/adapters/rest.d.ts +3 -3
  4. package/dist/adapters/rest.js +2 -2
  5. package/dist/adapters/rest.js.map +1 -1
  6. package/dist/{ar-G7MVXRE4.js → ar-KSXQ2USS.js} +20 -20
  7. package/dist/ar-KSXQ2USS.js.map +1 -0
  8. package/dist/{assets-ySndlUS3.d.ts → assets-DOuBBsOp.d.ts} +50 -50
  9. package/dist/{ca_ES-EUSRU4VG.js → ca_ES-JG4FM55E.js} +20 -20
  10. package/dist/ca_ES-JG4FM55E.js.map +1 -0
  11. package/dist/{chunk-RDAK5MUM.js → chunk-6S6TO2N6.js} +41 -37
  12. package/dist/chunk-6S6TO2N6.js.map +1 -0
  13. package/dist/{chunk-KFBQSPNA.js → chunk-D3S26ARP.js} +2 -2
  14. package/dist/chunk-D3S26ARP.js.map +1 -0
  15. package/dist/{chunk-AKXIFHQN.js → chunk-KLPTCWA2.js} +1 -1
  16. package/dist/{chunk-AKXIFHQN.js.map → chunk-KLPTCWA2.js.map} +1 -1
  17. package/dist/{de-2SVTG25S.js → de-VSW2LHNF.js} +20 -20
  18. package/dist/de-VSW2LHNF.js.map +1 -0
  19. package/dist/drawings.d.ts +16 -16
  20. package/dist/drawings.js +12 -12
  21. package/dist/{es-UDHZM4W7.js → es-5ZXIPVOB.js} +20 -20
  22. package/dist/es-5ZXIPVOB.js.map +1 -0
  23. package/dist/feature-manifest.json +51 -3
  24. package/dist/{fr-35VN4G4S.js → fr-4F6DAWGD.js} +20 -20
  25. package/dist/fr-4F6DAWGD.js.map +1 -0
  26. package/dist/{he_IL-JPZMCURP.js → he_IL-B7PBQUEV.js} +20 -20
  27. package/dist/he_IL-B7PBQUEV.js.map +1 -0
  28. package/dist/{id_ID-XSTFHHJQ.js → id_ID-CYJRMXPC.js} +20 -20
  29. package/dist/id_ID-CYJRMXPC.js.map +1 -0
  30. package/dist/index.d.ts +599 -225
  31. package/dist/index.js +10110 -9106
  32. package/dist/index.js.map +1 -1
  33. package/dist/{it-EIY6JESQ.js → it-KTX4SG5P.js} +20 -20
  34. package/dist/it-KTX4SG5P.js.map +1 -0
  35. package/dist/{ja-42N4XRCW.js → ja-V7OURU5D.js} +20 -20
  36. package/dist/ja-V7OURU5D.js.map +1 -0
  37. package/dist/{ko-3VRPTIP3.js → ko-2RNEBYVE.js} +20 -20
  38. package/dist/ko-2RNEBYVE.js.map +1 -0
  39. package/dist/{ms_MY-SKN7O6M5.js → ms_MY-EY235E7Q.js} +20 -20
  40. package/dist/ms_MY-EY235E7Q.js.map +1 -0
  41. package/dist/{pl-TYARHCLT.js → pl-ADNVRP55.js} +20 -20
  42. package/dist/pl-ADNVRP55.js.map +1 -0
  43. package/dist/{pt-72IIFLL4.js → pt-OL2DR3DO.js} +20 -20
  44. package/dist/pt-OL2DR3DO.js.map +1 -0
  45. package/dist/quickcharts.css +218 -205
  46. package/dist/{resources-Ch8eA1Mw.d.ts → resources-Le6flcEn.d.ts} +3 -3
  47. package/dist/rest-openapi.json +11 -11
  48. package/dist/{ru-TPH4F7IB.js → ru-V6YFEPAF.js} +20 -20
  49. package/dist/ru-V6YFEPAF.js.map +1 -0
  50. package/dist/{sv-DC5RS67Z.js → sv-TORXQVE7.js} +20 -20
  51. package/dist/sv-TORXQVE7.js.map +1 -0
  52. package/dist/{th-I6H6AOZ3.js → th-K6SJSYSY.js} +20 -20
  53. package/dist/th-K6SJSYSY.js.map +1 -0
  54. package/dist/theme-manifest.json +99 -11
  55. package/dist/{tr-7FSUVCNW.js → tr-4AEM66KW.js} +20 -20
  56. package/dist/tr-4AEM66KW.js.map +1 -0
  57. package/dist/{vi-DLRPHLLH.js → vi-YU5XCEUU.js} +20 -20
  58. package/dist/vi-YU5XCEUU.js.map +1 -0
  59. package/dist/{zh-ZXIQBBEP.js → zh-QXKRYHU5.js} +20 -20
  60. package/dist/zh-QXKRYHU5.js.map +1 -0
  61. package/dist/{zh_TW-TYNUJU7U.js → zh_TW-AYPDKG4Q.js} +20 -20
  62. package/dist/zh_TW-AYPDKG4Q.js.map +1 -0
  63. package/package.json +1 -1
  64. package/dist/ar-G7MVXRE4.js.map +0 -1
  65. package/dist/ca_ES-EUSRU4VG.js.map +0 -1
  66. package/dist/chunk-KFBQSPNA.js.map +0 -1
  67. package/dist/chunk-RDAK5MUM.js.map +0 -1
  68. package/dist/de-2SVTG25S.js.map +0 -1
  69. package/dist/es-UDHZM4W7.js.map +0 -1
  70. package/dist/fr-35VN4G4S.js.map +0 -1
  71. package/dist/he_IL-JPZMCURP.js.map +0 -1
  72. package/dist/id_ID-XSTFHHJQ.js.map +0 -1
  73. package/dist/it-EIY6JESQ.js.map +0 -1
  74. package/dist/ja-42N4XRCW.js.map +0 -1
  75. package/dist/ko-3VRPTIP3.js.map +0 -1
  76. package/dist/ms_MY-SKN7O6M5.js.map +0 -1
  77. package/dist/pl-TYARHCLT.js.map +0 -1
  78. package/dist/pt-72IIFLL4.js.map +0 -1
  79. package/dist/ru-TPH4F7IB.js.map +0 -1
  80. package/dist/sv-DC5RS67Z.js.map +0 -1
  81. package/dist/th-I6H6AOZ3.js.map +0 -1
  82. package/dist/tr-7FSUVCNW.js.map +0 -1
  83. package/dist/vi-DLRPHLLH.js.map +0 -1
  84. package/dist/zh-ZXIQBBEP.js.map +0 -1
  85. package/dist/zh_TW-TYNUJU7U.js.map +0 -1
package/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # @trdrs/quickcharts
2
2
 
3
- Quick Charts is a charting library that draws over the datafeed and the storage you supply. The chart
4
- consumes the `ChartDatafeed` interface and never a concrete backend, so your feed drives it without a
5
- change to the chart. It does not include market data, trading, accounts, execution, community, news
6
- or hosting: the host supplies data and storage, and the chart draws.
3
+ Quick Charts is a datafeed-driven charting library with drawings, indicators, layouts and replay.
4
+ It draws over the datafeed and the storage you supply: the chart consumes the `ChartDatafeed`
5
+ interface, so your feed drives it without a change to the chart.
7
6
 
8
7
  ## Install
9
8
 
@@ -21,8 +20,8 @@ runtime. Use default peer resolution; do not suppress peer checks.
21
20
  Quick Charts is licensed under the Apache License 2.0: see `LICENSE` and `NOTICE`. Because the
22
21
  renderer is *your* dependency, its own Apache-2.0 NOTICE
23
22
  obligations attach to **your** bundle: `THIRD-PARTY-NOTICES.md` in this package spells out exactly
24
- what to carry and how. The chart includes no trading, accounts or executions; an application that
25
- trades composes those outside the chart, through the extension seam below.
23
+ what to carry and how. An application composes its own features on the chart through the extension
24
+ seam below.
26
25
 
27
26
  Quickstart, the smallest working chart (see [The widget](#the-widget) for the full options). The
28
27
  stylesheet import is not optional: it carries the chart's layout as well as its look, and without it
@@ -44,9 +43,8 @@ await widget.ready()
44
43
  Implement `ChartDatafeed` (see `datafeed.ts`). Required methods: `search`, `resolve`, `history`,
45
44
  `subscribeBars`. Optional: `serverTime` (countdown skew correction) and `config` (a feed-level
46
45
  capability declaration, described [below](#capability-declaration-config-optional)). The datafeed serves
47
- symbol metadata, bars and bar updates and nothing else: a quote board (last, change, volume) or a
48
- top-of-book is not a chart concern, and your host fans quotes to its own consumers from its own
49
- source.
46
+ symbol metadata, bars and bar updates; your host serves quotes (last, change, volume, the top of
47
+ book) to its own consumers from its own source.
50
48
 
51
49
  When `appearance.countdown` is enabled, a live streaming time bar replaces the native last-value
52
50
  label with one price-and-time label. The price uses the resolved symbol formatter. Replay, stale,
@@ -176,8 +174,8 @@ export const feed: ChartDatafeed = {
176
174
  ```
177
175
 
178
176
  Declare only what is **true**. Absent method / absent field / empty list = unconstrained, so a feed that
179
- serves any interval must not declare a finite `resolutions` list, because the widget then enforces it.
180
- The UDF adapter declares automatically from the server's own `/config` (and only ever declares
177
+ serves any timeframe must not declare a finite `resolutions` list, because the widget then enforces
178
+ it. The UDF adapter declares automatically from the server's own `/config` (and only ever declares
181
179
  timeframes it would actually serve).
182
180
 
183
181
  ## The UDF on-ramp
@@ -213,10 +211,11 @@ What the adapter honors of the protocol:
213
211
 
214
212
  Symbology is the set of display facts that decide how a market's prices are written. Your datafeed
215
213
  owns them (`resolve` answers with `SymbolInfo`), and one package formatter uses them everywhere a
216
- price appears: the price scale, the crosshair and last-price labels, the legend, the level menu,
217
- every drawing label, study scales, and the extension seam. Precision comes from the symbol, never
218
- from the size of the price, so the same market reads at the same width on every surface. A study
219
- that declares its own precision keeps it; every other value writes through the symbol formatter.
214
+ price appears: the price scale, the crosshair and last-price labels, the legend, the context menu,
215
+ every drawing label, indicator scales, and the extension seam. Precision comes from the symbol,
216
+ never from the size of the price, so the same market reads at the same width on every surface. An
217
+ indicator that declares its own precision keeps it; every other value writes through the symbol
218
+ formatter.
220
219
 
221
220
  `PriceFormat` expresses every supported form in five facts:
222
221
 
@@ -303,8 +302,8 @@ symbolNames({ symbol: 'NASDAQ:AAPL', name: 'Apple Inc', exchange: 'NASDAQ', type
303
302
  symbolNames('HYPERLIQUID:ETH').mark // 'ETH', before the symbol resolves
304
303
  ```
305
304
 
306
- Symbology is display truth, not trading truth. It carries no order quantity, price step, lot size,
307
- or pip value. Your broker integration owns those, and a broker's execution grid can differ from a
305
+ Symbology is display truth: how the chart writes a market's prices. Execution facts, such as lot
306
+ size and pip value, stay with your trading integration, and its execution grid can differ from the
308
307
  chart's display grid.
309
308
 
310
309
  If your data comes from a UDF server, map its `/symbols` answer without collapsing the facts:
@@ -322,14 +321,14 @@ info?.dataStatus // 'streaming'
322
321
 
323
322
  Quick Charts persists two different kinds of thing, and the difference decides where each one lives.
324
323
 
325
- Saved charts, layouts, drawing documents and templates are **entities**: a user names them, opens
324
+ Saved charts, layouts, drawing documents and templates are **entities**: a viewer names them, opens
326
325
  them in two tabs, and can lose them. They have identity and versions, and they live on the
327
326
  save/load adapter you supply. Viewer preferences (the last symbol and timeframe, the scale mode, the
328
327
  replay speed) are **flat settings**: they need no identity, and they live on the small `ChartStorage`
329
328
  port beside it.
330
329
 
331
330
  The chart never writes on its own. It tells you when a save would be worth making, through the
332
- `saveNeeded` event, and it runs a write only when a user asks for one through the built-in UI or
331
+ `saveNeeded` event, and it runs a write only when a viewer asks for one through the built-in UI or
333
332
  when you call a save method yourself.
334
333
 
335
334
  ### What a saved entity carries
@@ -367,7 +366,7 @@ changes, and a menu with Save, an Autosave switch, Make a copy, Rename, Download
367
366
  new layout, the layouts used most recently, and Open layout: the Layouts dialog, with search, a sort
368
367
  by name or by date modified, a star that keeps a layout at the top, and delete behind a
369
368
  confirmation. The sort and the stars are the viewer's, kept in the widget's `ChartStorage`. Each
370
- saved layout is listed by the market and interval its active chart showed when
369
+ saved layout is listed by the market and timeframe its active chart showed when
371
370
  it was saved, or by its age where the store kept neither.
372
371
 
373
372
  Every row runs a command (`widget.layout.save`, `rename`, `load`, `open`, `delete`, `create`, `autosave`,
@@ -417,7 +416,7 @@ const adapter = memorySaveLoadAdapter()
417
416
  const controller = new AbortController()
418
417
 
419
418
  await adapter.layouts.list(controller.signal)
420
- await adapter.templates('study').list()
419
+ await adapter.templates('indicator').list()
421
420
  const context = { version: 1, kind: 'symbol-global', symbol: 'ESZ2026' } as const
422
421
  await adapter.drawings(context).create(emptyDrawingDocument(context))
423
422
 
@@ -428,8 +427,9 @@ outcome.kind // 'not-found'
428
427
 
429
428
  An aborted call rejects with an error named `AbortError` and changes nothing.
430
429
 
431
- `memorySaveLoadAdapter` persists nothing. Use it for tests, server rendering and an intentionally
432
- ephemeral embed, and implement the same contract over your own backend for durable storage.
430
+ `memorySaveLoadAdapter` keeps everything in memory for the life of the page. Use it for tests,
431
+ server rendering and an intentionally ephemeral embed, and implement the same contract over your
432
+ own backend for durable storage.
433
433
 
434
434
  ### The REST adapter and its wire contract
435
435
 
@@ -447,10 +447,11 @@ const restSaves = createRestSaveLoadAdapter({
447
447
  await restSaves.layouts.list()
448
448
  ```
449
449
 
450
- You supply two values and nothing else. `baseUrl` is where you mounted the routes, and `request` is
451
- how a request is made. Authorization, cookies, CORS, retries, timeouts and tenancy stay in your
452
- request function, because their consequences are yours: the adapter holds no credential, no token
453
- store, no header policy and no default origin, and it makes no request until you call a verb.
450
+ You supply two values. `baseUrl` is where you mounted the routes, and `request` is how a request is
451
+ made. Authorization, cookies, CORS, retries, timeouts and tenancy stay in your request function,
452
+ because their consequences are yours: the adapter builds each request from `baseUrl` and hands it to
453
+ your function, which adds the credentials and headers your service needs, and it makes a request
454
+ only when you call a verb.
454
455
 
455
456
  Your service serves four collections under that base URL. Every path below is relative to it.
456
457
 
@@ -532,8 +533,8 @@ remain in step while the menu is open; transient tile maximize does not change t
532
533
  names.
533
534
 
534
535
  Growing a layout keeps every existing chart instance and initializes each new tile from pane 0's
535
- safe presentation: symbol, timeframe, style, studies and comparisons. Mutable study configuration
536
- is copied per tile; replay, session state and saved-chart identity are not copied.
536
+ safe presentation: symbol, timeframe, style, indicators and comparisons. Mutable indicator
537
+ configuration is copied per tile; replay, session state and saved-chart identity are not copied.
537
538
 
538
539
  `load` answers rather than rejecting. Five kinds, told apart without reading a message:
539
540
 
@@ -587,13 +588,13 @@ switch (opened.kind) {
587
588
  A load that a later load supersedes is `cancelled`, with no signal of your own involved, and only
588
589
  the later load binds. This includes a later load begun synchronously by a chart callback during
589
590
  application. A widget disposed while the store is still answering answers the same way. That is
590
- what keeps a slow answer for the chart a user has left from landing on the chart they are looking
591
+ what keeps a slow answer for the chart a viewer has left from landing on the chart they are looking
591
592
  at.
592
593
 
593
594
  Where a body fails after part of it has been applied, the chart puts back the content it held. If
594
595
  that fails too the chart is holding neither, and it stops saving: `saveLoad.notSaving()` reports it,
595
596
  `save` answers `not-saving` and writes nothing, the built-in layouts menu says so on the toolbar and
596
- stands its Save and its autosave down, and the sentence for the trader arrives on `saveConflict`.
597
+ stands its Save and its autosave down, and the sentence for the viewer arrives on `saveConflict`.
597
598
 
598
599
  Saving starts again when a load lands, and only then: a whole content out of the store, applied in
599
600
  full, under the resource it came from. `save(name, { asNew: true })` stays available throughout, and
@@ -623,7 +624,7 @@ Complete recovery requires every typed appearance leaf with a valid concrete col
623
624
  in combined mode, a drawings array. Separate-mode chart content does not need that array.
624
625
 
625
626
  Hydration and rollback do not emit `saveNeeded` for their own storage writes. An event already
626
- queued by a user edit is preserved. A late save result does not replace a newer load's binding;
627
+ queued by a viewer's edit is preserved. A late save result does not replace a newer load's binding;
627
628
  the built-in commands announce a saved layout only while that result is still current.
628
629
 
629
630
  ### Drawings: combined or separate
@@ -676,18 +677,18 @@ tile.
676
677
 
677
678
  ### Templates
678
679
 
679
- `adapter.templates(kind)` is a store per kind: `'study'` for indicator settings, `'drawing'` for a
680
- drawing tool's look, and `'palette'` for chart appearance. A drawing template carries the `tool` it
681
- belongs to, because a trend-line template means nothing on a rectangle. Their content is opaque,
680
+ `adapter.templates(kind)` is a store per kind: `'indicator'` for indicator settings, `'drawing'` for
681
+ a drawing tool's look, and `'palette'` for chart appearance. A drawing template carries the `tool`
682
+ it belongs to, because a trend-line template means nothing on a rectangle. Their content is opaque,
682
683
  like a chart's.
683
684
 
684
- ### User settings
685
+ ### Viewer settings
685
686
 
686
687
  The widget keeps the viewer's flat preferences in a `ChartStorage`. Every key is an opaque string in
687
688
  the `quickcharts.` namespace, and every value is an opaque string; a store routes or scopes them and
688
- reads neither. The default is an in-memory store that lasts the page; supply your own to keep them
689
- per device or per account. A browser store is a few lines you write; it is not part of the package,
690
- because a device-local default is not a persistence architecture:
689
+ treats both as opaque. The default is an in-memory store that lasts the page; supply your own to
690
+ keep them per device or per viewer. A browser store is a few lines you write and own, so where
691
+ preferences live stays your decision:
691
692
 
692
693
  ```ts
693
694
  import { memoryChartStorage, type ChartStorage } from '@trdrs/quickcharts'
@@ -710,7 +711,7 @@ live on the saved-resource adapter above, never here.
710
711
  A conflict means someone else wrote the same entity after the copy on screen was read. The chart
711
712
  never resolves that by overwriting. It reports the case with a sentence from its own catalog and the
712
713
  ref that stands now, and leaves the decision to you: reload and lose the local edit, save a copy
713
- under a new name, or show the user both.
714
+ under a new name, or show the viewer both.
714
715
 
715
716
  Reach the report in two places. A verb you called answers a typed `conflict` outcome. A write the
716
717
  widget made for the viewer, through the built-in UI or its own drawing sync, arrives on the
@@ -863,7 +864,7 @@ widget.dispose() // teardown; every handle and subscription is inert afterwards
863
864
  The widget paints the main series and volume, applies live updates by the bar rules above, pages
864
865
  older history in as the viewer scrolls left (stopping at the feed's `noData`), persists the viewer's
865
866
  preferences through `ChartStorage`, runs configured indicator instances through the manifest
866
- pipeline, and mounts the drawing layer, the legend and the level menu.
867
+ pipeline, and mounts the drawing layer, the legend and the context menu.
867
868
 
868
869
  When older main or comparison history extends the renderer's shared timeline, the chart retains
869
870
  the latest fractional logical window, spacing and permitted off-data padding. Its compensating
@@ -875,7 +876,7 @@ the maintenance span or either endpoint. Public range methods and built-in navig
875
876
  enter that same epoch, so every supported navigation door has the same precedence.
876
877
 
877
878
  `toolbarContainer` optionally supplies a separate element for the top toolbar, such as a space
878
- between a host's navigation and account controls. The widget appends one package-owned, themed
879
+ between a host's navigation and its other controls. The widget appends one package-owned, themed
879
880
  child there; it never clears or styles the supplied element. The toolbar follows the widget's
880
881
  theme, language and active chart, and its child is removed at disposal. The toolbar's menus and
881
882
  dialogs open in the widget's layer on the document body, at viewport coordinates, so each menu
@@ -885,12 +886,13 @@ toolbar available in fullscreen. Without `toolbarContainer`, the toolbar sits ab
885
886
  part of the default composition. When horizontal space is limited, the toolbar scrolls without
886
887
  dropping controls from the keyboard order.
887
888
 
888
- `drawingToolbarContainer` supplies separate host space for a single drawing rail. The package
889
+ `drawingToolbarContainer` supplies separate host space for a single drawing toolbar. The package
889
890
  fills that space's height with its own themed child, follows the active chart when a layout
890
- changes, and removes the child on disposal. An external rail leaves the full chart width for the
891
- canvas; without the option, each chart keeps its internal rail and reserved leading column.
892
- Drawing flyouts stay beside the external rail within the widget's bounds regardless of the active
893
- pane. Switching charts closes an open flyout before the rail targets the newly active chart.
891
+ changes, and removes the child on disposal. An external drawing toolbar leaves the full chart width
892
+ for the canvas; without the option, each chart keeps its internal drawing toolbar and reserved
893
+ leading column. Drawing flyouts stay beside the external drawing toolbar within the widget's bounds
894
+ regardless of the active pane. Switching charts closes an open flyout before the drawing toolbar
895
+ targets the newly active chart.
894
896
  Include this container in the custom fullscreen target alongside the chart and top toolbar.
895
897
 
896
898
  ### Widget and chart
@@ -918,14 +920,277 @@ function drive(widget: ChartWidget): void {
918
920
  }
919
921
  ```
920
922
 
921
- ### The seven chart styles
923
+ ### Chart styles
922
924
 
923
- `candles`, `hollow`, `bars`, `line`, `area`, `baseline`, `stepline`, listed in picker order as
924
- `CHART_STYLES`. A style switch is presentation: nothing refetches, and the loaded bars, indicators,
925
- drawings, comparisons, scale and visible range all survive it. Four of the seven are value-shaped
925
+ `candles`, `hollow`, `bars`, `line`, `area`, `baseline`, `stepline`, listed as `CHART_STYLES`. A
926
+ style switch is presentation: nothing refetches, and the loaded bars, indicators, drawings,
927
+ comparisons, scale and visible range all survive it. Four of the seven are value-shaped
926
928
  (`valueShaped(style)`), which is the one predicate a host branches on when it renders open, high and
927
929
  low values of its own.
928
930
 
931
+ By default a chart offers all seven styles, and the style picker groups them by family. `styles`
932
+ names the styles you offer, in the order the picker lists them, and `style` must be one of them:
933
+
934
+ ```ts
935
+ import { createChart, type ChartDatafeed } from '@trdrs/quickcharts'
936
+
937
+ declare const datafeed: ChartDatafeed
938
+
939
+ // Candles, line and area, in this order, opening on the line.
940
+ createChart({ container, datafeed, styles: ['candles', 'line', 'area'], style: 'line' })
941
+
942
+ // Every style and no picker: a control of your own runs the chart.style.<id> commands.
943
+ createChart({ container, datafeed, ui: { topBar: { styles: false } } })
944
+
945
+ // Candles and line, with no picker.
946
+ createChart({ container, datafeed, styles: ['candles', 'line'], ui: { topBar: { styles: false } } })
947
+ ```
948
+
949
+ A style you leave out has no `chart.style.<id>` command, so no menu, shortcut or host control
950
+ reaches it, and `setStyle` ignores it. With one style offered the picker is not shown, since there
951
+ is nothing to choose. `ui.topBar.styles: false` hides the picker on its own terms, so you can
952
+ restrict the set, hide the picker, do both, or do neither. A saved layout, saved chart or stored
953
+ preference that names a style you left out opens on the first style in `styles`, and the rest of it
954
+ restores. An empty list, an id outside `CHART_STYLES`, a repeated id and a `style` outside the list
955
+ are setup errors that `createChart` throws.
956
+
957
+ ### Offered timeframes
958
+
959
+ By default a chart offers the 26 presets (`TIMEFRAME_PRESETS`) and lets the viewer compose custom
960
+ timeframes. `timeframes` names the timeframe tokens you offer, presets or any other token the
961
+ grammar reads, and `timeframe` must be one of them. The order you give does not matter: the picker
962
+ lists each unit's group smallest first. `customTimeframes: false` keeps the presets and removes
963
+ custom timeframes:
964
+
965
+ ```ts
966
+ import { createChart, type ChartDatafeed } from '@trdrs/quickcharts'
967
+
968
+ declare const datafeed: ChartDatafeed
969
+
970
+ // Four timeframes, one of them beyond the presets, opening on five minutes.
971
+ createChart({ container, datafeed, timeframes: ['2m', '5m', '1h', '1d'], timeframe: '5m' })
972
+
973
+ // Every preset and no custom timeframes.
974
+ createChart({ container, datafeed, customTimeframes: false })
975
+
976
+ // Two timeframes and no picker: a control of your own runs the chart.timeframe.<token> commands.
977
+ createChart({ container, datafeed, timeframes: ['1m', '5m'], ui: { topBar: { timeframes: false } } })
978
+ ```
979
+
980
+ A preset you leave out has no `chart.timeframe.<token>` command, and `chart.timeframe.set` and
981
+ `setTimeframe` ignore any token you leave out. The picker shows only what you list: a group with
982
+ no listed token is not drawn, a listed token beyond the presets sits in its unit's group without a
983
+ delete, and there is no custom-timeframe composer. The viewer's saved chips show where you list
984
+ them. When you list none of them, the chips are your five smallest timeframes, and the viewer's
985
+ stored chips stay stored for a chart that offers them. With one timeframe offered the picker is not
986
+ shown. `ui.topBar.timeframes: false` hides the picker on its own terms. A range preset whose
987
+ timeframe you leave out reads its span at your nearest coarser timeframe, or at your largest when
988
+ none is coarser. Within what you offer, the feed's `resolutions` and the symbol's
989
+ `supportedResolutions` still apply: a chip the feed or the symbol does not serve is disabled, and
990
+ the picker's list leaves its row out.
991
+
992
+ A saved layout, saved chart or stored preference that names a timeframe you left out opens on your
993
+ smallest timeframe, and the rest of it restores. With `customTimeframes: false` a stored custom
994
+ token opens on `1m`. An empty list, a token the grammar cannot read, a repeated token, a
995
+ `timeframe` or `layout.charts[].timeframe` outside what you offer, and `customTimeframes: true`
996
+ beside a list are setup errors that `createChart` throws. `customTimeframes` is the switch, and
997
+ `preferences.customTimeframes` is the list of custom tokens a first-run viewer starts with.
998
+
999
+ ### Offered layouts
1000
+
1001
+ By default a chart offers all 55 arrangements (`ARRANGEMENTS`) and lets the viewer change every
1002
+ sync switch. `layouts` names the arrangement codes you offer, and the layout opens on
1003
+ `layout.arrangement` or else on the first code you list. `layoutSync` names the sync switches the
1004
+ viewer may change:
1005
+
1006
+ ```ts
1007
+ import { createChart, type ChartDatafeed, type ChartSaveLoadAdapter } from '@trdrs/quickcharts'
1008
+
1009
+ declare const datafeed: ChartDatafeed
1010
+ declare const saveLoad: ChartSaveLoadAdapter
1011
+
1012
+ // A single chart: no layout setup menu, and no re-tile from any door.
1013
+ createChart({ container, datafeed, layouts: ['s'] })
1014
+
1015
+ // One chart, two side by side or a 2x2 grid, with the crosshair always synced and symbol sync the
1016
+ // only switch the viewer changes.
1017
+ createChart({ container, datafeed, layouts: ['s', '2h', '4'], layoutSync: ['symbol'], layout: { sync: { crosshair: true } } })
1018
+
1019
+ // Every arrangement, with the layout setup menu kept and the saved-layouts menu hidden.
1020
+ createChart({ container, datafeed, saveLoad, ui: { topBar: { savedLayouts: false } } })
1021
+ ```
1022
+
1023
+ The layout setup menu shows only the arrangements you list, in its own rows by chart count, and
1024
+ leaves out a row with none of them. An arrangement you leave out has no tile, and
1025
+ `widget.layout.setArrangement` and its command ignore it. With one arrangement offered there is
1026
+ nothing to choose, so the menu is not shown, unless that arrangement holds several charts and a
1027
+ sync switch is offered, in which case the menu holds the switches alone. A switch you leave out holds
1028
+ the value `layout.sync` gives it, or off: the menu does not show it, `widget.layout.setSync` and its
1029
+ command leave it as it is, and a saved layout cannot change it. An empty `layoutSync` fixes every
1030
+ switch.
1031
+
1032
+ A saved layout whose arrangement you left out opens on the offered arrangement with the most charts
1033
+ not above the saved count, the first one listed winning a tie. When every offered arrangement holds
1034
+ more charts, it opens on the one with the fewest, and the extra panes clone the first chart as a
1035
+ re-tile does. The saved charts past the ones shown are not dropped. The layout carries them with the
1036
+ saved arrangement and its divider positions, and a save writes them back unchanged beside the shown
1037
+ charts as they are now: a host offering only `['s']` opens a saved 4-chart layout as one chart, and
1038
+ after it saves, a host offering every arrangement still opens all four. A re-tile is a new
1039
+ arrangement and ends the carry, as it tears down the charts it has no pane for. A saved active chart
1040
+ among the hidden ones leaves the first chart active, and the sync switches restore as saved, apart
1041
+ from those `layoutSync` leaves out. An empty `layouts`, an unknown or repeated code, a
1042
+ `layout.arrangement` outside the list, and an unknown or repeated sync switch are setup errors that
1043
+ `createChart` throws.
1044
+
1045
+ `ui.topBar.layouts: false` hides the layout setup menu and the saved-layouts menu together;
1046
+ `ui.topBar.layoutSetup: false` and `ui.topBar.savedLayouts: false` hide one of them, and neither
1047
+ shows a menu `layouts: false` hid. The saved-layouts menu saves, opens and lists layouts, so it is
1048
+ shown only with a layouts store (`saveLoad.layouts`). Without one, its Download chart data row joins
1049
+ the image menu beside Download image and Copy image, the way an export menu offers a chart as a
1050
+ picture or as data.
1051
+
1052
+ ### Offered drawing tools
1053
+
1054
+ By default a chart offers every drawing tool. `drawingTools` names the tools you offer, by the type
1055
+ `access.drawingTool` and the `tool.<type>` icon ids use: a type `drawingTools.all()` lists (from
1056
+ `@trdrs/quickcharts/drawings`), `zoom` or `eraser`. The order you give does not matter, because the
1057
+ drawing toolbar keeps its own groups and sections:
1058
+
1059
+ ```ts
1060
+ import { createChart, type ChartDatafeed } from '@trdrs/quickcharts'
1061
+
1062
+ declare const datafeed: ChartDatafeed
1063
+
1064
+ // Lines, levels and a ruler: no shapes, patterns, notes or glyphs.
1065
+ createChart({ container, datafeed, drawingTools: ['trend_line', 'horizontal_line', 'fib_retracement', 'measure'] })
1066
+
1067
+ // A chart whose viewers keep and remove the drawings on it and make nothing new.
1068
+ createChart({ container, datafeed, drawingTools: ['eraser'] })
1069
+ ```
1070
+
1071
+ A tool you leave out is not drawn anywhere a tool is chosen: not in its group's flyout, not as a
1072
+ group's face, which wears the first tool the group offers, not on the favorites bar, and not on the
1073
+ drawing toolbar for `measure` and `zoom`. A section or group it empties goes with it. The glyph
1074
+ picker's kinds are the `emoji`, `sticker` and `icon` tools, so a kind you leave out has no tab, and
1075
+ leaving out all three removes the glyph group. Every door that would arm it refuses:
1076
+ `chart.drawings.arm` answers `denied`, and `armTool`, `placeImage` and an image pasted over the
1077
+ chart place nothing. A copy is a new drawing, so a drawing of a tool you leave out is not copied
1078
+ into another: `chart.drawings.clone` and `chart.drawings.paste` are unavailable for it, `clone` and
1079
+ `paste` make nothing, and a Control- or Command-drag moves the drawing instead of duplicating it.
1080
+ Copying it to the clipboard is not refused, so it pastes on a chart that offers its tool.
1081
+
1082
+ Drawings of a tool you leave out that are already on the chart, from a saved chart or layout, a
1083
+ drawings document or another host, render and stay fully editable. They select, restyle through the
1084
+ selection's settings bar and the settings dialog, lock, hide and delete exactly as any drawing does:
1085
+ from the settings bar, the eraser, the remove menu and the Delete key. The eraser is always offered,
1086
+ listed or not, because it removes drawings rather than making them, which is why `['eraser']` is the
1087
+ way to offer no tool that creates one.
1088
+
1089
+ The list composes with your access policy: a tool is offered when you list it and the policy permits
1090
+ it, and a listed tool the policy refuses is drawn as `access.refused` says (see Refused controls).
1091
+ Nothing stored is rewritten: a starred tool you leave out keeps its star and returns to the favorites
1092
+ bar on a chart that offers it, and a group's remembered face is kept the same way.
1093
+ `features.drawings: false` removes drawing altogether, whatever the list says. An empty list, a type
1094
+ that names no tool and a repeated type are setup errors that `createChart` throws.
1095
+
1096
+ ### Offered indicators
1097
+
1098
+ By default a chart offers every built-in indicator. `builtInIndicators` names the built-ins you
1099
+ offer, by the definition id `access.indicator` receives: an id `BUILT_IN_INDICATORS` lists, such as
1100
+ `sma`. The order you give does not matter, because the indicator picker keeps its own:
1101
+
1102
+ ```ts
1103
+ import { BUILT_IN_INDICATORS, createChart, type ChartDatafeed } from '@trdrs/quickcharts'
1104
+
1105
+ declare const datafeed: ChartDatafeed
1106
+
1107
+ // Moving averages and RSI only.
1108
+ createChart({ container, datafeed, builtInIndicators: ['sma', 'ema', 'rsi'] })
1109
+
1110
+ // A chart that opens with an indicator it offers.
1111
+ const sma = BUILT_IN_INDICATORS.find((definition) => definition.id === 'sma')!
1112
+ createChart({ container, datafeed, builtInIndicators: ['sma'], indicators: [{ id: 'sma-1', definition: sma }] })
1113
+ ```
1114
+
1115
+ A built-in you leave out is not listed in the indicator picker: not among the built-ins, not
1116
+ among the favorites, and not in search results. Every door that would add one refuses:
1117
+ `chart.indicators.add` (from the picker, the keyboard or your own control) answers `denied`,
1118
+ `indicators.add` adds nothing, `indicators.set` leaves out an instance of it the chart does not
1119
+ already hold, and a new pane that a re-tile adds copies the first chart's indicators without it. The
1120
+ chart has no verb that duplicates an indicator, so a second instance is an add like any other.
1121
+
1122
+ Instances of a built-in you leave out that are already on the chart, from a saved chart or layout,
1123
+ an undo step or another host sharing the store, render and stay fully editable and removable. The
1124
+ settings dialog, `chart.indicators.update`, the legend's eye and remove control, remove all and
1125
+ `indicators.remove` work on them exactly as on any instance, and an edit that would move one onto
1126
+ another built-in you leave out keeps it as it stands. Nothing saved is rewritten because of the
1127
+ list: a starred built-in you leave out keeps its star and returns to the favorites on a chart that
1128
+ offers it.
1129
+
1130
+ The list filters the built-ins alone. A definition of your own, whose `manifest.id` names no
1131
+ built-in, is never filtered, and neither is what your `indicatorPicker` source lists: its
1132
+ collections and rows are your content. The source is handed only the offered ids as `builtInIds`.
1133
+ The list composes with your access policy: a built-in is offered when you list it and the policy
1134
+ permits it, and a listed one the policy refuses is drawn as `access.refused` says (see Refused
1135
+ controls). A non-list, an empty list, an id that names no built-in, a repeated id and an
1136
+ `indicators` instance whose built-in you do not list are setup errors that `createChart` throws.
1137
+
1138
+ ### Offered ranges
1139
+
1140
+ By default the bottom bar offers every range preset in `RANGE_PRESETS`, from `1D` to `All`.
1141
+ `ranges` names the presets you offer by their keys, in the order the bottom bar draws them:
1142
+
1143
+ ```ts
1144
+ import { createChart, type ChartDatafeed } from '@trdrs/quickcharts'
1145
+
1146
+ declare const datafeed: ChartDatafeed
1147
+
1148
+ // A day, a week's worth of sessions and a year, in this order.
1149
+ createChart({ container, datafeed, ranges: ['1D', '5D', '1Y'] })
1150
+
1151
+ // No range buttons: the bottom bar keeps its clock, timezone picker and session view.
1152
+ createChart({ container, datafeed, ranges: [] })
1153
+ ```
1154
+
1155
+ A preset you leave out has no `chart.range.<key>` command, so no menu, shortcut or host control
1156
+ reaches it, `chart.range.set` ignores its key, and the bottom bar draws no button for it. An empty
1157
+ list offers none, which is how you keep the rest of the bottom bar without range buttons;
1158
+ `ui.bottomBar: false` removes the whole bar. `chart.range.set` still takes an explicit
1159
+ `{ from, to }` window either way. A preset whose timeframe your `timeframes` leave out reads its
1160
+ span at your nearest coarser timeframe, as it does without a list. A non-list, a key that names no
1161
+ preset and a repeated key are setup errors that `createChart` throws.
1162
+
1163
+ ### Offered timezones
1164
+
1165
+ By default the timezone picker offers every zone in `TIMEZONES` and the exchange choice.
1166
+ `timezones` names the choices you offer: zone ids, and `exchange` (`EXCHANGE_TIMEZONE`) for the
1167
+ charted symbol's own zone. The picker keeps its own order (UTC, the exchange choice, then every
1168
+ zone by its current offset); the order you give decides only which choice comes first:
1169
+
1170
+ ```ts
1171
+ import { createChart, EXCHANGE_TIMEZONE, type ChartDatafeed } from '@trdrs/quickcharts'
1172
+
1173
+ declare const datafeed: ChartDatafeed
1174
+
1175
+ // The exchange's own time, New York or London, opening on the exchange's time for a first-run viewer.
1176
+ createChart({ container, datafeed, timezones: [EXCHANGE_TIMEZONE, 'America/New_York', 'Europe/London'] })
1177
+
1178
+ // Always Tokyo time: no picker, and the clock reads Tokyo.
1179
+ createChart({ container, datafeed, timezones: ['Asia/Tokyo'] })
1180
+ ```
1181
+
1182
+ A choice you leave out has no `chart.timezone.<id>` command (`chart.timezone.exchange` for the
1183
+ exchange choice), `chart.timezone.set` and `setTimezone` ignore it, and the picker lists no row for
1184
+ it. With one choice offered the picker is not shown, since there is nothing to choose, and the
1185
+ bottom bar's clock still reads the time in that zone.
1186
+
1187
+ A chart opens on the viewer's stored choice, else on `preferences.timezone`, when you list it, and
1188
+ otherwise on the first choice you list. A stored choice you leave out is not rewritten: it stays
1189
+ stored until the viewer picks another zone, so a chart that offers it again opens on it. The
1190
+ exported registry is not filtered: `TIMEZONES`, `isTimezoneChoice` and `timezoneListing` answer
1191
+ the full set for code of your own. A non-list, an empty list, an id that names no zone and a
1192
+ repeated id are setup errors that `createChart` throws.
1193
+
929
1194
  ### Events
930
1195
 
931
1196
  Typed maps, one per scope. Every subscription returns its unsubscribe and is inert after
@@ -981,7 +1246,9 @@ function toolbar(widget: ChartWidget): void {
981
1246
 
982
1247
  A command whose one id spans many subjects declares `refuses(arg)` beside `available()`: an
983
1248
  argument your access policy turns away answers `denied` before availability is asked, which is how
984
- `chart.drawings.arm` refuses a tool. A refusal is a value, never a throw: `ok`, `unavailable`,
1249
+ `chart.drawings.arm` refuses a tool. `onChange` fires when the registered set or a shortcut changes,
1250
+ and when `widget.refreshAccess()` says your policy may answer differently. A refusal is a value,
1251
+ never a throw: `ok`, `unavailable`,
985
1252
  `denied`, `unknown`, or `failed` with the
986
1253
  error. Your own commands register through the same door: a chart extension's `contributeCommands`
987
1254
  puts them in this list with `scope: 'chart'` and its own label text.
@@ -1005,10 +1272,10 @@ over it is absent. A control you hide in `ui` is the only thing that changes. Th
1005
1272
  Two presentation flags also close the one command whose only job is opening their dialog.
1006
1273
  `ui.symbolSearch: false` removes the search dialog and `chart.symbol.search`, while
1007
1274
  `chart.symbol.set` still changes the symbol. `ui.indicatorPicker: false` removes the indicator
1008
- browser and `chart.indicators.open`, while `chart.indicators.add` still adds an indicator.
1275
+ picker and `chart.indicators.open`, while `chart.indicators.add` still adds an indicator.
1009
1276
 
1010
- Neither plane is authorization. A hidden control's command still answers to `access`, and no flag
1011
- supplies a capability the datafeed lacks: a feed with no search keeps every search door closed.
1277
+ Authorization belongs to `access`: a hidden control's command still answers to it, and a flag
1278
+ reaches only what the datafeed can do, so a feed with no search keeps every search door closed.
1012
1279
 
1013
1280
  ```ts
1014
1281
  import { createChart, createUdfDatafeed } from '@trdrs/quickcharts'
@@ -1028,6 +1295,99 @@ The library reads `features` and `ui` once, when the widget is created. A key or
1028
1295
  plane does not take throws a `TypeError` naming its path before anything mounts. The package's
1029
1296
  `dist/feature-manifest.json` lists every flag in its `features` and `ui` blocks.
1030
1297
 
1298
+ ### Refused controls
1299
+
1300
+ By default the chart's own controls draw what your access policy refuses disabled, which suits an
1301
+ offer the viewer can unlock. `access.refused: 'hide'` leaves it out instead, for a chart that simply
1302
+ does not offer it:
1303
+
1304
+ ```ts
1305
+ import { createChart, type ChartDatafeed } from '@trdrs/quickcharts'
1306
+
1307
+ declare const datafeed: ChartDatafeed
1308
+ declare const session: { may(feature: string): boolean }
1309
+
1310
+ createChart({
1311
+ container,
1312
+ datafeed,
1313
+ access: {
1314
+ refused: 'hide',
1315
+ command: (id) => id !== 'chart.replay.start' || session.may('replay'),
1316
+ drawingTool: (tool) => tool !== 'brush' && tool !== 'highlighter',
1317
+ indicator: (id) => id !== 'vwap',
1318
+ },
1319
+ })
1320
+ ```
1321
+
1322
+ With `'hide'`, a refused drawing tool is not in its group's flyout, on the favorites bar or in the
1323
+ glyph picker; a section, a group or a favorites bar it leaves empty goes with it, and a group's face
1324
+ that would wear a refused tool wears the first one the group still offers. A refused indicator is
1325
+ not in the indicator picker. A control or menu row whose command the policy refuses is not drawn
1326
+ (top bar, bottom bar, menus, the drawing toolbar and the selection's bar, the navigation cluster,
1327
+ the replay transport, legend row controls and the context menu), and a rule left with nothing beside
1328
+ it goes too. A picker always keeps its current choice: the chosen style, timeframe, timezone or
1329
+ scale is listed even when its command is refused.
1330
+
1331
+ Only a refusal hides. A permitted command that cannot run now (nothing to undo, no bars loaded,
1332
+ nothing selected) is drawn disabled as before. The policy is asked whenever the chrome syncs (a
1333
+ change on the active chart, a change to the command registry) and whenever a menu or flyout opens,
1334
+ and `widget.refreshAccess()` asks it again at any moment (see A policy that changes), so a policy
1335
+ that follows your session moves the controls with it. Nothing stored is rewritten: a
1336
+ viewer's favorite keeps its star and returns to the favorites bar once you permit the tool again,
1337
+ and drawings and indicators already on the chart render exactly as they do under a refusal by
1338
+ default. The keyboard, `widget.commands` and the chart handles refuse exactly as they do with
1339
+ `'disable'`. A value other than `'disable'` or `'hide'` is a setup error that `createChart` throws.
1340
+
1341
+ Under either value the policy refuses creating content, never what is already on the chart or in
1342
+ saved content: it refuses adding, never restoring. A saved chart, a layout load, an undo or a redo,
1343
+ a drawings document and shared drawing storage put back every indicator and drawing they carry,
1344
+ those the policy refuses included, so a load followed by a save never rewrites a viewer's content
1345
+ without them. An indicator whose definition `access.indicator` refuses, once on the chart (added
1346
+ before the policy changed, or put back by a restore), renders and stays fully editable and
1347
+ removable:
1348
+ `indicators.set`, `chart.indicators.update` and the settings dialog edit it, an edit of any other
1349
+ indicator keeps it, and an edit that would move it, or another indicator, onto a refused definition
1350
+ leaves that indicator as it stands. A new instance of a refused definition is still left out by
1351
+ every door. A drawing whose tool `access.drawingTool` refuses selects, restyles, locks, hides and
1352
+ deletes as any drawing does; a copy is a new drawing, so it is not cloned, pasted or duplicated by a
1353
+ Control- or Command-drag, while copying it to the clipboard is not refused.
1354
+
1355
+ ### A policy that changes
1356
+
1357
+ The predicates are asked live: every door that runs a command, arms a tool or adds an indicator asks
1358
+ at that moment, so a refusal holds from the instant your policy answers it. The chart's own controls
1359
+ read the policy when they sync and when a menu opens. When its answers change and nothing on the
1360
+ chart does (a viewer's plan changed mid-session), call `widget.refreshAccess()`:
1361
+
1362
+ ```ts
1363
+ import { createChart, type ChartDatafeed } from '@trdrs/quickcharts'
1364
+
1365
+ declare const datafeed: ChartDatafeed
1366
+ declare const session: { may(feature: string): boolean; onChange(listener: () => void): void }
1367
+
1368
+ const widget = createChart({
1369
+ container,
1370
+ datafeed,
1371
+ access: {
1372
+ refused: 'hide',
1373
+ command: (id) => id !== 'chart.replay.start' || session.may('replay'),
1374
+ indicator: (id) => id !== 'vwap' || session.may('vwap'),
1375
+ },
1376
+ })
1377
+ session.onChange(() => widget.refreshAccess())
1378
+ ```
1379
+
1380
+ Every surface that reads the policy reads it again at once: the top and bottom bars and their
1381
+ controls, the drawing toolbar's tools and groups, the favorites bar, the selection's bar, the
1382
+ navigation cluster, the replay transport and the legend's row controls, shown or left out as
1383
+ `access.refused` says and enabled or disabled. A menu, flyout or dialog that is open reads it too:
1384
+ the bars' menus, the context menu, the indicator picker and the saved layouts dialog rebuild their
1385
+ rows in place, and a drawing toolbar flyout (the glyph picker among them) or a selection bar panel
1386
+ opens again from its control, or closes when that control is now left out or disabled. Listeners of `widget.commands.onChange`
1387
+ hear it as well, so a control of your own can read `commands.available` again. Nothing stored
1388
+ changes, nothing on the chart changes, and no `saveNeeded` is raised: a refresh is a reading, never
1389
+ a write.
1390
+
1031
1391
  ### Your own interface
1032
1392
 
1033
1393
  The widget supports three ways to present a chart, over the same behavior and the same commands.
@@ -1072,14 +1432,15 @@ The chart's own dialogs still open from their commands while its bars are hidden
1072
1432
  them in the widget's layer on the document, and `widget.layout.save` asks a layout that was never
1073
1433
  saved for its name. Hide a dialog in `ui` when your interface provides its own.
1074
1434
 
1075
- The library does not export its internal components, replace its renderer, or take markup for its
1076
- controls. The class names under the widget's root are private, apart from the styling hooks listed
1435
+ The library draws its own controls over its own renderer and keeps its internal components to
1436
+ itself. The class names under the widget's root are private, apart from the styling hooks listed
1077
1437
  under Theme.
1078
1438
 
1079
1439
  ### Fullscreen and image
1080
1440
 
1081
- Chart-root fullscreen fills the screen with the widget's own element. It never takes over your
1082
- application shell, which is a different thing from a fit-to-container layout your CSS owns.
1441
+ Chart-root fullscreen fills the screen with the widget's own element, or with the
1442
+ `fullscreen.target` you name; your application shell and any fit-to-container layout stay in your
1443
+ CSS.
1083
1444
 
1084
1445
  ```ts
1085
1446
  import { type ChartWidget } from '@trdrs/quickcharts'
@@ -1098,24 +1459,24 @@ async function shareable(widget: ChartWidget): Promise<void> {
1098
1459
  identity and the attribution you configured through `image`. Nothing is uploaded, shared or stored:
1099
1460
  you receive a blob and decide.
1100
1461
 
1101
- ### Indicator browser content
1462
+ ### Indicator picker content
1102
1463
 
1103
- The Indicators button and `commands.execute('chart.indicators.open')` open the same browser.
1464
+ The Indicators button and `commands.execute('chart.indicators.open')` open the same picker.
1104
1465
  Pass `{ collection: 'saved' }` as the command argument to open a declared host collection directly.
1105
- An unknown collection falls back to the shipped catalog. Reopening an already open browser keeps its current selection and query.
1466
+ An unknown collection falls back to the shipped catalog. Reopening an already open picker keeps its current selection and query.
1106
1467
  It lists the 23 shipped definitions, with search and Favorites. Add creates a new instance on the
1107
- currently active chart and keeps the browser open. Favorites use your `ChartStorage` port.
1468
+ currently active chart and keeps the picker open. Favorites use your `ChartStorage` port.
1108
1469
 
1109
1470
  Supply `ChartWidgetOptions.indicatorPicker` to include additional localized collections and rows
1110
- in that browser. You supply data and opaque actions, not DOM, indicator definitions, or a renderer.
1471
+ in that picker. You supply data and opaque actions, not DOM, indicator definitions, or a renderer.
1111
1472
  Built-in annotations can add favorite state, counts and actions; they cannot replace a definition,
1112
- its name, or Add. Without a source, the browser has only shipped built-ins and Favorites.
1473
+ its name, or Add. Without a source, the picker has only shipped built-ins and Favorites.
1113
1474
 
1114
1475
  ```ts
1115
1476
  import type { IndicatorPickerSource } from '@trdrs/quickcharts'
1116
1477
 
1117
1478
  const indicatorPicker: IndicatorPickerSource = {
1118
- collections: [{ id: 'saved', label: 'Saved studies' }],
1479
+ collections: [{ id: 'saved', label: 'Saved indicators' }],
1119
1480
  async list({ collection, query, builtInIds }, signal) {
1120
1481
  if (signal.aborted) return { kind: 'unavailable', message: 'Content unavailable.' }
1121
1482
  return {
@@ -1134,8 +1495,10 @@ const indicatorPicker: IndicatorPickerSource = {
1134
1495
  }
1135
1496
  ```
1136
1497
 
1137
- The source receives the collection, query and shipped definition ids. It owns filtering and any
1138
- service limits; the browser does not promise an unlimited catalog or implement paging for you.
1498
+ The source receives the collection, query and the shipped definition ids the picker lists (only
1499
+ those `builtInIndicators` offers, when you name a list). Its own collections and rows are never
1500
+ filtered by that list. It owns filtering, paging and any
1501
+ service limits; the picker lists what it answers.
1139
1502
  Collection ids `builtin` and `favorites`, and action ids `add` and `favorite`, are reserved.
1140
1503
  Item ids cannot collide with shipped definition ids or these reserved ids. Duplicate ids,
1141
1504
  malformed rows and missing primary actions refuse the response; the shipped catalog remains usable.
@@ -1144,7 +1507,7 @@ name a localized navigation group and use `layout: 'list'` for name/secondary-ac
1144
1507
  An absent count stays blank. Omit `favorite` to omit that action on a contributed item.
1145
1508
 
1146
1509
  A favorite action receives the requested next boolean. Successful actions refresh the current
1147
- collection unless they return `close: true`. Refusals and exceptions keep the browser open with
1510
+ collection unless they return `close: true`. Refusals and exceptions keep the picker open with
1148
1511
  plain text, never rendered markup. Icons reuse `ChartExtensionIcon`; invalid icons draw no glyph.
1149
1512
  The host localizes contributed labels and messages; the chart localizes its own controls.
1150
1513
 
@@ -1159,8 +1522,9 @@ chrome is painted from it and renders nothing without it.
1159
1522
 
1160
1523
  - **The top bar.** The symbol pill opens the symbol search for the active chart; the compare door
1161
1524
  opens it in compare mode. The timeframe picker shows the saved timeframes as chips and a list of
1162
- the 26 presets in five groups, each row savable as a chip, with a composer for a custom interval
1163
- under the unit's ceiling. The style picker lists the seven styles. Indicators opens the picker
1525
+ the timeframes the chart offers (by default the 26 presets in five groups), each row savable as a
1526
+ chip, with a composer for a custom timeframe under the unit's ceiling while custom timeframes are
1527
+ offered. The style picker lists the styles the chart offers. Indicators opens the picker
1164
1528
  over the 23 built-in definitions, and the legend's gear opens the settings dialog for an instance
1165
1529
  (inputs, style, visibility). Bar replay enters and leaves replay for the active chart; entering
1166
1530
  asks where to begin rather than choosing a starting bar. Its starting-point menu answers with a
@@ -1168,19 +1532,20 @@ chrome is painted from it and renders nothing without it.
1168
1532
  date runs `chart.replay.startFirst`: the chart pages the feed's history back to its oldest bar,
1169
1533
  within the 20,000 bars one session holds, and starts there. Undo and
1170
1534
  redo step back and forward through the active chart's own content, and each names the change it
1171
- would move. Layout setup offers the 55
1172
- arrangements and the five sync switches; the saved-layouts menu saves, copies, renames, opens and
1173
- deletes layouts through `saveLoad.layouts` and marks unsaved changes, with an autosave switch.
1535
+ would move. Layout setup offers the arrangements and sync switches the chart offers (by default
1536
+ the 55 arrangements and the five sync switches); the saved-layouts menu, shown with a layouts
1537
+ store, saves, copies, renames, opens and deletes layouts through `saveLoad.layouts` and marks
1538
+ unsaved changes, with an autosave switch and Download chart data.
1174
1539
  Chart settings edits appearance, grid and session shading, the price-scale mode and the theme
1175
1540
  mode, and its Reset defaults row runs `chart.appearance.reset`, which drops the viewer's own
1176
1541
  appearance edits and returns the price scale to normal so the chart reads as the theme and your
1177
1542
  constructor options paint it. `ui: { topBar: { settings: { theme: false } } }` removes its Theme
1178
1543
  section and leaves the rest of the menu, for a host that offers the theme choice in its own
1179
- settings; the widget's theme API and theme commands are untouched. Fullscreen and the image menu (Download image, and Copy image where the browser can) close
1180
- the bar.
1181
- - **The bottom bar.** The range presets, the clock in the display zone with the timezone list
1182
- (UTC and the exchange choice first), and the session view for a symbol that trades outside
1183
- regular hours.
1544
+ settings; the widget's theme API and theme commands are untouched. Fullscreen and the image menu (Download image, Copy image where the browser can, and
1545
+ Download chart data when there is no layouts store) close the bar.
1546
+ - **The bottom bar.** The range presets the chart offers, the clock in the display zone with the
1547
+ list of the timezones it offers (UTC and the exchange choice first), and the session view for a
1548
+ symbol that trades outside regular hours.
1184
1549
  - **Around the charts.** A navigation cluster (zoom, scroll, reset) sits at the bottom of each
1185
1550
  pane, and past one tile it carries a sixth control that fills the layout with the active tile or
1186
1551
  gives the layout back, over `widget.layout.toggleMaximize`, the same verb the Alt press on a tile
@@ -1193,7 +1558,7 @@ chrome is painted from it and renders nothing without it.
1193
1558
  replay entry can claim the row. Chart notices report a feed that cannot serve the symbol, an
1194
1559
  image that could not be copied, or a save the store refused.
1195
1560
 
1196
- Undo and redo step through the chart's own content: the symbol, the interval, the style, the price
1561
+ Undo and redo step through the chart's own content: the symbol, the timeframe, the style, the price
1197
1562
  scale and whether it frames itself, the appearance a viewer authored, the comparisons, the
1198
1563
  indicators and the drawings. A step is one reading of that content, so a step back puts the whole
1199
1564
  reading back rather than reversing a single verb, and the two controls name the change they would
@@ -1273,7 +1638,8 @@ null when `ui: { topBar: false }` removes the bar, which is what to check before
1273
1638
  you would have nowhere to put.
1274
1639
 
1275
1640
  Every control acts through the command registry and reflects `commands.available`, so a command
1276
- your access policy refuses renders disabled and does nothing. The saved-layouts menu runs the
1641
+ your access policy refuses renders disabled and does nothing, or is not drawn with
1642
+ `access.refused: 'hide'`. The saved-layouts menu runs the
1277
1643
  layout verbs `widget.layout.save`, `rename`, `load`, `open`, `delete`, `create` and `autosave`; what a
1278
1644
  verb did reports through the `layout` event and what it refused through `saveConflict`. Copy image
1279
1645
  reports through the `image` event (a refused copy falls back to a download). Each control has a flag
@@ -1298,7 +1664,9 @@ trimmed.on('saveNeeded', () => note('the layout has unsaved changes'))
1298
1664
  ```
1299
1665
 
1300
1666
  The search dialog's class chips come from your datafeed's `config().classes`, named through
1301
- `search.classNames`; a class you do not name wears its token. `search.scope` names what your
1667
+ `search.classNames`; a class you do not name wears its token. `search.spreads`,
1668
+ `search.allClasses` and `search.classSelection` choose how the classes and the spread operators are
1669
+ offered, as "Classes and spread operators" under Search describes. `search.scope` names what your
1302
1670
  search is limited to at the far edge of that strip, with the mark its `mark` paints; your datafeed
1303
1671
  decides what is found. The range presets read your
1304
1672
  datafeed's optional `earliestBar(symbol)`: a preset deeper than the history you serve is disabled.
@@ -1329,8 +1697,8 @@ is a factory that returns a fresh `<svg>` element. An id you leave out keeps the
1329
1697
  as a `TypeError` when the widget is created.
1330
1698
 
1331
1699
  An id names what a glyph means, not one control. `settings` is the top bar's settings button, a
1332
- study's gear in the legend and the drawing settings bar's gear, so one drawing stands in all three.
1333
- Drawing tools take `tool.<type>`, chart styles `style.<style>` and layout arrangements
1700
+ indicator's gear in the legend and the drawing settings bar's gear, so one drawing stands in all
1701
+ three. Drawing tools take `tool.<type>`, chart styles `style.<style>` and layout arrangements
1334
1702
  `layout.<code>`, from the registries the rest of the API uses.
1335
1703
 
1336
1704
  ```ts
@@ -1380,15 +1748,16 @@ before costs that one glyph its artwork. The control draws the chart's own glyph
1380
1748
 
1381
1749
  `icons` covers the chart's own controls. The emoji, sticker and icon glyphs a drawing places come
1382
1750
  through the asset port, and a chart extension draws its rows and layers from the
1383
- `ChartExtensionIcon` descriptors it contributes. The product's mark in the plot's corner is not an
1384
- icon. `mountContextMenu`, `openSymbolSearch` and `mountSymbolSearch` take the same `icons` for a menu
1385
- or search you mount without a widget.
1751
+ `ChartExtensionIcon` descriptors it contributes. The product's mark in the plot's corner is the
1752
+ chart's own artwork, outside `icons`. `mountContextMenu`, `openSymbolSearch`, `mountSymbolSearch`,
1753
+ `openTimeframePicker` and `mountTimeframePicker` take the same `icons` for a menu, search or picker
1754
+ you mount without a widget.
1386
1755
 
1387
1756
  ### Neutral marks
1388
1757
 
1389
1758
  Serve `marks` and `timescaleMarks` from your datafeed and the chart draws them. A mark is a note
1390
1759
  about a moment: its color is a semantic theme role rather than a literal, its words are yours, and
1391
- the chart neither interprets nor acts on it.
1760
+ the chart draws it as given.
1392
1761
 
1393
1762
  ```ts
1394
1763
  import { type BarMark, type ChartDatafeed } from '@trdrs/quickcharts'
@@ -1426,13 +1795,13 @@ void withMarks
1426
1795
  display name, venue, timeframe, OHLC and change against the previous painted close. Prices use
1427
1796
  the symbol's formatter. Hover selects a bar; leaving restores the latest painted reading. Replay
1428
1797
  does not expose bars beyond its cursor. Missing metadata leaves the supplied symbol unchanged.
1429
- Legend and search use a decorative monogram; they do not fetch symbol logos or infer a provider.
1430
- Study and separate-pane comparison rows follow their renderer panes. Stable row controls retain
1431
- focus during value updates. Study values read the first plot, using declared precision or the
1432
- symbol formatter; Volume reads bar volume using resolved volume precision. The row-list toggle,
1433
- study eye and pane collapse are independent controls. Pane restore remembers study identity
1434
- across pane removal. The status control opens session details, retains an unknown-session state
1435
- and is hidden during replay. Price-scale chips use the same commands as `setScaleMode`.
1798
+ Legend and search rows wear your `symbolMark`, or a decorative monogram without one.
1799
+ Indicator and separate-pane comparison rows follow their renderer panes. Stable row controls
1800
+ retain focus during value updates. Indicator values read the first plot, using declared precision
1801
+ or the symbol formatter; Volume reads bar volume using resolved volume precision. The row-list
1802
+ toggle, indicator eye and pane collapse are independent controls. Pane restore remembers indicator
1803
+ identity across pane removal. The status control opens session details, retains an unknown-session
1804
+ state and is hidden during replay. Price-scale chips use the same commands as `setScaleMode`.
1436
1805
  - **An interface language** (`locale`, English by default), one of the 21 the package ships.
1437
1806
  `BUILT_IN_LOCALES` lists them for a picker: each carries its stable code, its canonical BCP 47
1438
1807
  `tag`, its reading direction (`ar` and `he_IL` are `rtl`), and its endonym. The chart's own chrome
@@ -1524,9 +1893,9 @@ theme.resetCustom()
1524
1893
  stop()
1525
1894
  ```
1526
1895
 
1527
- Switching modes keeps the symbol, timeframe, visible range, drawings and studies the chart already
1528
- has. A palette you supply is a tint rather than a replacement: a role you do not name keeps its
1529
- built-in value for that mode.
1896
+ Switching modes keeps the symbol, timeframe, visible range, drawings and indicators the chart
1897
+ already has. A palette you supply is a tint rather than a replacement: a role you do not name keeps
1898
+ its built-in value for that mode.
1530
1899
 
1531
1900
  ### Roles
1532
1901
 
@@ -1535,26 +1904,75 @@ inventory, and the built-in palettes and the generated stylesheet are built from
1535
1904
  property names are private, so a color, a size or a radius is always set through a role. The
1536
1905
  class names inside the stylesheet are private too, except the supported hooks below.
1537
1906
 
1907
+ ### Motion
1908
+
1909
+ Every duration, timing function and motion scale the chart uses is a role in the `motion` family,
1910
+ so you retune the chart's motion the way you retune its colors. Both built-in palettes carry the
1911
+ same values:
1912
+
1913
+ | Role | Default | What it times |
1914
+ |---|---|---|
1915
+ | `motion.durationFast` | `90ms` | A state change under the pointer, such as a hover fill. |
1916
+ | `motion.durationBase` | `150ms` | A modal dialog and its backdrop opening and closing, and the replay row entering and leaving. |
1917
+ | `motion.durationModerate` | `250ms` | A control moving to its new state, such as a switch knob sliding. |
1918
+ | `motion.durationSlow` | `350ms` | A small mark settling, such as a disclosure caret turning or a checkbox filling. |
1919
+ | `motion.durationSlower` | `500ms` | A larger mark turning, such as the drawing toolbar's chevron. |
1920
+ | `motion.easingStandard` | `ease` | A modal dialog box, the replay row, a caret dip, a checkbox fill. |
1921
+ | `motion.easingOut` | `ease-out` | A modal backdrop fading, a switch knob sliding. |
1922
+ | `motion.easingLinear` | `linear` | A color or opacity change under the pointer. |
1923
+ | `motion.easingSpring` | `cubic-bezier(0.175, 0.885, 0.32, 1.275)` | A caret or chevron turning with a slight overshoot. |
1924
+ | `motion.scaleEnter` | `0.97` | The scale a modal dialog box grows from as it opens. |
1925
+
1926
+ Every modal dialog the chart opens (the symbol search, chart settings, the indicator browser and
1927
+ indicator settings, the saved-layouts browser, the name and confirm prompts, go to date, and the
1928
+ drawing settings, image picker and template prompts) opens with its backdrop fading in over
1929
+ `motion.durationBase` on `motion.easingOut`, and its box fading in and growing from
1930
+ `motion.scaleEnter` to full size over `motion.durationBase` on `motion.easingStandard`. It closes
1931
+ with the same motion reversed. A closing dialog stops taking input and returns focus at once, and the
1932
+ chart removes it when the same `motion.durationBase` has elapsed, so a duration you set times both
1933
+ the transition and the removal. A dialog opened over another moves on its own and closes first, and
1934
+ a dialog the chart replaces or tears down goes at once.
1935
+
1936
+ A duration takes a CSS duration such as `200ms` or `0.2s`; an easing takes any CSS timing function,
1937
+ such as `ease-in-out`, `steps(4)` or `cubic-bezier(0.2, 0, 0, 1)`; a scale takes a unitless factor
1938
+ such as `0.95`.
1939
+
1940
+ ```ts
1941
+ import { createThemeController, type CustomThemes } from '@trdrs/quickcharts'
1942
+
1943
+ const calm: CustomThemes = {
1944
+ light: { 'motion.durationBase': '200ms', 'motion.scaleEnter': '0.95' },
1945
+ dark: { 'motion.durationBase': '200ms', 'motion.scaleEnter': '0.95' },
1946
+ }
1947
+ const theme = createThemeController({ mode: 'dark', custom: calm })
1948
+ note(theme.get()['motion.durationBase'])
1949
+ ```
1950
+
1951
+ When the reader's system asks for reduced motion (`prefers-reduced-motion: reduce`), the
1952
+ stylesheet resolves every duration role to `0ms` and marks those declarations `!important`, so a
1953
+ palette you supply cannot bring motion back: dialogs, menus and the replay row open and close at
1954
+ once.
1955
+
1538
1956
  ### Cascade layers
1539
1957
 
1540
1958
  The stylesheet declares two cascade layers and puts everything it contains in them: the built-in
1541
- palettes in `trdrs.tokens`, every recipe in `trdrs.chart`. Nothing in it is unlayered. It marks a
1542
- declaration `!important` only where a later layer must not undo it: an element's `hidden`
1543
- attribute, the shortened motion under a reduced-motion preference, and the missing focus ring of a
1544
- search field, which shows its focus by its caret. Your first stylesheet must declare the complete
1545
- order before any product stylesheet loads, because a layer's position is fixed by the first
1546
- statement that names it:
1959
+ palettes in `quickcharts.tokens`, every recipe in `quickcharts.chart`. Nothing in it is unlayered.
1960
+ It marks a declaration `!important` only where a later layer or an inline palette must not undo it:
1961
+ an element's `hidden` attribute, the zero motion durations under a reduced-motion preference, and
1962
+ the missing focus ring of a search field, which shows its focus by its caret. Your first stylesheet
1963
+ must declare the complete order before any product stylesheet loads, because a layer's position is
1964
+ fixed by the first statement that names it:
1547
1965
 
1548
1966
  ```css
1549
- @layer reset, trdrs.tokens, trdrs.chart, trdrs.platform, host;
1967
+ @layer reset, quickcharts.tokens, quickcharts.chart, host;
1550
1968
  ```
1551
1969
 
1552
1970
  Put your reset in `reset`. A reset left unlayered outranks every layered rule and strips the
1553
1971
  chart's controls of their borders and grounds. Put an intentional override of a supported hook in
1554
- `host`, or leave it unlayered; either wins over the chart's recipe. A chart-only host declares only
1555
- `reset`, the two chart layers and `host`; `trdrs.platform` is the layer a later product declares
1556
- for itself. Inline styles the chart writes for measured geometry outrank any stylesheet rule, and
1557
- a rule marked `!important` reverses layer order, so neither is a way to restyle the chart.
1972
+ `host`, or leave it unlayered; either wins over the chart's recipe. Layers of your own product go
1973
+ between `quickcharts.chart` and `host`. Inline styles the chart writes for measured geometry
1974
+ outrank any stylesheet rule, and a rule marked `!important` reverses layer order, so neither is a
1975
+ way to restyle the chart.
1558
1976
 
1559
1977
  ### Supported styling hooks
1560
1978
 
@@ -1566,9 +1984,9 @@ applying in any release. Removing a hook, a state or a customization is a breaki
1566
1984
  | Hook | Purpose | States | Customization |
1567
1985
  |---|---|---|---|
1568
1986
  | `.qc-topbar` | The top toolbar: symbol search, timeframe, chart style, indicators, layouts, replay and the widget menus. | | background-color, border, padding, gap, box-shadow |
1569
- | `.qc-drawing-toolbar` | The drawing rail beside the plot. | `aria-orientation`: vertical beside the plot, horizontal in a host row. | background-color, border, padding, gap, box-shadow |
1987
+ | `.qc-drawing-toolbar` | The drawing toolbar beside the plot. | `aria-orientation`: vertical beside the plot, horizontal in a host row. | background-color, border, padding, gap, box-shadow |
1570
1988
  | `.qc-bottombar` | The bottom bar: range shortcuts, the session clock and the timezone. | | background-color, border, padding, gap, box-shadow |
1571
- | `.qc-legend` | The legend over the plot: the symbol, its reading and each study row. | | background-color, border, border-radius, padding, box-shadow, inset |
1989
+ | `.qc-legend` | The legend over the plot: the symbol, its reading and each indicator row. | | background-color, border, border-radius, padding, box-shadow, inset |
1572
1990
  | `.qc-menu-panel` | A floating menu opened from a toolbar control. | `hidden`: present while the menu is closed. | background-color, border, border-radius, padding, box-shadow |
1573
1991
  | `.qc-dialog` | A modal dialog: settings, search, layouts and the drawing editors. | `data-role`: which dialog this is, in the chart's own vocabulary. | background-color, border, border-radius, padding, box-shadow, max-width |
1574
1992
 
@@ -1600,8 +2018,8 @@ problem. Configuration errors never reach a render.
1600
2018
  ### Theme and appearance are two ladders
1601
2019
 
1602
2020
  The theme palette is the broad brand surface. Chart appearance is the specific one: series colors,
1603
- candle anatomy, grid visibility and study visuals in `ChartOverrides.appearance`. Where both could
1604
- affect the same pixel, appearance wins.
2021
+ candle anatomy, grid visibility and indicator visuals in `ChartOverrides.appearance`. Where both
2022
+ could affect the same pixel, appearance wins.
1605
2023
 
1606
2024
  Theme palette precedence, lowest first:
1607
2025
 
@@ -1612,7 +2030,7 @@ Chart appearance precedence, lowest first:
1612
2030
 
1613
2031
  1. the built-in appearance for the selected mode;
1614
2032
  2. the constructor's `appearance` partial;
1615
- 3. restored user appearance;
2033
+ 3. restored viewer appearance;
1616
2034
  4. runtime `chart.applyAppearance` patches.
1617
2035
 
1618
2036
  Resetting custom palettes returns the chart to the built-in mode and leaves saved chart appearance
@@ -1624,23 +2042,24 @@ opens in those colors on any theme.
1624
2042
 
1625
2043
  ## Compare
1626
2044
 
1627
- Every chart can draw OTHER symbols beside its own. A compare is study-like: legend-managed, three
1628
- placements, persisted in the chart content blob. `same-percent` shares the
1629
- main price scale and flips it to percent while any such compare lives (the prior scale mode comes
1630
- back when the last one leaves); `new-scale` binds the LEFT scale with absolute prices (the left
1631
- axis exists only while such a compare does); `new-pane` takes a pane of its own. Compared bars
1632
- clip to the main series window, so a compare never extends the time axis. `features.compareSymbols` supplies
1633
- a curated quick-add list for the compare dialog; `compare.symbols()` reads it back.
2045
+ Compare draws OTHER symbols beside a chart's own. Each comparison is managed like an indicator:
2046
+ from the legend, at one of three placements, persisted in the chart content blob. `same-percent`
2047
+ shares the main price scale and flips it to percent while any such comparison lives (the prior
2048
+ scale mode comes back when the last one leaves); `new-scale` binds the LEFT scale with absolute
2049
+ prices (the left axis exists only while such a comparison does); `new-pane` takes a pane of its
2050
+ own. Compared bars clip to the main series window, so a comparison never extends the time axis.
2051
+ `features.compareSymbols` supplies a curated quick-add list for the compare dialog;
2052
+ `compare.symbols()` reads it back.
1634
2053
 
1635
2054
  The widget's standard Compare door lives in the top bar, and `ui: { topBar: { compare: false } }`
1636
2055
  hides it. The legend header does not duplicate that door. Commands and comparison-row actions remain
1637
- available when the top-bar door is hidden, and `features: { compare: false }` removes comparing
1638
- altogether. The built-in dialog's search rows add at any of the three placements, curated `compareSymbols` rows sit
1639
- above results, and the ADDED section removes. Each compare takes a legend row whose title reopens
1640
- the dialog in change-symbol mode (the pick re-keys the compare in place), with an eye and a remove
1641
- beside the value (% under `same-percent`, the last close otherwise). In a layout, compares belong to
1642
- each chart of their own (`widget.activeChart().compare`) and ride the layout blob with the rest of
1643
- that chart's content.
2056
+ available when the top-bar door is hidden, and `features: { compare: false }` removes compare
2057
+ altogether. The built-in dialog's search rows add at any of the three placements, curated
2058
+ `compareSymbols` rows sit above results, and the ADDED section removes. Each comparison takes a
2059
+ legend row whose title reopens the dialog in change-symbol mode (the pick re-keys the comparison in
2060
+ place), with an eye and a remove beside the value (% under `same-percent`, the last close
2061
+ otherwise). In a layout, each chart holds its own comparisons (`widget.activeChart().compare`), and
2062
+ they ride the layout blob with the rest of that chart's content.
1644
2063
 
1645
2064
  ```ts
1646
2065
  import { createChart, createUdfDatafeed } from '@trdrs/quickcharts'
@@ -1656,15 +2075,15 @@ compare.add('CL', { placement: 'new-pane' })
1656
2075
  compare.setVisible('CL', false)
1657
2076
  const active = compare.list() // [{ symbol, placement, color, visible }]
1658
2077
  note(`NQ last: ${compare.latest('NQ') ?? '-'}`)
1659
- compare.remove('NQ') // the scale mode the trader held comes back
2078
+ compare.remove('NQ') // the scale mode the viewer held comes back
1660
2079
  ```
1661
2080
 
1662
2081
  ## Timeframes
1663
2082
 
1664
- A timeframe token names a bar interval: a count and a unit, `1m`, `4h`, `1d`, `3mo`, `500t`. The
1665
- units are ticks (`t`), seconds (`s`), minutes (`m`), hours (`h`), days (`d`), weeks (`w`) and
1666
- months (`mo`), and each unit has a ceiling (`TIMEFRAME_MAX`). A token outside the grammar parses
1667
- as null, and the chart never asks a feed for it.
2083
+ A timeframe token names how much one bar spans: a count and a unit, `1m`, `4h`, `1d`, `3mo`, `500t`.
2084
+ The units are ticks (`t`), seconds (`s`), minutes (`m`), hours (`h`), days (`d`), weeks (`w`) and
2085
+ months (`mo`), and each unit has a ceiling (`TIMEFRAME_MAX`). A token outside the grammar parses as
2086
+ null, and the chart never asks a feed for it.
1668
2087
 
1669
2088
  ```ts
1670
2089
  import { formatTimeframe, isIntradayTimeframe, parseTimeframe, timeframeSeconds } from '@trdrs/quickcharts'
@@ -1772,7 +2191,7 @@ if (equity) {
1772
2191
 
1773
2192
  ## Ranges
1774
2193
 
1775
- `RANGE_PRESETS` lists the nine range presets, each a visible span and the interval it reads best
2194
+ `RANGE_PRESETS` lists the nine range presets, each a visible span and the timeframe it reads best
1776
2195
  at: `1D` over one-minute bars through `All` over monthly bars. `rangeAvailable` withholds a preset
1777
2196
  deeper than the history a symbol has, and `frameRange` sets a pane's visible window for a span,
1778
2197
  anchored on the last real bar so a future whitespace horizon never frames as empty space. The
@@ -1800,7 +2219,7 @@ host's own search surface subscribes to the same state.
1800
2219
  The widget prefetches the default catalog and reuses completed pages across search, compare and
1801
2220
  change-symbol dialogs. Each opening owns its query, class filter and pending work. Closing it
1802
2221
  discards late results without clearing completed pages. Locale changes and widget disposal clear
1803
- the widget cache. A replacement widget never inherits a previous provider's cache.
2222
+ the widget cache. A replacement widget starts with a cache of its own.
1804
2223
 
1805
2224
  Completed reuse holds at most 32 queries and 5,000 rows, evicting least-recent entries. Retained
1806
2225
  multi-page results reopen without a first-page refetch. Eviction does not truncate an active
@@ -1838,6 +2257,87 @@ isSymbolPair('BTC/USD') // true
1838
2257
  spreadExpression(' es - nq ') // 'ES-NQ'
1839
2258
  ```
1840
2259
 
2260
+ ### Classes and spread operators
2261
+
2262
+ The dialog offers your datafeed's `config().classes` as a strip of chips, an All chip first, one
2263
+ class selected at a time and sent to your feed as `cls`. Its field offers six spread operators
2264
+ behind a toggle, and a query that reads as an expression leads the list with the expression row.
2265
+ Three `search` options change that, and a chart that sets none of them searches exactly as above.
2266
+
2267
+ - `spreads`: `false` offers no operator toggle, no operator and no expression row, and your feed
2268
+ receives the query exactly as typed. `{ operators }` keeps spreads on and offers only the listed
2269
+ operators (`SpreadOperatorId`: `division`, `subtraction`, `addition`, `multiplication`,
2270
+ `exponentiation`, `reciprocal`), in the listed order. Default `true`. The compare dialog never
2271
+ offers the operators.
2272
+ - `allClasses`: `false` offers no All chip. With one class at a time, the first class you declare
2273
+ starts selected; with several, no selection means every class. `{ label }` writes the All chip
2274
+ with your label. Default: the chip, with the catalog's label.
2275
+ - `classSelection`: `'multiple'` makes every chip a toggle, and the All chip clears the selection.
2276
+ Your feed receives `classes` with every selected class in declared order, and `cls` too while
2277
+ exactly one is selected, so a feed that reads only `cls` still narrows a single chip. Default
2278
+ `'single'`.
2279
+
2280
+ A `classes` entry is a class token, or a `SearchClassNode`, `{ id, children }`, whose children are
2281
+ narrower classes. Selecting such a class opens a second row of chips beneath the strip: its own
2282
+ all chip, written like the All chip, and one chip per child, named through `search.classNames`.
2283
+ Your feed always receives the most specific class selected: the child when one is picked, the
2284
+ class when none is. The dialog renders two levels; a child's own `children` are not offered, and
2285
+ the child narrows by its `id`. The second row is a labelled group of pressed buttons, reached by
2286
+ Tab after the strip, as the strip is.
2287
+
2288
+ ```ts
2289
+ import { createChart, type ChartDatafeed, type DatafeedConfig } from '@trdrs/quickcharts'
2290
+
2291
+ declare const cryptoBase: ChartDatafeed
2292
+
2293
+ // Spot markets by quote currency, beside perpetuals.
2294
+ const cryptoFeed: ChartDatafeed = {
2295
+ ...cryptoBase,
2296
+ async config(): Promise<DatafeedConfig> {
2297
+ return { classes: [{ id: 'spot', children: ['usdc', 'usdt'] }, 'perp'] }
2298
+ },
2299
+ // `cls` is 'spot' while the whole class is selected, and 'usdc' once USDC is picked under it.
2300
+ search: (query, options) => cryptoBase.search(query, options),
2301
+ }
2302
+
2303
+ const crypto = createChart({
2304
+ container,
2305
+ datafeed: cryptoFeed,
2306
+ search: {
2307
+ classNames: { spot: 'Spot', usdc: 'USDC', usdt: 'USDT', perp: 'Perpetuals' },
2308
+ allClasses: { label: 'Every market' },
2309
+ spreads: { operators: ['division', 'subtraction'] },
2310
+ },
2311
+ })
2312
+ void crypto
2313
+ ```
2314
+
2315
+ With several classes at once, read `classes` and fall back to `cls`:
2316
+
2317
+ ```ts
2318
+ import { createChart, type ChartDatafeed } from '@trdrs/quickcharts'
2319
+
2320
+ declare const catalogFeed: ChartDatafeed
2321
+
2322
+ const multiFeed: ChartDatafeed = {
2323
+ ...catalogFeed,
2324
+ async search(query, options = {}) {
2325
+ // ['future', 'option'] with two chips selected; absent with none, which is every class.
2326
+ const classes = options.classes ?? (options.cls ? [options.cls] : [])
2327
+ return myBackend.search(query, classes, options.limit, options.offset)
2328
+ },
2329
+ }
2330
+
2331
+ const multi = createChart({
2332
+ container,
2333
+ datafeed: multiFeed,
2334
+ search: { classSelection: 'multiple', allClasses: false, spreads: false },
2335
+ })
2336
+ void multi
2337
+ ```
2338
+
2339
+ `openSymbolSearch` and `mountSymbolSearch` take the same three options beside their `classes`.
2340
+
1841
2341
  ### The picker, away from a chart
1842
2342
 
1843
2343
  A page that picks a market where no chart is mounted opens the same dialog the chart's symbol pill
@@ -1877,9 +2377,63 @@ card.dispose()
1877
2377
  catalog.dispose()
1878
2378
  ```
1879
2379
 
2380
+ ### The timeframe picker, away from a chart
2381
+
2382
+ A page that asks for a timeframe where no chart is mounted (a backtest's timeframe, an alert's
2383
+ timeframe) opens the list the chart's timeframe caret drops: the same unit groups, rows, labels and
2384
+ custom composer, built by the same code, so a timeframe reads there exactly as it reads on a chart.
2385
+ `openTimeframePicker` drops it from a control you own, which gets `aria-expanded` while it is open
2386
+ and focus back when it closes; `mountTimeframePicker` builds it bare into a box you own and
2387
+ position, as a card on a phone. Picking a row, or composing a custom timeframe, hands the token to
2388
+ `onPick`, because there is no chart here to set. The drop-down then closes; the card calls
2389
+ `onClose`, your cue to close its box.
2390
+
2391
+ It takes what you already give a chart. `timeframe` is the value it opens on, checked and focused,
2392
+ and a custom value sits in its unit's group. `timeframes` and `customTimeframes` offer exactly what
2393
+ they offer a chart (see Offered timeframes), with the same setup errors, thrown before anything
2394
+ mounts, plus one for a `timeframe` the grammar cannot read. `resolutions` and
2395
+ `supportedResolutions` say what your feed and the symbol serve: as in the chart's list, a row they
2396
+ do not serve is left out, and the composer does not offer one. `theme`, `locale` (or a prepared
2397
+ `i18n`) and `icons` paint it as they paint a chart, on a theme root of its own, so the stylesheet
2398
+ reaches it outside any chart in either mode.
2399
+
2400
+ It carries no saved chips and no stars, and keeps nothing: the chips are a chart's quick-select row,
2401
+ stored with the chart's preferences, and a field that asks for one timeframe has no row to put them
2402
+ in. A composed timeframe is handed back and not added to any list. The drop-down takes the keyboard
2403
+ as the chart's does (arrow keys, Home and End rove the rows; Escape and a press outside close
2404
+ it) and moves as every menu does.
2405
+
2406
+ ```ts
2407
+ import { mountTimeframePicker, openTimeframePicker } from '@trdrs/quickcharts'
2408
+
2409
+ declare const timeframeField: HTMLButtonElement
2410
+ declare const timeframeBox: HTMLElement
2411
+ declare const backtest: { timeframe: string; setTimeframe(timeframe: string): void }
2412
+
2413
+ timeframeField.addEventListener('click', () => {
2414
+ const picker = openTimeframePicker({
2415
+ anchor: timeframeField,
2416
+ timeframe: backtest.timeframe,
2417
+ resolutions: ['1m', '5m', '15m', '1h', '4h', '1d'], // what the feed serves
2418
+ theme: { mode: 'light' },
2419
+ onPick: (timeframe) => backtest.setTimeframe(timeframe),
2420
+ })
2421
+ void picker // picker.close() closes it from outside; Escape and a press outside close it too
2422
+ })
2423
+
2424
+ const card = mountTimeframePicker({
2425
+ container: timeframeBox,
2426
+ timeframes: ['5m', '15m', '1h', '4h', '1d'],
2427
+ timeframe: '1h',
2428
+ onPick: (timeframe) => backtest.setTimeframe(timeframe),
2429
+ onClose: () => timeframeBox.remove(),
2430
+ })
2431
+ card.focus()
2432
+ ```
2433
+
1880
2434
  ### The legend
1881
2435
 
1882
- Every chart carries its own legend over its plot: an identity row naming the market, the interval
2436
+ Every chart carries its own legend over its plot: an identity row naming the market, the timeframe
1883
2437
  and the venue, and a values row carrying the hovered bar's O H L C and its move against the previous
1884
2438
  close. The numbers are written through that chart's own formatter, so a level in the legend is the
1885
2439
  level its price axis writes, and they follow the pointer while the crosshair is on the plot and the
@@ -1895,10 +2449,10 @@ The `size` is the chart's, not a suggestion: the box is already laid out, and pa
1895
2449
  you were given is what lines the marks up down a list. The chart ships no artwork and fetches none,
1896
2450
  so without this every mark is the neutral monogram it draws itself.
1897
2451
 
1898
- `venueMark` and `providerMark` paint the source at the end of every symbol search and compare row
1899
- the same way: the venue a market lists on, or the data provider where a row names no venue. Each
1900
- receives the name the row writes (`exchange` or `provider`), the element and the size, and returns
1901
- a disposer. Without them a source wears its initial on a neutral disc.
2452
+ `venueMark` and `dataSourceMark` paint the source at the end of every symbol search and compare
2453
+ row the same way: the venue a market lists on, or the data source where a row names no venue. Each
2454
+ receives the name the row writes (`exchange` or `dataSource`), the element and the size, and
2455
+ returns a disposer. Without them a source wears its initial on a neutral disc.
1902
2456
 
1903
2457
  ```ts
1904
2458
  import { createChart, createUdfDatafeed } from '@trdrs/quickcharts'
@@ -1942,7 +2496,7 @@ re-tile, a restore. Pointing your own surface at a chart moves no chart's symbol
1942
2496
  concepts stay separate: each chart keeps charting what it charts, and one of them is the one you are
1943
2497
  looking at.
1944
2498
 
1945
- Five sync toggles fan changes across the charts: `symbol`, `interval` and `dateRange` replay a
2499
+ Five sync toggles fan changes across the charts: `symbol`, `timeframe` and `dateRange` replay a
1946
2500
  change onto every chart, `crosshair` mirrors continuously by time, and `time` centers every chart on
1947
2501
  a clicked moment. The whole layout serializes as ONE opaque content blob (arrangement, sync flags,
1948
2502
  durable chart-entity identities, active chart, every chart's own content), so a saved multi-chart
@@ -2019,19 +2573,21 @@ const saved = drawings?.export() // the persistence wire format (SerializedDrawi
2019
2573
  note(`${selected?.type ?? 'nothing'} selected, ${saved?.length ?? 0} drawings`)
2020
2574
  ```
2021
2575
 
2022
- ### The toolbar
2576
+ ### The drawing toolbar
2023
2577
 
2024
- The toolbar is the rail down the chart's leading edge. Its buttons are the cursor with its three
2578
+ The drawing toolbar runs down the chart's leading edge. Its buttons are the cursor with its three
2025
2579
  pointer modes and the eraser; seven tool groups, each opening a flyout of the group's sections
2026
2580
  with a star on every row that lands the tool on the favorites bar; Measure and Zoom; the magnet
2027
2581
  with its weak and strong strengths; stay in drawing mode; lock all; the eye that hides drawings,
2028
2582
  indicators, or both; drawing sync, shown only in a layout of more than one chart; the remove menu,
2029
2583
  which names what each row takes and carries the locked-item policy; and the favorites star. Each
2030
2584
  group button wears the tool it last armed, and every action is a `chart.drawings.*` command
2031
- through the registry: a control renders disabled, never hidden, while the registry would not run
2032
- its command (a tool or verb your access policy refuses, a selection verb with nothing selected,
2033
- the Image tool without an asset port), and a command the policy refuses answers `denied` from the
2034
- toolbar as from anywhere else, including `chart.drawings.arm` for a refused tool. An image is
2585
+ through the registry: a control renders disabled while the registry would not run its command (a
2586
+ tool or verb your access policy refuses, a selection verb with nothing selected, the Image tool
2587
+ without an asset port), and with `access.refused: 'hide'` a tool or verb the policy refuses is left
2588
+ out instead (see Refused controls), and a command the policy refuses answers `denied` from the
2589
+ toolbar as from anywhere else, including `chart.drawings.arm` for a refused tool. A tool your
2590
+ `drawingTools` list leaves out is not on the toolbar at all (see Offered drawing tools). An image is
2035
2591
  placed through `chart.drawings.placeImage`, from the picker or a system-clipboard paste over the
2036
2592
  chart. Every flyout, palette and dialog a surface opens sits inside the chart root and closes with
2037
2593
  it, and the eye and lock all announce their state through a live region. Lock all also makes Paste
@@ -2048,7 +2604,7 @@ chosen.
2048
2604
  Selecting a drawing shows the settings bar: templates, the stroke color with its opacity, the
2049
2605
  background for tools that have one, the text color and font size for text tools, thickness and
2050
2606
  line style, the settings gear, lock, delete, and a More menu with the stacking moves, the
2051
- per-interval visibility presets, clone, copy and hide. Every edit persists at once and becomes the
2607
+ per-timeframe visibility presets, clone, copy and hide. Every edit persists at once and becomes the
2052
2608
  tool's default for the next drawing of that type. The settings dialog opens from the gear with
2053
2609
  Inputs, Style, Text, Table, Coordinates and Visibility pages as the tool has them; its edits apply
2054
2610
  live, Cancel restores the drawing, and Ok commits the session as one edit.
@@ -2057,7 +2613,7 @@ Text-bearing tools open an inline editor where the text sits, in the drawing's o
2057
2613
  placement committed empty is removed; an existing note committed empty is blanked. Ctrl or Cmd
2058
2614
  with Enter commits, Escape cancels, and a press on the chart commits.
2059
2615
 
2060
- Templates are named setups a trader saves from either surface and applies on demand. They and
2616
+ Templates are named setups a viewer saves from either surface and applies on demand. They and
2061
2617
  the remembered defaults ride `ChartSaveLoadAdapter.templates('drawing')`, so a host that keeps
2062
2618
  saved charts on a server keeps these there too. Same-tool default changes are written in issue
2063
2619
  order, and a delayed read or failed write does not replace the newest local choice. Store work is
@@ -2085,20 +2641,25 @@ layer.armTool('rectangle')
2085
2641
  layer.destroy()
2086
2642
  ```
2087
2643
 
2644
+ A layer you mount yourself arms whatever you arm. Its `copies` option answers whether a new drawing
2645
+ may be made as a copy of a drawing of a type: return false for a tool you do not offer, and clone,
2646
+ paste and a Control- or Command-drag duplicate make nothing for its drawings, `canPaste` answers
2647
+ false, and the drag moves the drawing itself.
2648
+
2088
2649
  ### `@trdrs/quickcharts/drawings`
2089
2650
 
2090
2651
  Everything about drawings a host builds its own UI from is one subpath. A host that never draws
2091
2652
  never imports any of it.
2092
2653
 
2093
2654
  ```ts
2094
- import { buildRailGroups, drawingTools, parseDrawingsStore, restoreDrawings, serializeDrawingsStore } from '@trdrs/quickcharts/drawings'
2655
+ import { buildDrawingToolbarGroups, drawingTools, parseDrawingsStore, restoreDrawings, serializeDrawingsStore } from '@trdrs/quickcharts/drawings'
2095
2656
 
2096
2657
  // The catalog: 90 tools in 14 categories, read-only.
2097
2658
  const trendLine = drawingTools.get('trend_line')
2098
2659
  const fibs = drawingTools.byCategory('fibonacci')
2099
2660
 
2100
- // The rail's own structure, as data: seven groups, their sections, and a catalog key per heading.
2101
- for (const group of buildRailGroups()) {
2661
+ // The drawing toolbar as data: seven groups, their sections, and a catalog key per heading.
2662
+ for (const group of buildDrawingToolbarGroups()) {
2102
2663
  for (const section of group.sections) note(`${group.id}/${section.label}: ${section.tools.length}`)
2103
2664
  }
2104
2665
 
@@ -2108,10 +2669,10 @@ const drawings = restoreDrawings(store['ES'] ?? [])
2108
2669
  localStorage.setItem('acme.chart.drawings', serializeDrawingsStore(store))
2109
2670
  ```
2110
2671
 
2111
- The subpath carries the workflow models too: what the rail's eye blanks (`HideMode`, which reaches
2112
- chart-owned drawings and indicators and nothing else), the cursor modes and the two transient
2113
- tools, the magnet policy over `magnetSnap`, the remove menu, favorites over a `FavoritesPort`, the
2114
- standing preference record, and per-tool defaults and named templates over
2672
+ The subpath carries the workflow models too: what the drawing toolbar's eye blanks (`HideMode`,
2673
+ which reaches chart-owned drawings and indicators and nothing else), the cursor modes and the two
2674
+ transient tools, the magnet policy over `magnetSnap`, the remove menu, favorites over a
2675
+ `FavoritesPort`, the standing preference record, and per-tool defaults and named templates over
2115
2676
  `ChartSaveLoadAdapter.templates('drawing')`. Each is a pure function or a plain record, so a host
2116
2677
  builds its own controls without reimplementing the decisions behind them.
2117
2678
 
@@ -2119,7 +2680,7 @@ What a lock refuses is the library's own. A host locks and unlocks through the d
2119
2680
  reads what they answer: `chart.drawings.lock` holds one drawing where it stands,
2120
2681
  `chart.drawings.lockAll` suspends the whole chart, and every editing command answers `ok` or
2121
2682
  `unavailable` for the state the chart is actually in, so a control built on those outcomes says
2122
- exactly what the chart will accept. The gesture list below is what a trader meets.
2683
+ exactly what the chart will accept. The gesture list below is what a viewer meets.
2123
2684
 
2124
2685
  ```ts
2125
2686
  import { blanks, chooseHideMode, DEFAULT_HIDE_STATE, DrawingTemplates } from '@trdrs/quickcharts/drawings'
@@ -2154,9 +2715,9 @@ note(IMAGE_ACCEPT)
2154
2715
 
2155
2716
  What to know:
2156
2717
 
2157
- - **The subpath is a subset, not a re-export.** The drawing classes, the model store and the
2158
- mutable registry stay inside the library. There is no door for a host-authored tool: tool
2159
- contribution belongs to the access-policy plane, not to a bare registration call.
2718
+ - **The subpath is a deliberate subset.** The drawing classes, the model store and the
2719
+ mutable registry stay inside the library. A host chooses among the catalog's tools through
2720
+ `drawingTools` and the access policy.
2160
2721
  - **Every tool places.** Fixed-anchor tools place by press-drag-release or click then click; an
2161
2722
  instant tool (the position tools) lands whole from one press; a multipoint tool adds a point per
2162
2723
  click until a double-click ends the run; a freehand tool captures the drag as a stroke; a
@@ -2177,13 +2738,13 @@ What to know:
2177
2738
  so anchors never drift apart); grab an anchor handle to reshape; hold Shift to constrain a
2178
2739
  two-point placement or an anchor drag to 45 degree rays; Ctrl-drag duplicates; the magnet pulls
2179
2740
  a placed or dragged anchor onto the bar's own open, high, low or close; a locked drawing
2180
- selects, takes the Delete and Clone a trader asks for by name, and refuses a move, a resize, a
2741
+ selects, takes the Delete and Clone a viewer asks for by name, and refuses a move, a resize, a
2181
2742
  text edit, the eraser and a Ctrl-drag copy; lock all suspends every edit, Delete and Clone
2182
2743
  included, until it is released.
2183
2744
 
2184
2745
  ## Extensions
2185
2746
 
2186
- An extension is host code that draws on the chart, adds rows to its level menu, offers commands,
2747
+ An extension is host code that draws on the chart, adds rows to its context menu, offers commands,
2187
2748
  and stores viewer state in the chart's own save blob. The chart attaches it at mount, pushes its
2188
2749
  changes at it, and takes it down at teardown, along with everything it drew.
2189
2750
 
@@ -2237,9 +2798,9 @@ What to know:
2237
2798
  layer on the document body, painted with the widget's theme: a menu or an editor mounted there at
2238
2799
  viewport coordinates stands over every pane and over whatever the page stacks around the widget,
2239
2800
  which the pane's own overlay cannot promise. `ctx.symbolTitle()` is the name symbology gives the
2240
- charted market, the name the legend and the level menu print; `ctx.chart.symbol()` stays the
2801
+ charted market, the name the legend and the context menu print; `ctx.chart.symbol()` stays the
2241
2802
  ticker an action is sent for. `ctx.painters` carries the mark painters you supplied as `.symbol`,
2242
- `.venue` and `.provider`, so a surface that names a market paints it with the same mark the
2803
+ `.venue` and `.dataSource`, so a surface that names a market paints it with the same mark the
2243
2804
  legend and the search rows wear rather than shipping artwork of its own; a painter you did not
2244
2805
  supply is `null`, and the surface writes the name alone.
2245
2806
  - **A contributed row may bring its own glyph.** `ChartExtensionMenuItem.icon` is a
@@ -2254,19 +2815,20 @@ What to know:
2254
2815
  default), an action on the price the pointer landed on, placed under Copy price and Paste; or
2255
2816
  `view`, a switch over what the chart shows, placed under the remove rows. Rows keep the order
2256
2817
  they were contributed in within their group.
2257
- - **A drawn layer can join the rail's eye.** `ctx.contributeHideLayer({ id, label, icon, apply })`
2258
- lists the layer in the eye's menu after the chart's own drawings and indicators and before
2259
- "Hide all", which blanks it with them. `label` carries the row's wording in both states, `icon`
2260
- the two marks the eye wears while the layer is its subject, and `apply(hidden)` is called with
2261
- the current state at contribution and on every change. The handle reads `hidden()`, flips
2262
- `setHidden()` through the same eye the rail drives, and `remove()` withdraws the layer.
2818
+ - **A drawn layer can join the drawing toolbar's eye.**
2819
+ `ctx.contributeHideLayer({ id, label, icon, apply })` lists the layer in the eye's menu after the
2820
+ chart's own drawings and indicators and before "Hide all", which blanks it with them. `label`
2821
+ carries the row's wording in both states, `icon` the two marks the eye wears while the layer is
2822
+ its subject, and `apply(hidden)` is called with the current state at contribution and on every
2823
+ change. The handle reads `hidden()`, flips `setHidden()` through the same eye the drawing toolbar
2824
+ drives, and `remove()` withdraws the layer.
2263
2825
  - **A printed chord is a real binding.** `ChartExtensionMenuItem.shortcut` prints on the row and
2264
2826
  answers to the key. The chart's dispatcher offers a press no built-in verb claims to the rows your
2265
- provider contributes for the level under the pointer and runs that row's own `run`, the same call
2266
- the click makes. The press acts on the tile the pointer is inside, active or not; outside every
2267
- tile, on a point with no readable level, while the viewer is typing, or while a modal holds the
2268
- keyboard, the key is left to your page. A row your provider withholds claims no key, and a row
2269
- carries one chord.
2827
+ `contributeContextMenu` callback returns for the level under the pointer and runs that row's own
2828
+ `run`, the same call the click makes. The press acts on the tile the pointer is inside, active or
2829
+ not; outside every tile, on a point with no readable level, while the viewer is typing, or while
2830
+ a modal holds the keyboard, the key is left to your page. A row your callback withholds claims no
2831
+ key, and a row carries one chord.
2270
2832
  - **A glyph is bounded, and a refusal is free.** Up to 8 shapes, each up to 2048 characters of path
2271
2833
  data with every number in it finite, and an `outline` stroke width above 0 and up to 8. A shape
2272
2834
  outside that is dropped and the rest of the glyph still draws; a descriptor outside it draws
@@ -2289,7 +2851,7 @@ What to know:
2289
2851
  re-attaches it on every switch, so a market-scoped overlay cannot carry one market's drawing onto
2290
2852
  another's bars.
2291
2853
  - **A failing extension is its own problem.** A throw in `attach` drops that extension and the
2292
- chart still mounts; a throw in a subscriber, a menu provider, a command or a teardown is
2854
+ chart still mounts; a throw in a subscriber, a menu builder, a command or a teardown is
2293
2855
  contained.
2294
2856
  - **Layouts attach per chart.** A widget hands its shared options to every chart it tiles, so each
2295
2857
  chart gets its own attachment, its own context and its own state slot.
@@ -2317,7 +2879,7 @@ Every claim in this document maps to a test or a generated artifact in the packa
2317
2879
  and the tarball, and the third-party notices are held by the boundary tests and
2318
2880
  `THIRD-PARTY-NOTICES.md`.
2319
2881
 
2320
- ## Versioning & deprecation
2882
+ ## Versioning
2321
2883
 
2322
2884
  - **SemVer, enforced at the gate.** The public surface is pinned by an API-surface test (every
2323
2885
  exported name and its runtime kind), the shipped type declarations are compiled against by a
@@ -2329,8 +2891,8 @@ Every claim in this document maps to a test or a generated artifact in the packa
2329
2891
  and fields (`config` and `serverTime` are the pattern): an existing
2330
2892
  implementation keeps compiling, and the widget treats absence as "unconstrained / not supported".
2331
2893
  Your integration never breaks by standing still within a major.
2332
- - **Deprecation runs a full major.** A deprecated export keeps working for the remainder of the
2333
- current major, is marked `@deprecated` in the types with its replacement named in the note (your
2334
- editor flags every call site), and is removed only in the next major, never silently.
2894
+ - **A major version may rename or remove public names.** Its upgrading guide in
2895
+ [CHANGELOG.md](CHANGELOG.md) lists each one, old to new, with how saved state carries over: what
2896
+ the previous major saved still opens, and the next save writes the current names.
2335
2897
  - **The wire timeframe grammar is stable vocabulary.** `<N><unit>` with units `t s m h d w mo`.
2336
2898
  Extensions may add units; an existing token never changes meaning.