@trdrs/quickcharts 1.0.1 → 1.2.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.
package/dist/index.js CHANGED
@@ -8949,24 +8949,52 @@ var OPERATOR_GLYPHS = {
8949
8949
 
8950
8950
  // src/ui/chrome/dialog.ts
8951
8951
  function openDialog(options) {
8952
- const scrim = h("div", { class: "qc-scrim qc-dialog-scrim" });
8952
+ const scrim = h("div", { class: `qc-scrim qc-dialog-scrim${options.scrimClassName ? ` ${options.scrimClassName}` : ""}` });
8953
8953
  const box = h("div", { class: `qc-overlay qc-dialog${options.className ? ` ${options.className}` : ""}`, role: options.ariaRole ?? "dialog", "aria-modal": "true", "aria-label": options.label, "data-role": options.role });
8954
8954
  if (options.width) box.style.width = `${options.width}px`;
8955
8955
  stopPointer(box);
8956
8956
  scrim.appendChild(box);
8957
+ if (options.exitMs) scrim.dataset.state = "opening";
8957
8958
  let isOpen = true;
8958
8959
  const previouslyFocused = document.activeElement;
8959
8960
  const escape = pushEscapeOwner();
8960
8961
  let untrack = () => void 0;
8961
- const close = () => {
8962
- if (!isOpen) return;
8962
+ let removed = false;
8963
+ let exitTimer = null;
8964
+ const remove = () => {
8965
+ if (removed) return;
8966
+ removed = true;
8967
+ if (exitTimer !== null) clearTimeout(exitTimer);
8968
+ scrim.remove();
8969
+ options.onClose?.();
8970
+ };
8971
+ const close = (closeOptions) => {
8972
+ if (!isOpen) {
8973
+ if (closeOptions?.animate === false) remove();
8974
+ return;
8975
+ }
8963
8976
  isOpen = false;
8977
+ options.onClosing?.();
8964
8978
  escape.release();
8965
8979
  untrack();
8966
8980
  document.removeEventListener("keydown", onKey, true);
8967
- scrim.remove();
8968
- options.onClose?.();
8969
8981
  previouslyFocused?.focus?.();
8982
+ const reduceMotion = document.defaultView?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
8983
+ if (closeOptions?.animate === false || !options.exitMs || reduceMotion) {
8984
+ remove();
8985
+ return;
8986
+ }
8987
+ scrim.dataset.state = "closing";
8988
+ scrim.style.pointerEvents = "none";
8989
+ box.inert = true;
8990
+ box.removeAttribute("role");
8991
+ box.removeAttribute("aria-modal");
8992
+ box.setAttribute("aria-hidden", "true");
8993
+ const onAnimationEnd = (event) => {
8994
+ if (event.target === box) remove();
8995
+ };
8996
+ box.addEventListener("animationend", onAnimationEnd, { once: true });
8997
+ exitTimer = setTimeout(remove, options.exitMs + 50);
8970
8998
  };
8971
8999
  const onKey = (event) => {
8972
9000
  if (event.key === "Escape" && ownsEscape(escape.token)) {
@@ -8998,6 +9026,10 @@ function openDialog(options) {
8998
9026
  const handle = { element: box, close, open: () => isOpen };
8999
9027
  options.build(box, handle);
9000
9028
  (layerFor(options.host) ?? options.host).appendChild(scrim);
9029
+ if (options.exitMs) {
9030
+ scrim.getBoundingClientRect();
9031
+ scrim.dataset.state = "open";
9032
+ }
9001
9033
  untrack = trackOverlay(options.host, close);
9002
9034
  document.addEventListener("keydown", onKey, true);
9003
9035
  const target = options.initialFocus?.(box) ?? focusables(box)[0] ?? null;
@@ -14974,6 +15006,7 @@ function mountChartLegend(container, strings, controls) {
14974
15006
  update(next) {
14975
15007
  chip = next;
14976
15008
  row2.dataset.qcHidden = String(chip.hidden);
15009
+ row2.title = chip.description ?? "";
14977
15010
  rowMark.element.hidden = !chip.mark;
14978
15011
  if (!chip.mark) {
14979
15012
  dropMark?.();
@@ -15008,7 +15041,7 @@ function mountChartLegend(container, strings, controls) {
15008
15041
  if (chip.tone && !chip.hidden && !chip.note) value.dataset.qcTone = chip.tone;
15009
15042
  else delete value.dataset.qcTone;
15010
15043
  gear.hidden = !chip.hasInputs || !controls.onSettings;
15011
- nameButton(gear, strings.t("legend.indicatorSettings"));
15044
+ nameButton(gear, chip.settingsLabel ?? strings.t("legend.indicatorSettings"));
15012
15045
  collapse.hidden = !chip.pane || !controls.onPaneOp || chip.hidden;
15013
15046
  collapse.replaceChildren(controls.icons.glyph(chip.collapsed ? ICONS.paneRestore : ICONS.paneCollapse, { size: 18 }));
15014
15047
  nameButton(collapse, strings.t(chip.collapsed ? "legend.restorePane" : "legend.collapsePane"));
@@ -15021,6 +15054,7 @@ function mountChartLegend(container, strings, controls) {
15021
15054
  eye.dataset.hidden = eyeState;
15022
15055
  }
15023
15056
  nameButton(eye, strings.t(chip.hidden ? "legend.showIndicator" : "legend.hideIndicator"));
15057
+ eye.hidden = chip.hideable === false;
15024
15058
  remove.hidden = !chip.removable || !controls.onRemove;
15025
15059
  nameButton(remove, chip.removeLabel ?? strings.t("legend.removeCompare"));
15026
15060
  },
@@ -15566,6 +15600,7 @@ function openMarketStatus(anchor, deps) {
15566
15600
 
15567
15601
  // src/widget/legend.ts
15568
15602
  var COMPARE_ROW_PREFIX = "cmp:";
15603
+ var HOST_ROW_PREFIX = "host:";
15569
15604
  function barIndexAt(bars, time) {
15570
15605
  if (bars.length === 0) return -1;
15571
15606
  if (time === null) return bars.length - 1;
@@ -15594,10 +15629,12 @@ function attachLegendPlane(deps) {
15594
15629
  push: () => void 0,
15595
15630
  setHeader: () => void 0,
15596
15631
  setDot: () => void 0,
15632
+ setHostRows: () => void 0,
15597
15633
  destroy: () => void 0
15598
15634
  };
15599
15635
  }
15600
15636
  let destroyed = false;
15637
+ const hostRows = /* @__PURE__ */ new Map();
15601
15638
  const paneRemembered = /* @__PURE__ */ new Map();
15602
15639
  const paneKeys = () => {
15603
15640
  const keys = { 0: "main" };
@@ -15653,6 +15690,11 @@ function attachLegendPlane(deps) {
15653
15690
  }
15654
15691
  } : {},
15655
15692
  onSettings: (id, rect) => {
15693
+ const host2 = hostRows.get(id);
15694
+ if (host2) {
15695
+ host2.onSettings?.();
15696
+ return;
15697
+ }
15656
15698
  if (deps.openIndicatorSettings(id)) return;
15657
15699
  const inst = deps.indicators.list().find((i) => i.id === id);
15658
15700
  if (!inst) return;
@@ -15723,7 +15765,21 @@ function attachLegendPlane(deps) {
15723
15765
  const remembered = paneRemembered.get(`study:${row2.id}`);
15724
15766
  return { ...row2, paneIndex, maximized: remembered !== void 0 && !row2.collapsed && (deps.chart.panes()[paneIndex]?.getHeight() ?? 0) > remembered };
15725
15767
  }),
15726
- ...(deps.compare?.chips(hovered) ?? []).map((row2) => ({ ...row2, paneIndex: deps.compare.handle.paneIndexOf(row2.id.slice(COMPARE_ROW_PREFIX.length)) ?? 0 }))
15768
+ ...(deps.compare?.chips(hovered) ?? []).map((row2) => ({ ...row2, paneIndex: deps.compare.handle.paneIndexOf(row2.id.slice(COMPARE_ROW_PREFIX.length)) ?? 0 })),
15769
+ ...[...hostRows].map(([id, row2]) => ({
15770
+ id,
15771
+ title: row2.title,
15772
+ inputs: row2.inputs,
15773
+ value: null,
15774
+ note: row2.status,
15775
+ description: row2.description,
15776
+ settingsLabel: row2.settingsLabel,
15777
+ hidden: false,
15778
+ hideable: false,
15779
+ removable: false,
15780
+ hasInputs: !!row2.onSettings,
15781
+ paneIndex: 0
15782
+ }))
15727
15783
  ]);
15728
15784
  const tops = {};
15729
15785
  let top = 0;
@@ -15766,6 +15822,12 @@ function attachLegendPlane(deps) {
15766
15822
  };
15767
15823
  deps.chart.subscribeCrosshairMove(onCrosshair);
15768
15824
  return {
15825
+ setHostRows(rows2) {
15826
+ if (destroyed) return;
15827
+ hostRows.clear();
15828
+ for (const row2 of rows2) hostRows.set(`${HOST_ROW_PREFIX}${row2.id}`, { ...row2 });
15829
+ paint();
15830
+ },
15769
15831
  push() {
15770
15832
  paint();
15771
15833
  },
@@ -15791,6 +15853,7 @@ function attachLegendPlane(deps) {
15791
15853
  },
15792
15854
  destroy() {
15793
15855
  destroyed = true;
15856
+ hostRows.clear();
15794
15857
  resize?.disconnect();
15795
15858
  observed.clear();
15796
15859
  deps.chart.unsubscribeCrosshairMove(onCrosshair);
@@ -16929,7 +16992,13 @@ function registerChartCommands(deps) {
16929
16992
  available: () => features.history && handle.history.canRedo(),
16930
16993
  execute: () => handle.history.redo()
16931
16994
  });
16932
- add({ id: "chart.indicators.open", scope: "chart", label: "chrome.indicators", available: () => ui.indicatorPicker, execute: () => deps.indicatorsOpen() });
16995
+ add({
16996
+ id: "chart.indicators.open",
16997
+ scope: "chart",
16998
+ label: "chrome.indicators",
16999
+ available: () => ui.indicatorPicker,
17000
+ execute: (arg) => deps.indicatorsOpen(arg && typeof arg === "object" && "collection" in arg && typeof arg.collection === "string" ? arg.collection : void 0)
17001
+ });
16933
17002
  const isInstance = (arg) => !!arg && typeof arg === "object" && typeof arg.id === "string" && typeof arg.definition === "object";
16934
17003
  add({
16935
17004
  id: "chart.indicators.add",
@@ -18726,7 +18795,7 @@ function createChartInstance(deps) {
18726
18795
  level: () => menuLevel,
18727
18796
  formatter: () => symbolFormatter,
18728
18797
  compareOpen: (mode, changeFrom) => compare?.openDialog(mode, changeFrom),
18729
- indicatorsOpen: () => deps.doors.showIndicatorPicker(),
18798
+ indicatorsOpen: (collection) => deps.doors.showIndicatorPicker(collection),
18730
18799
  symbolSearchOpen: () => deps.doors.openSearch({ mode: "search", chart: handle }),
18731
18800
  drawingVerbs: () => drawings2.verbs
18732
18801
  });
@@ -18773,6 +18842,7 @@ function createChartInstance(deps) {
18773
18842
  }
18774
18843
  return {
18775
18844
  handle,
18845
+ setLegendRows: (rows2) => legend2.setHostRows(rows2),
18776
18846
  screenshot: () => chart.takeScreenshot(),
18777
18847
  repaintTheme() {
18778
18848
  applyLook();
@@ -20596,7 +20666,7 @@ function openIndicatorPicker(deps) {
20596
20666
  } catch {
20597
20667
  }
20598
20668
  let query = "";
20599
- let collection = "builtin";
20669
+ let collection = deps.initialCollection === "favorites" || collections?.some((row2) => row2.id === deps.initialCollection) ? deps.initialCollection : "builtin";
20600
20670
  let generation = 0;
20601
20671
  let controller = null;
20602
20672
  let busy = false;
@@ -20909,6 +20979,7 @@ function buildSearchSurface(deps, box, frame) {
20909
20979
  markDisposers = [];
20910
20980
  for (const drop of drops) drop();
20911
20981
  };
20982
+ let scopeMarkDisposer = null;
20912
20983
  const input = h("input", { type: "text", role: "combobox", class: "qc-search-input", "aria-label": t("search.placeholder"), placeholder: t("search.placeholder"), autocomplete: "off", "aria-autocomplete": "list", "aria-expanded": "true", "aria-controls": listId, spellcheck: "false", value: query });
20913
20984
  const clear = button2({ label: t("search.clear"), icon: deps.icons.glyph(ICONS.clear, { size: 18 }), className: "qc-search-op", onClick: () => setQuery("") });
20914
20985
  const rule = compare ? null : h("span", { class: "qc-search-rule", "aria-hidden": "true" });
@@ -20948,25 +21019,37 @@ function buildSearchSurface(deps, box, frame) {
20948
21019
  const classes = compare ? null : deps.classes();
20949
21020
  let strip = null;
20950
21021
  const classChips = /* @__PURE__ */ new Map();
20951
- if (classes && classes.length > 0) {
21022
+ const searchScope = deps.scope?.() ?? null;
21023
+ if (classes && classes.length > 0 || searchScope) {
20952
21024
  strip = h("div", { class: "qc-search-classes", role: "group", "aria-label": t("search.classFilter") });
20953
- const all = [{ id: "", label: t("search.allClasses") }, ...classes.map((id) => ({ id, label: deps.classNames?.[id] ?? id }))];
20954
- for (const c of all) {
20955
- const chip = button2({
20956
- label: c.label,
20957
- text: c.label,
20958
- className: "qc-chip qc-search-class",
20959
- pressed: c.id === cls,
20960
- onClick: () => {
20961
- cls = c.id;
20962
- for (const [id, b] of classChips) b.setAttribute("aria-pressed", String(id === cls));
20963
- active = -1;
20964
- search2.search(serverQuery(), cls);
20965
- render();
20966
- }
20967
- });
20968
- classChips.set(c.id, chip);
20969
- strip.appendChild(chip);
21025
+ if (classes && classes.length > 0) {
21026
+ const all = [{ id: "", label: t("search.allClasses") }, ...classes.map((id) => ({ id, label: deps.classNames?.[id] ?? id }))];
21027
+ for (const c of all) {
21028
+ const chip = button2({
21029
+ label: c.label,
21030
+ text: c.label,
21031
+ className: "qc-chip qc-search-class",
21032
+ pressed: c.id === cls,
21033
+ onClick: () => {
21034
+ cls = c.id;
21035
+ for (const [id, b] of classChips) b.setAttribute("aria-pressed", String(id === cls));
21036
+ active = -1;
21037
+ search2.search(serverQuery(), cls);
21038
+ render();
21039
+ }
21040
+ });
21041
+ classChips.set(c.id, chip);
21042
+ strip.appendChild(chip);
21043
+ }
21044
+ }
21045
+ if (searchScope) {
21046
+ const mark = h("span", { class: "qc-search-scope-mark", "aria-hidden": "true" });
21047
+ const drop = searchScope.mark?.({ host: mark, size: SOURCE_MARK_SIZE });
21048
+ if (typeof drop === "function") {
21049
+ mark.dataset.qcHost = "true";
21050
+ scopeMarkDisposer = drop;
21051
+ } else if (mark.childNodes.length === 0) mark.textContent = searchScope.label.charAt(0).toUpperCase();
21052
+ strip.appendChild(h("span", { class: "qc-search-scope", "aria-label": searchScope.label }, mark, h("span", {}, searchScope.label)));
20970
21053
  }
20971
21054
  }
20972
21055
  const list = h("div", { class: "qc-search-list", role: "listbox", id: listId, "aria-label": t("search.results") });
@@ -21135,6 +21218,8 @@ function buildSearchSurface(deps, box, frame) {
21135
21218
  setDisabled(clear, false);
21136
21219
  return {
21137
21220
  teardown() {
21221
+ scopeMarkDisposer?.();
21222
+ scopeMarkDisposer = null;
21138
21223
  releaseMarks();
21139
21224
  search2.dispose();
21140
21225
  observer?.disconnect();
@@ -21150,6 +21235,8 @@ function openSearchDialog(deps) {
21150
21235
  host: deps.host,
21151
21236
  label: title,
21152
21237
  className: `qc-search-dialog qc-search-dialog--${mode}`,
21238
+ scrimClassName: "qc-search-dialog-scrim",
21239
+ exitMs: 140,
21153
21240
  // The compare family (compare, and a compare row's change-symbol) shares one role for a host
21154
21241
  // test to find; a symbol search, with or without a chart behind it, is the other.
21155
21242
  role: mode === "compare" || mode === "change-symbol" ? "compare-dialog" : "symbol-search",
@@ -21929,127 +22016,238 @@ var TOGGLE_ROWS = [
21929
22016
  { leaf: "grid", label: "settings.gridLines" },
21930
22017
  { leaf: "sessions", label: "settings.sessionShading" }
21931
22018
  ];
22019
+ var nextSettingsId = 0;
21932
22020
  function mountSettingsMenu(deps) {
21933
22021
  const t = () => deps.i18n.t;
21934
- let menu2 = null;
22022
+ const settingsId = ++nextSettingsId;
22023
+ let dialog = null;
22024
+ let content = null;
22025
+ let activePage = "appearance";
22026
+ let committed = false;
22027
+ let initial = null;
21935
22028
  const controls = [];
21936
22029
  const retireControls = () => {
21937
22030
  for (const control of controls) control.destroy();
21938
22031
  controls.length = 0;
21939
22032
  };
21940
- const trigger = button2({ label: t()("settings.menu"), icon: deps.icons.glyph(ICONS.settings), className: "qc-toolbar-button", onClick: () => toggleMenu(trigger, open2) });
22033
+ const trigger = button2({
22034
+ label: t()("settings.menu"),
22035
+ icon: deps.icons.glyph(ICONS.settings),
22036
+ className: "qc-toolbar-button",
22037
+ onClick: () => {
22038
+ if (dialog) {
22039
+ if (dialog.open()) dialog.close();
22040
+ return;
22041
+ }
22042
+ open2();
22043
+ }
22044
+ });
21941
22045
  trigger.setAttribute("aria-haspopup", "dialog");
21942
22046
  trigger.setAttribute("aria-expanded", "false");
21943
22047
  const apply = (partial) => {
21944
22048
  deps.commands.execute("chart.appearance.apply", { appearance: partial });
21945
22049
  };
22050
+ const refresh = () => {
22051
+ if (!dialog?.open() || !content) return;
22052
+ retireControls();
22053
+ content.replaceChildren();
22054
+ buildContent(content);
22055
+ };
22056
+ const pageDefinitions = () => [
22057
+ { id: "appearance", label: t()("settings.sectionAppearance"), icon: STYLE_ICONS.candles },
22058
+ { id: "display", label: t()("settings.sectionDisplay"), icon: ICONS.template },
22059
+ { id: "scale", label: t()("settings.sectionScale"), icon: ICONS.ruler },
22060
+ ...deps.ui.settingsTheme ? [{ id: "theme", label: t()("settings.sectionTheme"), icon: ICONS.settings }] : []
22061
+ ];
22062
+ const showPage = (page, focus = false) => {
22063
+ activePage = page;
22064
+ refresh();
22065
+ if (focus) content?.querySelector(`[data-settings-page="${page}"]`)?.focus();
22066
+ };
22067
+ const buildAppearancePage = (panel) => {
22068
+ const appearance = activeChart(deps).appearance().appearance;
22069
+ const canApply = deps.commands.available("chart.appearance.apply");
22070
+ panel.appendChild(menuHeading(t()("settings.sectionAppearance")));
22071
+ for (const row2 of COLOR_ROWS) {
22072
+ const label2 = t()(row2.label);
22073
+ const value = appearance[row2.leaf];
22074
+ const control = createColorControl(t(), {
22075
+ label: label2,
22076
+ value,
22077
+ opacity: readColor(value)?.alpha ?? 1,
22078
+ disabled: !canApply,
22079
+ closeOnPick: true,
22080
+ onPick: (color2) => {
22081
+ control.update(color2);
22082
+ apply({ [row2.leaf]: color2 });
22083
+ },
22084
+ openPanel: (anchor, palette, onClosed) => openInlinePanel(anchor, palette, anchor.closest(".qc-settings-row") ?? anchor, onClosed)
22085
+ });
22086
+ controls.push(control);
22087
+ panel.appendChild(fieldRow(label2, control.element, { className: "qc-settings-row" }));
22088
+ }
22089
+ };
22090
+ const buildDisplayPage = (panel) => {
22091
+ const appearance = activeChart(deps).appearance().appearance;
22092
+ const canApply = deps.commands.available("chart.appearance.apply");
22093
+ panel.appendChild(menuHeading(t()("settings.sectionDisplay")));
22094
+ for (const row2 of TOGGLE_ROWS) {
22095
+ panel.appendChild(switchRow({ label: t()(row2.label), checked: appearance[row2.leaf], disabled: !canApply, onChange: (on) => apply({ [row2.leaf]: on }) }));
22096
+ }
22097
+ };
22098
+ const choice = (label2, checked, disabled, onSelect) => {
22099
+ const row2 = h("button", {
22100
+ type: "button",
22101
+ class: "qc-chart-settings-choice",
22102
+ role: "radio",
22103
+ "aria-checked": String(checked),
22104
+ disabled
22105
+ }, h("span", { class: "qc-chart-settings-radio", "aria-hidden": "true" }), h("span", { class: "qc-chart-settings-choice-label" }, label2));
22106
+ row2.addEventListener("click", onSelect);
22107
+ return row2;
22108
+ };
22109
+ const buildScalePage = (panel) => {
22110
+ const scale = activeChart(deps).scaleMode();
22111
+ panel.appendChild(menuHeading(t()("settings.sectionScale")));
22112
+ const group = h("div", { class: "qc-chart-settings-choices", role: "radiogroup", "aria-label": t()("settings.sectionScale") });
22113
+ for (const option of SCALE_MODE_OPTIONS) {
22114
+ const id = `chart.scale.${option.id}`;
22115
+ group.appendChild(choice(commandLabel(deps, id), option.id === scale, option.id !== scale && !deps.commands.available(id), () => {
22116
+ deps.commands.execute(id);
22117
+ refresh();
22118
+ }));
22119
+ }
22120
+ panel.appendChild(group);
22121
+ };
22122
+ const buildThemePage = (panel) => {
22123
+ const mode = deps.widget.theme.mode();
22124
+ panel.appendChild(menuHeading(t()("settings.sectionTheme")));
22125
+ const group = h("div", { class: "qc-chart-settings-choices", role: "radiogroup", "aria-label": t()("settings.sectionTheme") });
22126
+ for (const themeMode of THEME_MODES) {
22127
+ const id = `widget.theme.${themeMode}`;
22128
+ group.appendChild(choice(commandLabel(deps, id), themeMode === mode, themeMode !== mode && !deps.commands.available(id), () => {
22129
+ deps.commands.execute(id);
22130
+ refresh();
22131
+ }));
22132
+ }
22133
+ panel.appendChild(group);
22134
+ };
22135
+ const buildContent = (body) => {
22136
+ const pages = pageDefinitions();
22137
+ if (!pages.some((page) => page.id === activePage)) activePage = "appearance";
22138
+ const panelId = `qc-chart-settings-panel-${settingsId}`;
22139
+ const selectedTabId = `qc-chart-settings-tab-${settingsId}-${activePage}`;
22140
+ const nav = h("div", { class: "qc-chart-settings-nav", role: "tablist", "aria-orientation": "vertical", "aria-label": t()("settings.menu") });
22141
+ const panel = h("section", { id: panelId, class: "qc-chart-settings-panel", role: "tabpanel", "aria-labelledby": selectedTabId, tabindex: "0" });
22142
+ pages.forEach((page, index) => {
22143
+ const selected = page.id === activePage;
22144
+ const tab = h("button", {
22145
+ id: `qc-chart-settings-tab-${settingsId}-${page.id}`,
22146
+ type: "button",
22147
+ class: "qc-chart-settings-nav-item",
22148
+ role: "tab",
22149
+ "aria-selected": String(selected),
22150
+ "aria-controls": panelId,
22151
+ tabindex: selected ? "0" : "-1",
22152
+ "data-settings-page": page.id
22153
+ }, deps.icons.glyph(page.icon, { size: 20 }), h("span", {}, page.label));
22154
+ tab.addEventListener("click", () => showPage(page.id, true));
22155
+ tab.addEventListener("keydown", (event) => {
22156
+ let next = index;
22157
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") next = (index + 1) % pages.length;
22158
+ else if (event.key === "ArrowUp" || event.key === "ArrowLeft") next = (index - 1 + pages.length) % pages.length;
22159
+ else if (event.key === "Home") next = 0;
22160
+ else if (event.key === "End") next = pages.length - 1;
22161
+ else return;
22162
+ event.preventDefault();
22163
+ showPage(pages[next].id, true);
22164
+ });
22165
+ nav.appendChild(tab);
22166
+ });
22167
+ if (activePage === "appearance") buildAppearancePage(panel);
22168
+ else if (activePage === "display") buildDisplayPage(panel);
22169
+ else if (activePage === "scale") buildScalePage(panel);
22170
+ else buildThemePage(panel);
22171
+ body.append(nav, panel);
22172
+ };
22173
+ const restoreInitialState = () => {
22174
+ if (!initial) return;
22175
+ const chart = activeChart(deps);
22176
+ deps.commands.execute("chart.appearance.apply", { appearance: initial.appearance });
22177
+ if (chart.scaleMode() !== initial.scale) deps.commands.execute(`chart.scale.${initial.scale}`);
22178
+ if (deps.ui.settingsTheme && deps.widget.theme.mode() !== initial.theme) deps.commands.execute(`widget.theme.${initial.theme}`);
22179
+ };
21946
22180
  const open2 = () => {
21947
- menu2 = openMenu({
22181
+ const chart = activeChart(deps);
22182
+ activePage = "appearance";
22183
+ committed = false;
22184
+ initial = {
22185
+ appearance: { ...chart.appearance().appearance },
22186
+ scale: chart.scaleMode(),
22187
+ theme: deps.widget.theme.mode()
22188
+ };
22189
+ dialog = openDialog({
21948
22190
  host: deps.overlays,
21949
- anchor: trigger,
21950
22191
  label: t()("settings.menu"),
21951
- role: "dialog",
21952
- className: "qc-settings-menu",
21953
- width: 280,
21954
- align: "end",
21955
- build(body, handle) {
21956
- retireControls();
21957
- const chart = activeChart(deps);
21958
- const appearance = chart.appearance().appearance;
21959
- const canApply = deps.commands.available("chart.appearance.apply");
21960
- body.appendChild(menuHeading(t()("settings.sectionAppearance")));
21961
- for (const row2 of COLOR_ROWS) {
21962
- const label2 = t()(row2.label);
21963
- const value = appearance[row2.leaf];
21964
- const control = createColorControl(t(), {
21965
- label: label2,
21966
- value,
21967
- opacity: readColor(value)?.alpha ?? 1,
21968
- disabled: !canApply,
21969
- closeOnPick: true,
21970
- onPick: (color2) => {
21971
- control.update(color2);
21972
- apply({ [row2.leaf]: color2 });
21973
- },
21974
- openPanel: (anchor, content, onClosed) => openInlinePanel(anchor, content, anchor.closest(".qc-settings-row") ?? anchor, () => {
21975
- onClosed();
21976
- handle.reposition();
21977
- })
21978
- });
21979
- controls.push(control);
21980
- body.appendChild(fieldRow(label2, control.element, { className: "qc-settings-row" }));
21981
- }
21982
- body.appendChild(menuSeparator());
21983
- body.appendChild(menuHeading(t()("settings.sectionDisplay")));
21984
- for (const row2 of TOGGLE_ROWS) {
21985
- body.appendChild(switchRow({ label: t()(row2.label), checked: appearance[row2.leaf], disabled: !canApply, onChange: (on) => apply({ [row2.leaf]: on }) }));
21986
- }
21987
- body.appendChild(menuSeparator());
21988
- body.appendChild(menuHeading(t()("settings.sectionScale")));
21989
- const scale = chart.scaleMode();
21990
- for (const option of SCALE_MODE_OPTIONS) {
21991
- const id = `chart.scale.${option.id}`;
21992
- body.appendChild(
21993
- menuItem({
21994
- text: commandLabel(deps, id),
21995
- role: "menuitemradio",
21996
- checked: option.id === scale,
21997
- disabled: option.id !== scale && !deps.commands.available(id),
21998
- onSelect: () => {
21999
- deps.commands.execute(id);
22000
- handle.refresh();
22001
- }
22002
- })
22003
- );
22004
- }
22005
- body.appendChild(menuSeparator());
22006
- body.appendChild(
22007
- menuItem({
22008
- text: commandLabel(deps, "chart.appearance.reset"),
22009
- disabled: !deps.commands.available("chart.appearance.reset"),
22010
- onSelect: () => {
22011
- deps.commands.execute("chart.appearance.reset");
22012
- handle.refresh();
22192
+ className: "qc-settings-menu qc-chart-settings-dialog",
22193
+ scrimClassName: "qc-chart-settings-scrim",
22194
+ exitMs: 180,
22195
+ width: 750,
22196
+ build(box, handle) {
22197
+ content = h("div", { class: "qc-dialog-body qc-chart-settings-body" });
22198
+ const footer = h("div", { class: "qc-chart-settings-footer" });
22199
+ const reset = button2({
22200
+ label: commandLabel(deps, "chart.appearance.reset"),
22201
+ text: commandLabel(deps, "chart.appearance.reset"),
22202
+ className: "qc-chart-settings-reset",
22203
+ disabled: !deps.commands.available("chart.appearance.reset"),
22204
+ onClick: () => {
22205
+ deps.commands.execute("chart.appearance.reset");
22206
+ refresh();
22207
+ }
22208
+ });
22209
+ const actions2 = h("div", { class: "qc-chart-settings-actions" });
22210
+ actions2.append(
22211
+ button2({ label: t()("drawing.cancel"), text: t()("drawing.cancel"), className: "qc-chart-settings-cancel", onClick: () => handle.close() }),
22212
+ button2({
22213
+ label: t()("drawing.ok"),
22214
+ text: t()("drawing.ok"),
22215
+ className: "qc-button--primary qc-chart-settings-ok",
22216
+ onClick: () => {
22217
+ committed = true;
22218
+ handle.close();
22013
22219
  }
22014
22220
  })
22015
22221
  );
22016
- if (deps.ui.settingsTheme) {
22017
- body.appendChild(menuSeparator());
22018
- body.appendChild(menuHeading(t()("settings.sectionTheme")));
22019
- const mode = deps.widget.theme.mode();
22020
- for (const themeMode of THEME_MODES) {
22021
- const id = `widget.theme.${themeMode}`;
22022
- body.appendChild(
22023
- menuItem({
22024
- text: commandLabel(deps, id),
22025
- role: "menuitemradio",
22026
- checked: themeMode === mode,
22027
- disabled: themeMode !== mode && !deps.commands.available(id),
22028
- onSelect: () => {
22029
- deps.commands.execute(id);
22030
- handle.refresh();
22031
- }
22032
- })
22033
- );
22034
- }
22035
- }
22222
+ footer.append(reset, actions2);
22223
+ box.append(dialogTitle(t()("drawing.settings"), t()("layouts.close"), () => handle.close(), deps.icons), content, footer);
22224
+ buildContent(content);
22225
+ },
22226
+ initialFocus: (box) => box.querySelector('[role="tab"][aria-selected="true"]'),
22227
+ onClosing: () => {
22228
+ trigger.setAttribute("aria-expanded", "false");
22229
+ if (!committed) restoreInitialState();
22036
22230
  },
22037
22231
  onClose: () => {
22038
22232
  retireControls();
22039
- menu2 = null;
22233
+ content = null;
22234
+ dialog = null;
22235
+ initial = null;
22236
+ trigger.setAttribute("aria-expanded", "false");
22040
22237
  }
22041
22238
  });
22239
+ trigger.setAttribute("aria-expanded", "true");
22042
22240
  };
22043
22241
  const sync = () => {
22044
22242
  name(trigger, t()("settings.menu"));
22045
- menu2?.refresh();
22243
+ refresh();
22046
22244
  };
22047
22245
  sync();
22048
22246
  return {
22049
22247
  element: trigger,
22050
22248
  sync,
22051
22249
  destroy() {
22052
- menu2?.close();
22250
+ dialog?.close({ animate: false });
22053
22251
  trigger.remove();
22054
22252
  }
22055
22253
  };
@@ -23034,14 +23232,54 @@ function mountReplayTransport(deps) {
23034
23232
  };
23035
23233
  const offStrings = i18n.onChange(sync);
23036
23234
  sync();
23235
+ let destroyed = false;
23037
23236
  return {
23038
23237
  element: bar,
23238
+ enter() {
23239
+ const view = bar.ownerDocument.defaultView;
23240
+ const reduceMotion = typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
23241
+ if (destroyed || !bar.isConnected) return;
23242
+ if (reduceMotion) {
23243
+ bar.dataset.state = "open";
23244
+ return;
23245
+ }
23246
+ bar.dataset.state = "opening";
23247
+ bar.getBoundingClientRect();
23248
+ bar.dataset.state = "open";
23249
+ },
23039
23250
  sync,
23040
- destroy() {
23251
+ destroy({ animate = false } = {}) {
23252
+ if (destroyed) return;
23253
+ destroyed = true;
23041
23254
  releasePicking();
23042
23255
  for (const handle2 of [...open2]) handle2.close();
23043
23256
  offStrings();
23044
- bar.remove();
23257
+ const view = bar.ownerDocument.defaultView;
23258
+ const reduceMotion = typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
23259
+ if (!animate || reduceMotion || !bar.isConnected || !view) {
23260
+ bar.remove();
23261
+ return;
23262
+ }
23263
+ bar.style.pointerEvents = "none";
23264
+ bar.inert = true;
23265
+ bar.removeAttribute("role");
23266
+ bar.setAttribute("aria-hidden", "true");
23267
+ let timer = null;
23268
+ const remove = () => {
23269
+ if (timer !== null) clearTimeout(timer);
23270
+ bar.remove();
23271
+ };
23272
+ const onTransitionEnd = (event) => {
23273
+ if (event.target === bar) remove();
23274
+ };
23275
+ bar.dataset.state = "closing";
23276
+ const transitionDuration = view.getComputedStyle(bar).transitionDuration;
23277
+ if (!transitionDuration.split(",").some((value) => Number.parseFloat(value) > 0)) {
23278
+ remove();
23279
+ return;
23280
+ }
23281
+ bar.addEventListener("transitionend", onTransitionEnd, { once: true });
23282
+ timer = setTimeout(remove, 240);
23045
23283
  }
23046
23284
  };
23047
23285
  }
@@ -23065,12 +23303,12 @@ function mountChrome(deps) {
23065
23303
  };
23066
23304
  prefetchSearch();
23067
23305
  disposers.push(() => {
23068
- searchDialog?.close();
23306
+ searchDialog?.close({ animate: false });
23069
23307
  searchOwner.dispose();
23070
23308
  deps.doors.openSearch = () => void 0;
23071
23309
  });
23072
23310
  disposers.push(i18n.onChange(() => {
23073
- searchDialog?.close();
23311
+ searchDialog?.close({ animate: false });
23074
23312
  searchOwner.dispose();
23075
23313
  searchOwner = createSearchSessionOwner(deps.datafeed, { pageSize: 50 });
23076
23314
  prefetchSearch();
@@ -23086,8 +23324,8 @@ function mountChrome(deps) {
23086
23324
  let replayOwner = null;
23087
23325
  let replayBar = null;
23088
23326
  const replayWasUp = /* @__PURE__ */ new WeakMap();
23089
- const releaseReplayRow = () => {
23090
- replayBar?.destroy();
23327
+ const releaseReplayRow = (animate = true) => {
23328
+ replayBar?.destroy({ animate });
23091
23329
  replayBar = null;
23092
23330
  replayOwner = null;
23093
23331
  };
@@ -23104,18 +23342,19 @@ function mountChrome(deps) {
23104
23342
  replayOwner = target;
23105
23343
  replayBar = mountReplayTransport({ chrome: overlays, i18n, icons: deps.icons, commands: target.commands, handle: target.chart, bars: target.bars, intraday: target.intraday });
23106
23344
  deps.panes.after(replayBar.element);
23345
+ replayBar.enter();
23107
23346
  }
23108
23347
  replayBar?.sync();
23109
23348
  };
23110
23349
  disposers.push(() => {
23111
23350
  deps.doors.replayChanged = () => void 0;
23112
- releaseReplayRow();
23351
+ releaseReplayRow(false);
23113
23352
  });
23114
23353
  deps.doors.notify = notify;
23115
23354
  let indicatorDialog = null;
23116
- deps.doors.showIndicatorPicker = () => {
23355
+ deps.doors.showIndicatorPicker = (initialCollection) => {
23117
23356
  if (!ui.indicatorPicker || indicatorDialog?.open()) return;
23118
- indicatorDialog = openIndicatorPicker({ ...ctx, access: deps.access, storage: deps.storage, indicatorPicker: deps.indicatorPicker });
23357
+ indicatorDialog = openIndicatorPicker({ ...ctx, access: deps.access, storage: deps.storage, indicatorPicker: deps.indicatorPicker, initialCollection });
23119
23358
  };
23120
23359
  disposers.push(() => {
23121
23360
  indicatorDialog?.close();
@@ -23125,7 +23364,7 @@ function mountChrome(deps) {
23125
23364
  if (disposed) return;
23126
23365
  if (request.mode !== "compare" && !ui.symbolSearch && !request.onPick) return;
23127
23366
  if (request.mode === "compare" && !features.compare) return;
23128
- searchDialog?.close();
23367
+ searchDialog?.close({ animate: false });
23129
23368
  searchDialog = openSearchDialog({
23130
23369
  host: overlays,
23131
23370
  i18n,
@@ -23138,6 +23377,7 @@ function mountChrome(deps) {
23138
23377
  recents: widget.recents,
23139
23378
  classes: () => deps.feedConfig()?.classes ?? null,
23140
23379
  classNames: deps.classNames,
23380
+ scope: deps.scope,
23141
23381
  painters: deps.painters,
23142
23382
  curated: request.chart?.compare.symbols() ?? [],
23143
23383
  request
@@ -23727,6 +23967,9 @@ function createChart(options) {
23727
23967
  // Read on demand, because the chrome mounts below this object: a host asks for the slot when it
23728
23968
  // has a door to put there, which is always after the widget exists.
23729
23969
  chrome: {
23970
+ legendRows: (chartId, rows2) => {
23971
+ if (!disposed) instances.get(chartId)?.setLegendRows(rows2);
23972
+ },
23730
23973
  topBar: (slot) => chrome2.topBarSlot(slot),
23731
23974
  toolbarButton: (options2) => chrome2.toolbarButton(options2),
23732
23975
  iconDiagnostics: () => iconDiagnostics.list()
@@ -23783,6 +24026,7 @@ function createChart(options) {
23783
24026
  datafeed: options.datafeed,
23784
24027
  feedConfig: () => feedConfig,
23785
24028
  classNames: options.search?.classNames,
24029
+ scope: options.search?.scope,
23786
24030
  painters,
23787
24031
  indicatorPicker: options.indicatorPicker,
23788
24032
  access: options.access,
@@ -23845,7 +24089,6 @@ function openSymbolSearch(options) {
23845
24089
  return {
23846
24090
  close() {
23847
24091
  dialog.close();
23848
- teardown();
23849
24092
  }
23850
24093
  };
23851
24094
  }