@trdrs/quickcharts 5.3.0 → 5.5.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
@@ -53,6 +53,7 @@ import {
53
53
  chooseMagnetStrength,
54
54
  clampFavoritesPosition,
55
55
  commitText,
56
+ contrastOf,
56
57
  createColorControl,
57
58
  createColorPalette,
58
59
  cursorButtonArmed,
@@ -114,7 +115,7 @@ import {
114
115
  withRow,
115
116
  withoutColumn,
116
117
  withoutRow
117
- } from "./chunk-FRZFOTMO.js";
118
+ } from "./chunk-ETG46YGK.js";
118
119
  import {
119
120
  ResourceAbortError,
120
121
  memorySaveLoadAdapter
@@ -2309,6 +2310,7 @@ function cssVarName(roleId) {
2309
2310
  function themeDeclarations(theme) {
2310
2311
  return Object.keys(theme).sort().map((roleId) => ({ property: cssVarName(roleId), value: String(theme[roleId]) }));
2311
2312
  }
2313
+ var CANVAS_INK_ATTRIBUTE = "data-qc-canvas";
2312
2314
  var STYLE_LAYERS = { tokens: "quickcharts.tokens", chart: "quickcharts.chart" };
2313
2315
  var LAYER_ORDER_STATEMENT = `@layer ${STYLE_LAYERS.tokens}, ${STYLE_LAYERS.chart};`;
2314
2316
  var HOST_LAYER_ORDER = `@layer reset, ${STYLE_LAYERS.tokens}, ${STYLE_LAYERS.chart}, host;`;
@@ -5230,6 +5232,19 @@ function withoutModeLeaves(partial) {
5230
5232
  }
5231
5233
  return out;
5232
5234
  }
5235
+ var CANVAS_GROUND = /* @__PURE__ */ new Set(["canvas.background", "canvas.backgroundBottom"]);
5236
+ function chartSettingsDefaultsOn(theme, ink) {
5237
+ const floor = chartSettingsDefaults(theme);
5238
+ if (!ink) return floor;
5239
+ const inked = chartSettingsDefaults(ink === "light" ? LIGHT_THEME : DARK_THEME);
5240
+ const out = floor;
5241
+ for (const path of MODE_LEAVES) {
5242
+ if (CANVAS_GROUND.has(path)) continue;
5243
+ const [section, leaf] = path.split(".");
5244
+ out[section][leaf] = inked[section][leaf];
5245
+ }
5246
+ return floor;
5247
+ }
5233
5248
  function settingsFromAppearance(appearance, isColor) {
5234
5249
  if (!appearance || typeof appearance !== "object" || Array.isArray(appearance)) return { settings: {}, rejected: [] };
5235
5250
  const old = appearance;
@@ -5310,6 +5325,18 @@ function coerceScaleMode(raw) {
5310
5325
  return SCALE_MODES.includes(raw ?? "") ? raw : "normal";
5311
5326
  }
5312
5327
 
5328
+ // src/theme/canvasInk.ts
5329
+ function canvasTone(background, beneath) {
5330
+ const light = contrastOf(BUILT_IN_THEMES.light["text.onCanvas"], background, beneath);
5331
+ const dark = contrastOf(BUILT_IN_THEMES.dark["text.onCanvas"], background, beneath);
5332
+ if (light === null || dark === null) return null;
5333
+ return light >= dark ? "light" : "dark";
5334
+ }
5335
+ function canvasInk(background, mode) {
5336
+ const tone = canvasTone(background, BUILT_IN_THEMES[mode]["canvas.background"]);
5337
+ return tone !== null && tone !== mode ? tone : null;
5338
+ }
5339
+
5313
5340
  // src/widget/chartLook.ts
5314
5341
  import { ColorType, LineStyle as LineStyle2 } from "lightweight-charts";
5315
5342
  function chartLookOptions(settings2, canvas, ui) {
@@ -8272,15 +8299,15 @@ var drawable = (shape) => {
8272
8299
  if (rule !== void 0 && rule !== "nonzero" && rule !== "evenodd") return false;
8273
8300
  return width === void 0 || Number.isFinite(width) && width > 0 && width <= GLYPH_LIMITS.strokeWidth;
8274
8301
  };
8275
- function buildGlyph(icon) {
8302
+ function buildGlyph(icon, size = GRID) {
8276
8303
  try {
8277
8304
  const paths = icon && Array.isArray(icon.paths) ? icon.paths : [];
8278
8305
  if (paths.length === 0 || paths.length > GLYPH_LIMITS.shapes) return null;
8279
8306
  const shapes = paths.filter(drawable);
8280
8307
  if (shapes.length === 0) return null;
8281
8308
  const root = document.createElementNS(SVG_NS, "svg");
8282
- set(root, GLYPH_ATTRIBUTES.svg, "width", String(GRID));
8283
- set(root, GLYPH_ATTRIBUTES.svg, "height", String(GRID));
8309
+ set(root, GLYPH_ATTRIBUTES.svg, "width", String(size));
8310
+ set(root, GLYPH_ATTRIBUTES.svg, "height", String(size));
8284
8311
  set(root, GLYPH_ATTRIBUTES.svg, "viewBox", `0 0 ${GRID} ${GRID}`);
8285
8312
  set(root, GLYPH_ATTRIBUTES.svg, "fill", "none");
8286
8313
  set(root, GLYPH_ATTRIBUTES.svg, "aria-hidden", "true");
@@ -20408,7 +20435,7 @@ function mountNavControls(deps) {
20408
20435
  }
20409
20436
 
20410
20437
  // src/ui/chrome/scaleControls.ts
20411
- var BUTTON = { width: 20, height: 22, gap: 4, foot: 4 };
20438
+ var BUTTON = { width: 20, height: 22, gap: 15, foot: 4 };
20412
20439
  var UNIT_BOX = { inset: 4 };
20413
20440
  function mountScaleControls(deps) {
20414
20441
  const t = deps.i18n.t;
@@ -20417,7 +20444,15 @@ function mountScaleControls(deps) {
20417
20444
  const log = h("button", { type: "button", class: "qc-scale-mode", "data-role": "scale-log" });
20418
20445
  modes.append(auto, log);
20419
20446
  stopPointer(modes);
20420
- const unit = h("div", { class: "qc-scale-unit", "data-role": "scale-unit" });
20447
+ const door = deps.openUnitMenu;
20448
+ const unit = door ? h("button", { type: "button", class: "qc-scale-unit", "data-role": "scale-unit" }) : h("div", { class: "qc-scale-unit", "data-role": "scale-unit" });
20449
+ if (door) {
20450
+ stopPointer(unit);
20451
+ unit.addEventListener("click", (event) => {
20452
+ const box2 = unit.getBoundingClientRect();
20453
+ door({ left: box2.left, top: box2.top, right: box2.right, bottom: box2.bottom }, event.detail === 0);
20454
+ });
20455
+ }
20421
20456
  deps.chrome.appendChild(unit);
20422
20457
  const plus = h("button", { type: "button", class: "qc-scale-plus", "data-role": "scale-plus" });
20423
20458
  plus.hidden = true;
@@ -20509,6 +20544,7 @@ function mountScaleControls(deps) {
20509
20544
  log.textContent = t("chrome.logScaleMark");
20510
20545
  name(log, t("chrome.logScale"));
20511
20546
  name(plus, t("chrome.priceLevelMenu"));
20547
+ if (door) unit.title = t("settings.currencyAndUnit");
20512
20548
  };
20513
20549
  const sync = () => {
20514
20550
  const box2 = deps.box();
@@ -20576,6 +20612,10 @@ function mountScaleControls(deps) {
20576
20612
  deps.gestures.addEventListener("pointerleave", onLeave);
20577
20613
  modes.addEventListener("pointerenter", onControlsEnter);
20578
20614
  modes.addEventListener("pointerleave", onControlsLeave);
20615
+ if (door) {
20616
+ unit.addEventListener("pointerenter", onControlsEnter);
20617
+ unit.addEventListener("pointerleave", onControlsLeave);
20618
+ }
20579
20619
  const offStrings = deps.i18n.onChange(label3);
20580
20620
  label3();
20581
20621
  sync();
@@ -21032,7 +21072,13 @@ function registerChartCommands(deps) {
21032
21072
  scope: "chart",
21033
21073
  label: "chrome.indicators",
21034
21074
  available: () => ui.indicatorPicker,
21035
- execute: (arg) => deps.indicatorsOpen(arg && typeof arg === "object" && "collection" in arg && typeof arg.collection === "string" ? arg.collection : void 0)
21075
+ execute: (arg) => {
21076
+ const field = (key) => {
21077
+ const value = arg && typeof arg === "object" ? arg[key] : void 0;
21078
+ return typeof value === "string" ? value : void 0;
21079
+ };
21080
+ return deps.indicatorsOpen(field("collection"), field("kind"));
21081
+ }
21036
21082
  });
21037
21083
  const isInstance = (arg) => !!arg && typeof arg === "object" && typeof arg.id === "string" && typeof arg.definition === "object";
21038
21084
  add({
@@ -22018,7 +22064,12 @@ function createChartInstance(deps) {
22018
22064
  if (named2.symbol?.session !== void 0 || stored !== "regular" && stored !== "extended") return named2;
22019
22065
  return mergePartialChartSettings({ symbol: { session: stored } }, named2);
22020
22066
  })();
22021
- const resolveSettings = () => layerChartSettings(chartSettingsDefaults(deps.theme.get()), hostPartial, viewerPartial);
22067
+ const resolveSettings = () => {
22068
+ const theme = deps.theme.get();
22069
+ const laid = layerChartSettings(chartSettingsDefaults(theme), hostPartial, viewerPartial);
22070
+ const ink = canvasInk(laid.canvas.background, deps.theme.mode());
22071
+ return ink ? layerChartSettings(chartSettingsDefaultsOn(theme, ink), hostPartial, viewerPartial) : laid;
22072
+ };
22022
22073
  let eff = resolveSettings();
22023
22074
  const scaleSideOf = (settings2) => settings2.priceScale.placement === "left" ? "left" : "right";
22024
22075
  let scaleSide = scaleSideOf(eff);
@@ -22038,6 +22089,13 @@ function createChartInstance(deps) {
22038
22089
  chrome2.dataset.qcDrawingToolbar = "true";
22039
22090
  }
22040
22091
  deps.container.append(gestures, chrome2);
22092
+ const paintChromeCanvas = () => {
22093
+ chrome2.style.setProperty("--qcd-chart-background", eff.canvas.background);
22094
+ const ink = canvasInk(eff.canvas.background, deps.theme.mode());
22095
+ if (ink) chrome2.setAttribute(CANVAS_INK_ATTRIBUTE, ink);
22096
+ else chrome2.removeAttribute(CANVAS_INK_ATTRIBUTE);
22097
+ };
22098
+ paintChromeCanvas();
22041
22099
  const lookOptions = () => chartLookOptions(eff, canvas(), {
22042
22100
  crosshairLabels: deps.ui.crosshairLabels,
22043
22101
  crosshairHorizontal: deps.ui.crosshairHorizontal,
@@ -22723,7 +22781,8 @@ function createChartInstance(deps) {
22723
22781
  },
22724
22782
  openPriceMenu: (anchor2, keyboard) => {
22725
22783
  menu2?.raisePriceMenu(anchor2, keyboard);
22726
- }
22784
+ },
22785
+ ...deps.scaleUnitMenu ? { openUnitMenu: (anchor2, keyboard) => deps.scaleUnitMenu?.({ chart: handle, anchor: anchor2, keyboard }) } : {}
22727
22786
  }) : null;
22728
22787
  scalePlus = scaleControls && menu2 ? attachScalePlus({
22729
22788
  chart,
@@ -22956,7 +23015,7 @@ function createChartInstance(deps) {
22956
23015
  marks?.repaint();
22957
23016
  watermark.refresh();
22958
23017
  nav?.sync();
22959
- chrome2.style.setProperty("--qcd-chart-background", eff.canvas.background);
23018
+ paintChromeCanvas();
22960
23019
  scaleControls?.sync();
22961
23020
  scalePlus?.refresh();
22962
23021
  paneButtons?.sync();
@@ -23768,7 +23827,7 @@ function createChartInstance(deps) {
23768
23827
  crosshairLevel,
23769
23828
  formatter: () => symbolFormatter,
23770
23829
  compareOpen: (mode, changeFrom) => compare?.openDialog(mode, changeFrom),
23771
- indicatorsOpen: (collection) => deps.doors.showIndicatorPicker(collection),
23830
+ indicatorsOpen: (collection, kind) => deps.doors.showIndicatorPicker(collection, kind),
23772
23831
  symbolSearchOpen: () => deps.doors.openSearch({ mode: "search", chart: handle }),
23773
23832
  drawingVerbs: () => drawings2.verbs
23774
23833
  });
@@ -25543,13 +25602,21 @@ function actions(value) {
25543
25602
  }
25544
25603
  return unique(result.map((a) => a.id)) ? result : null;
25545
25604
  }
25605
+ function emptyState2(value) {
25606
+ if (!record2(value) || !text(value.title) || !optionalText(value.text)) return null;
25607
+ const action = value.action === void 0 ? [] : actions([value.action]);
25608
+ if (!action) return null;
25609
+ return { title: value.title, ...value.text !== void 0 ? { text: value.text } : {}, ...value.icon !== void 0 ? { icon: value.icon } : {}, ...action[0] ? { action: action[0] } : {} };
25610
+ }
25546
25611
  function pickerCollections(value) {
25547
25612
  try {
25548
25613
  if (!Array.isArray(value)) return null;
25549
25614
  const result = [];
25550
25615
  for (const row2 of value) {
25551
25616
  if (!record2(row2) || !text(row2.id) || ["builtin", "favorites"].includes(row2.id) || !text(row2.label) || !optionalText(row2.group) || row2.layout !== void 0 && row2.layout !== "list" && row2.layout !== "table") return null;
25552
- result.push({ id: row2.id, label: row2.label, ...row2.icon !== void 0 ? { icon: row2.icon } : {}, ...row2.group !== void 0 ? { group: row2.group } : {}, ...row2.layout !== void 0 ? { layout: row2.layout } : {} });
25617
+ const empty = row2.empty === void 0 ? void 0 : emptyState2(row2.empty);
25618
+ if (empty === null) return null;
25619
+ result.push({ id: row2.id, label: row2.label, ...row2.icon !== void 0 ? { icon: row2.icon } : {}, ...row2.group !== void 0 ? { group: row2.group } : {}, ...row2.layout !== void 0 ? { layout: row2.layout } : {}, ...empty ? { empty } : {} });
25553
25620
  }
25554
25621
  return unique(result.map((row2) => row2.id)) ? result : null;
25555
25622
  } catch {
@@ -25650,8 +25717,10 @@ function openIndicatorPicker(deps) {
25650
25717
  } catch {
25651
25718
  }
25652
25719
  let query = "";
25653
- let collection = deps.initialCollection === "favorites" || collections?.some((row2) => row2.id === deps.initialCollection) ? deps.initialCollection : "builtin";
25654
- let kind = null;
25720
+ let hostStarred = false;
25721
+ const starred = () => favorites.length > 0 || hostStarred;
25722
+ let collection = deps.initialCollection === "favorites" && starred() || collections?.some((row2) => row2.id === deps.initialCollection) ? deps.initialCollection : "builtin";
25723
+ let kind = deps.initialKind ?? null;
25655
25724
  let generation = 0;
25656
25725
  let controller = null;
25657
25726
  let busy = false;
@@ -25691,6 +25760,9 @@ function openIndicatorPicker(deps) {
25691
25760
  const strip = h("div", { class: "qc-search-classes" });
25692
25761
  const nav = h("nav", { class: "qc-picker-nav", "aria-label": t("picker.collections") });
25693
25762
  const list = h("div", { class: "qc-picker-list", role: "table", "aria-label": t("picker.title") });
25763
+ const vacant = h("div", { class: "qc-picker-empty" });
25764
+ vacant.hidden = true;
25765
+ let railFavorites = false;
25694
25766
  const status2 = h("div", { class: "qc-picker-status qc-secondary", role: "status", "aria-live": "polite" });
25695
25767
  let pendingFocus = null;
25696
25768
  const actionButton = (label3, content, run, disabled = false, focusKey, className = "qc-picker-action") => {
@@ -25790,7 +25862,7 @@ function openIndicatorPicker(deps) {
25790
25862
  else if (focused && focused !== document.body) pendingFocus = null;
25791
25863
  const selected = (collections ?? []).find((row2) => row2.id === collection);
25792
25864
  list.classList.toggle("qc-picker-list--personal", selected?.layout === "list");
25793
- list.classList.toggle("qc-picker-list--catalog", collection === "builtin");
25865
+ if (railFavorites !== (starred() || collection === "favorites")) renderNavigation();
25794
25866
  for (const button3 of nav.querySelectorAll("[data-picker-collection]")) button3.setAttribute("aria-pressed", String(button3.getAttribute("data-picker-collection") === collection));
25795
25867
  for (const chip of strip.querySelectorAll("[data-picker-kind]")) chip.setAttribute("aria-pressed", String((chip.getAttribute("data-picker-kind") || null) === kind));
25796
25868
  const headings = h("div", { role: "row", class: "qc-picker-headings" }, ...["", t("picker.name"), t("picker.author"), t("picker.favorites"), ""].map((label3) => h("span", { role: "columnheader" }, label3)));
@@ -25805,13 +25877,31 @@ function openIndicatorPicker(deps) {
25805
25877
  body.append(buildRow({ id: definition.id, kind: "builtin", title: t(definition.nameKey), description: t(definition.descriptionKey), favorite, count: metadata?.favoriteCount, hostedFavorite: metadata?.favorite !== void 0 || !!source3 && loading, actions: metadata?.actions ?? [], primary: () => add(definition), permitted: indicatorPermitted(deps.access, definition) && deps.commands.available("chart.indicators.add"), builtin: definition }));
25806
25878
  }
25807
25879
  }
25808
- if (collection !== "builtin") for (const item of rows2.items) {
25880
+ for (const item of rows2.items) {
25809
25881
  if (collection === "favorites" && item.favorite !== true) continue;
25810
25882
  if (!admitted(item.kind ?? INDICATOR_KIND)) continue;
25811
25883
  const primary = item.actions.find((action) => action.id === item.primaryAction);
25812
25884
  body.append(buildRow({ id: item.id, kind: "item", title: item.title, description: item.description, author: item.author, favorite: item.favorite, count: item.favoriteCount, hostedFavorite: true, actions: item.actions, primary: () => void act({ kind: "item", id: item.id }, primary.id), permitted: !busy && !loading && !primary.disabled }));
25813
25885
  }
25814
25886
  replace(list, headings, body);
25887
+ const filled = (cell2) => [...body.querySelectorAll(cell2)].some((node) => node.textContent !== "");
25888
+ list.classList.toggle("qc-picker-list--authors", filled(".qc-picker-author"));
25889
+ list.classList.toggle("qc-picker-list--counts", filled(".qc-picker-count"));
25890
+ const empty = selected?.empty && !loading && available && query.trim() === "" && rows2.items.length === 0 ? selected.empty : null;
25891
+ list.hidden = !!empty;
25892
+ vacant.hidden = !empty;
25893
+ strip.hidden = (kinds ?? []).length === 0 || !!empty;
25894
+ if (empty) {
25895
+ const action = empty.action;
25896
+ const mark = buildGlyph(empty.icon, 120);
25897
+ replace(
25898
+ vacant,
25899
+ mark ? h("span", { class: "qc-picker-empty-mark", "aria-hidden": "true" }, mark) : null,
25900
+ h("strong", { class: "qc-picker-empty-title" }, empty.title),
25901
+ empty.text ? h("p", { class: "qc-picker-empty-text" }, empty.text) : null,
25902
+ action ? button2({ label: action.label, text: action.label, className: "qc-button--primary qc-drawing-footer-button qc-picker-empty-action", disabled: busy || action.disabled === true, onClick: () => void act({ kind: "collection", id: collection }, action.id) }) : null
25903
+ );
25904
+ } else replace(vacant);
25815
25905
  if (pendingFocus) {
25816
25906
  const target = [...list.querySelectorAll("[data-picker-focus]")].find((button3) => button3.dataset.pickerFocus === pendingFocus);
25817
25907
  if (target && !target.disabled) target.focus();
@@ -25820,7 +25910,7 @@ function openIndicatorPicker(deps) {
25820
25910
  input.focus();
25821
25911
  }
25822
25912
  }
25823
- status2.textContent = error || (loading ? t("picker.loading") : body.childElementCount === 0 ? t("picker.noMatches") : "");
25913
+ status2.textContent = error || (loading ? t("picker.loading") : body.childElementCount === 0 && !empty ? t("picker.noMatches") : "");
25824
25914
  list.setAttribute("aria-busy", String(loading || busy));
25825
25915
  };
25826
25916
  const refresh = async () => {
@@ -25845,6 +25935,7 @@ function openIndicatorPicker(deps) {
25845
25935
  if (parsed) {
25846
25936
  rows2 = parsed;
25847
25937
  available = true;
25938
+ hostStarred = parsed.builtIns.some((row2) => row2.favorite === true) || parsed.items.some((item) => item.favorite === true);
25848
25939
  } else error = result?.kind === "unavailable" && typeof result.message === "string" && result.message.trim() ? result.message : t("picker.unavailable");
25849
25940
  } catch {
25850
25941
  if (dialog.open() && revision === generation) error = t("picker.unavailable");
@@ -25862,8 +25953,9 @@ function openIndicatorPicker(deps) {
25862
25953
  };
25863
25954
  const renderNavigation = () => {
25864
25955
  replace(nav);
25956
+ railFavorites = starred() || collection === "favorites";
25865
25957
  const personal = t("picker.personal");
25866
- const navigation = [{ id: "favorites", label: t("picker.favorites"), group: personal, icon: deps.icons.glyph(ICONS.pickerFavorites, { size: 28 }) }, ...(collections ?? []).map((row2) => ({ ...row2, icon: hostIcon(row2.icon) })), { id: "builtin", label: t("picker.technicals"), group: t("picker.builtin"), icon: deps.icons.glyph(ICONS.pickerTechnicals, { size: 28 }) }];
25958
+ const navigation = [...railFavorites ? [{ id: "favorites", label: t("picker.favorites"), group: personal, icon: deps.icons.glyph(ICONS.pickerFavorites, { size: 28 }) }] : [], ...(collections ?? []).map((row2) => ({ ...row2, icon: hostIcon(row2.icon) })), { id: "builtin", label: t("picker.technicals"), group: t("picker.builtin"), icon: deps.icons.glyph(ICONS.pickerTechnicals, { size: 28 }) }];
25867
25959
  let section = null;
25868
25960
  let group;
25869
25961
  for (const item of navigation) {
@@ -25905,7 +25997,7 @@ function openIndicatorPicker(deps) {
25905
25997
  input.focus();
25906
25998
  } else if (roveFocus(list, event)) event.preventDefault();
25907
25999
  });
25908
- box2.append(searchField(deps.icons, input, clear), strip, h("div", { class: "qc-picker-body" }, nav, h("div", { class: "qc-picker-content" }, list, status2)));
26000
+ box2.append(searchField(deps.icons, input, clear), strip, h("div", { class: "qc-picker-body" }, nav, h("div", { class: "qc-picker-content" }, list, vacant, status2)));
25909
26001
  renderKinds();
25910
26002
  renderNavigation();
25911
26003
  redraw = render;
@@ -30203,9 +30295,9 @@ function mountChrome(deps) {
30203
30295
  });
30204
30296
  deps.doors.notify = notify;
30205
30297
  let indicatorDialog = null;
30206
- deps.doors.showIndicatorPicker = (initialCollection) => {
30298
+ deps.doors.showIndicatorPicker = (initialCollection, initialKind) => {
30207
30299
  if (!ui.indicatorPicker || indicatorDialog?.open()) return;
30208
- indicatorDialog = openIndicatorPicker({ ...ctx, storage: deps.storage, indicatorPicker: deps.indicatorPicker, initialCollection });
30300
+ indicatorDialog = openIndicatorPicker({ ...ctx, storage: deps.storage, indicatorPicker: deps.indicatorPicker, initialCollection, initialKind });
30209
30301
  };
30210
30302
  disposers.push(() => {
30211
30303
  indicatorDialog?.close({ animate: false });
@@ -30706,7 +30798,8 @@ function createChart(options2) {
30706
30798
  }
30707
30799
  },
30708
30800
  doors,
30709
- painters
30801
+ painters,
30802
+ ...options2.scaleUnitMenu ? { scaleUnitMenu: options2.scaleUnitMenu } : {}
30710
30803
  });
30711
30804
  } catch (error) {
30712
30805
  scope.dispose();