@funnest/charts-ui 0.6.9 → 0.6.11

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.
@@ -4,7 +4,11 @@ import type { WidgetContext } from '../context';
4
4
  export declare function DrawingToolbar({ ctx }: {
5
5
  ctx: WidgetContext;
6
6
  }): JSX.Element;
7
- /** Floating bar for the selected drawing: color + delete (also the only way to delete on touch). */
7
+ /**
8
+ * Floating bar for the selected drawing: move it by its grip (remembered; double-click resets),
9
+ * color (swatches, a custom one, recent customs), line width and style, text for text tools,
10
+ * lock, duplicate, delete (also the only way to delete on touch).
11
+ */
8
12
  export declare function SelectionBar({ ctx }: {
9
13
  ctx: WidgetContext;
10
14
  }): JSX.Element | null;
package/dist/context.d.ts CHANGED
@@ -79,6 +79,8 @@ export interface WidgetContext {
79
79
  /** Show the Funnest logo badge on the chart. */
80
80
  attribution: boolean;
81
81
  contextMenuItems: ContextMenuItems | undefined;
82
+ /** The app's own indicator browser for the 지표 button (instead of the built-in picker). */
83
+ onIndicatorsClick: ((chart: Chart) => void) | undefined;
82
84
  slots: {
83
85
  toolbar: HTMLElement;
84
86
  toolbarEnd: HTMLElement;
package/dist/i18n.d.ts CHANGED
@@ -75,6 +75,18 @@ export declare const STRINGS: {
75
75
  magnet: string;
76
76
  remove: string;
77
77
  color: string;
78
+ customColor: string;
79
+ lineWidth: string;
80
+ lineStyle: string;
81
+ lineSolid: string;
82
+ lineDashed: string;
83
+ lineDotted: string;
84
+ drawingText: string;
85
+ textPlaceholder: string;
86
+ lock: string;
87
+ unlock: string;
88
+ duplicate: string;
89
+ moveBar: string;
78
90
  indicators: string;
79
91
  addIndicator: string;
80
92
  indicatorSettings: string;
@@ -91,7 +103,6 @@ export declare const STRINGS: {
91
103
  removePeriod: string;
92
104
  period: string;
93
105
  source: string;
94
- lineWidth: string;
95
106
  inputs: string;
96
107
  style: string;
97
108
  intervalMinutes: string;
@@ -206,6 +217,18 @@ export declare const STRINGS: {
206
217
  magnet: string;
207
218
  remove: string;
208
219
  color: string;
220
+ customColor: string;
221
+ lineWidth: string;
222
+ lineStyle: string;
223
+ lineSolid: string;
224
+ lineDashed: string;
225
+ lineDotted: string;
226
+ drawingText: string;
227
+ textPlaceholder: string;
228
+ lock: string;
229
+ unlock: string;
230
+ duplicate: string;
231
+ moveBar: string;
209
232
  indicators: string;
210
233
  addIndicator: string;
211
234
  indicatorSettings: string;
@@ -222,7 +245,6 @@ export declare const STRINGS: {
222
245
  removePeriod: string;
223
246
  period: string;
224
247
  source: string;
225
- lineWidth: string;
226
248
  inputs: string;
227
249
  style: string;
228
250
  intervalMinutes: string;
package/dist/icons.d.ts CHANGED
@@ -55,3 +55,9 @@ export declare const SvgIcon: ({ markup }: {
55
55
  export declare const CornerIcon: () => import("preact").JSX.Element;
56
56
  /** Sliders: the 도구 menu. */
57
57
  export declare const ToolsIcon: () => import("preact").JSX.Element;
58
+ export declare const GripIcon: () => import("preact").JSX.Element;
59
+ export declare const LockIcon: ({ open }: {
60
+ open?: boolean;
61
+ }) => import("preact").JSX.Element;
62
+ export declare const CopyIcon: () => import("preact").JSX.Element;
63
+ export declare const TextIcon: () => import("preact").JSX.Element;
package/dist/index.js CHANGED
@@ -498,6 +498,18 @@ var STRINGS = {
498
498
  magnet: "\uC790\uC11D \uBAA8\uB4DC (\uC2DC\xB7\uACE0\xB7\uC800\xB7\uC885\uAC00\uC5D0 \uBD99\uC774\uAE30)",
499
499
  remove: "\uC0AD\uC81C",
500
500
  color: "\uC0C9\uC0C1",
501
+ customColor: "\uB2E4\uB978 \uC0C9",
502
+ lineWidth: "\uC120 \uAD75\uAE30",
503
+ lineStyle: "\uC120 \uBAA8\uC591",
504
+ lineSolid: "\uC2E4\uC120",
505
+ lineDashed: "\uD30C\uC120",
506
+ lineDotted: "\uC810\uC120",
507
+ drawingText: "\uD14D\uC2A4\uD2B8",
508
+ textPlaceholder: "\uD14D\uC2A4\uD2B8 \uC785\uB825",
509
+ lock: "\uC7A0\uADF8\uAE30",
510
+ unlock: "\uC7A0\uAE08 \uD480\uAE30",
511
+ duplicate: "\uBCF5\uC81C",
512
+ moveBar: "\uB04C\uC5B4\uC11C \uC62E\uAE30\uAE30 (\uB450 \uBC88 \uB204\uB974\uBA74 \uC81C\uC790\uB9AC)",
501
513
  indicators: "\uC9C0\uD45C",
502
514
  addIndicator: "\uC9C0\uD45C \uCD94\uAC00",
503
515
  indicatorSettings: "\uC9C0\uD45C \uC124\uC815",
@@ -514,7 +526,6 @@ var STRINGS = {
514
526
  removePeriod: "\uAE30\uAC04 \uC0AD\uC81C",
515
527
  period: "\uAE30\uAC04",
516
528
  source: "\uAC00\uACA9 \uAE30\uC900",
517
- lineWidth: "\uB450\uAED8",
518
529
  inputs: "\uC785\uB825\uAC12",
519
530
  style: "\uC2A4\uD0C0\uC77C",
520
531
  // intervals
@@ -631,6 +642,18 @@ var STRINGS = {
631
642
  magnet: "Magnet (snap to OHLC)",
632
643
  remove: "Remove",
633
644
  color: "Color",
645
+ customColor: "Other color",
646
+ lineWidth: "Line width",
647
+ lineStyle: "Line style",
648
+ lineSolid: "Solid",
649
+ lineDashed: "Dashed",
650
+ lineDotted: "Dotted",
651
+ drawingText: "Text",
652
+ textPlaceholder: "Type text",
653
+ lock: "Lock",
654
+ unlock: "Unlock",
655
+ duplicate: "Duplicate",
656
+ moveBar: "Drag to move (double-click to reset)",
634
657
  indicators: "Indicators",
635
658
  addIndicator: "Add indicator",
636
659
  indicatorSettings: "Indicator settings",
@@ -647,7 +670,6 @@ var STRINGS = {
647
670
  removePeriod: "Remove period",
648
671
  period: "Period",
649
672
  source: "Source",
650
- lineWidth: "Width",
651
673
  inputs: "Inputs",
652
674
  style: "Style",
653
675
  intervalMinutes: "Minutes",
@@ -859,6 +881,15 @@ var ToolsIcon = () => /* @__PURE__ */ a3(Icon, { size: 18, children: [
859
881
  /* @__PURE__ */ a3("circle", { cx: "12.5", cy: "6", r: "2" }),
860
882
  /* @__PURE__ */ a3("circle", { cx: "7.5", cy: "14", r: "2" })
861
883
  ] });
884
+ var GripIcon = () => /* @__PURE__ */ a3(Icon, { children: /* @__PURE__ */ a3("path", { d: "M7.5 5h.01M7.5 10h.01M7.5 15h.01M12.5 5h.01M12.5 10h.01M12.5 15h.01", "stroke-width": "2.4" }) });
885
+ var LockIcon = ({ open = false }) => /* @__PURE__ */ a3(Icon, { children: [
886
+ /* @__PURE__ */ a3("rect", { x: "4.5", y: "9", width: "11", height: "8", rx: "1.8" }),
887
+ /* @__PURE__ */ a3("path", { d: open ? "M7 9V6.5a3 3 0 0 1 5.8-1" : "M7 9V6.5a3 3 0 0 1 6 0V9" })
888
+ ] });
889
+ var CopyIcon = () => /* @__PURE__ */ a3(Icon, { children: [
890
+ /* @__PURE__ */ a3("rect", { x: "7", y: "7", width: "9.5", height: "9.5", rx: "1.8" }),
891
+ /* @__PURE__ */ a3("path", { d: "M13 7V5a1.5 1.5 0 0 0-1.5-1.5h-6A1.5 1.5 0 0 0 4 5v6a1.5 1.5 0 0 0 1.5 1.5H7" })
892
+ ] });
862
893
 
863
894
  // src/store.ts
864
895
  var Store = class {
@@ -943,7 +974,10 @@ function ToolGroup({
943
974
  };
944
975
  return /* @__PURE__ */ a3("div", { class: "fnc-tool-group", ref, "data-open": open, children: [
945
976
  /* @__PURE__ */ a3("button", { class: "fnc-btn", "data-tip": name, "aria-label": name, "aria-pressed": !!armed, onClick: () => pick(current), children: /* @__PURE__ */ a3(ToolIcon, { def: current, locale: s3.locale }) }),
946
- tools.length > 1 && /* @__PURE__ */ a3("button", { class: "fnc-tool-more", "aria-label": `${t3.moreTools}: ${name}`, "aria-haspopup": "menu", "aria-expanded": open, onClick: () => onOpen(!open), children: /* @__PURE__ */ a3(CornerIcon, {}) }),
977
+ tools.length > 1 && /* @__PURE__ */ a3("button", { class: "fnc-tool-more", "aria-label": `${t3.moreTools}: ${name}`, "aria-haspopup": "menu", "aria-expanded": open, onClick: () => onOpen(!open), children: [
978
+ /* @__PURE__ */ a3("span", { class: "fnc-tool-corner", children: /* @__PURE__ */ a3(CornerIcon, {}) }),
979
+ /* @__PURE__ */ a3("span", { class: "fnc-tool-chevron", children: /* @__PURE__ */ a3(ChevronRightIcon, {}) })
980
+ ] }),
947
981
  open && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-tool-menu", role: "menu", children: tools.map((def, i3) => {
948
982
  const section = def.section?.[s3.locale];
949
983
  const heading = section && section !== tools[i3 - 1]?.section?.[s3.locale];
@@ -1030,24 +1064,153 @@ function DrawingToolbar({ ctx }) {
1030
1064
  ] });
1031
1065
  }
1032
1066
  var SWATCHES = ["#3182F6", "#F04452", "#12B886", "#FF9F0A", "#7048E8"];
1067
+ var WIDTHS = [1, 2, 3, 4];
1068
+ var DASH_ORDER = ["solid", "dashed", "dotted"];
1069
+ var RECENT_KEY = "fnc-drawing-colors";
1070
+ var POS_KEY = "fnc-selbar-pos";
1071
+ var readJson = (key, fallback) => {
1072
+ try {
1073
+ const raw = localStorage.getItem(key);
1074
+ return raw ? JSON.parse(raw) : fallback;
1075
+ } catch {
1076
+ return fallback;
1077
+ }
1078
+ };
1079
+ var writeJson = (key, value) => {
1080
+ try {
1081
+ localStorage.setItem(key, JSON.stringify(value));
1082
+ } catch {
1083
+ }
1084
+ };
1085
+ function DashGlyph({ dash }) {
1086
+ const pattern = dash === "dashed" ? "4 3" : dash === "dotted" ? "1 3" : void 0;
1087
+ return /* @__PURE__ */ a3("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "aria-hidden": "true", children: /* @__PURE__ */ a3("path", { d: "M3 10h14", "stroke-dasharray": pattern }) });
1088
+ }
1033
1089
  function SelectionBar({ ctx }) {
1034
1090
  const s3 = useStore(ctx.store);
1035
1091
  const t3 = STRINGS[s3.locale];
1036
1092
  const d3 = s3.selection;
1093
+ const [offset, setOffset] = d2(() => readJson(POS_KEY, [0, 0]));
1094
+ const [recent, setRecent] = d2(() => readJson(RECENT_KEY, []));
1095
+ const [menu, setMenu] = d2(null);
1096
+ const colorInput = T2(null);
1037
1097
  if (!d3 || s3.layout.plotWidth <= 0) return null;
1038
- return /* @__PURE__ */ a3("div", { class: "fnc-selbar", role: "toolbar", style: { left: `${s3.layout.plotWidth / 2}px` }, children: [
1039
- SWATCHES.map((color) => /* @__PURE__ */ a3(
1098
+ const def = ctx.chart.drawingDefinitions().find((x2) => x2.type === d3.type);
1099
+ const style = d3.style ?? {};
1100
+ const setStyle = (patch) => ctx.chart.updateDrawing(d3.id, { style: { ...style, ...patch } });
1101
+ const current = d3.color ?? SWATCHES[0];
1102
+ const pickColor = (color) => ctx.chart.updateDrawing(d3.id, { color });
1103
+ const addRecent = (color) => {
1104
+ const next = [color, ...recent.filter((c3) => c3 !== color)].slice(0, 3);
1105
+ setRecent(next);
1106
+ writeJson(RECENT_KEY, next);
1107
+ };
1108
+ const locked = !!style.locked;
1109
+ const dash = style.dash ?? "solid";
1110
+ const startMove = (e3) => {
1111
+ e3.preventDefault();
1112
+ const start = [e3.clientX, e3.clientY];
1113
+ const from = offset;
1114
+ const move = (ev) => setOffset([from[0] + ev.clientX - start[0], from[1] + ev.clientY - start[1]]);
1115
+ const up = (ev) => {
1116
+ document.removeEventListener("pointermove", move);
1117
+ document.removeEventListener("pointerup", up);
1118
+ const next = [from[0] + ev.clientX - start[0], from[1] + ev.clientY - start[1]];
1119
+ setOffset(next);
1120
+ writeJson(POS_KEY, next);
1121
+ };
1122
+ document.addEventListener("pointermove", move);
1123
+ document.addEventListener("pointerup", up);
1124
+ };
1125
+ return /* @__PURE__ */ a3("div", { class: "fnc-selbar", role: "toolbar", style: { left: `${s3.layout.plotWidth / 2 + offset[0]}px`, top: `${8 + offset[1]}px` }, children: [
1126
+ /* @__PURE__ */ a3(
1127
+ "button",
1128
+ {
1129
+ class: "fnc-btn fnc-selbar-grip",
1130
+ "data-tip": t3.moveBar,
1131
+ "aria-label": t3.moveBar,
1132
+ onPointerDown: startMove,
1133
+ onDblClick: () => {
1134
+ setOffset([0, 0]);
1135
+ writeJson(POS_KEY, [0, 0]);
1136
+ },
1137
+ children: /* @__PURE__ */ a3(GripIcon, {})
1138
+ }
1139
+ ),
1140
+ [...SWATCHES, ...recent.filter((c3) => !SWATCHES.includes(c3))].map((color) => /* @__PURE__ */ a3("button", { class: "fnc-swatch", style: { background: color }, "data-tip": t3.color, "aria-label": `${t3.color} ${color}`, "aria-pressed": current === color, onClick: () => pickColor(color) })),
1141
+ /* @__PURE__ */ a3("label", { class: "fnc-swatch fnc-swatch-add", "data-tip": t3.customColor, "aria-label": t3.customColor, children: [
1142
+ "+",
1143
+ /* @__PURE__ */ a3(
1144
+ "input",
1145
+ {
1146
+ ref: colorInput,
1147
+ type: "color",
1148
+ value: /^#[0-9a-f]{6}$/i.test(current) ? current : "#3182f6",
1149
+ onInput: (e3) => pickColor(e3.target.value),
1150
+ onChange: (e3) => addRecent(e3.target.value)
1151
+ }
1152
+ )
1153
+ ] }),
1154
+ /* @__PURE__ */ a3("div", { class: "fnc-sep" }),
1155
+ /* @__PURE__ */ a3("div", { class: "fnc-selbar-menu", children: [
1156
+ /* @__PURE__ */ a3("button", { class: "fnc-btn fnc-selbar-width", "data-tip": t3.lineWidth, "aria-label": t3.lineWidth, "aria-expanded": menu === "width", onClick: () => setMenu(menu === "width" ? null : "width"), children: /* @__PURE__ */ a3("svg", { width: "20", height: "20", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ a3("path", { d: "M3 10h14", stroke: "currentColor", "stroke-width": style.width ?? 2, "stroke-linecap": "round" }) }) }),
1157
+ menu === "width" && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-selbar-pop", role: "menu", children: WIDTHS.map((w3) => /* @__PURE__ */ a3(
1158
+ "button",
1159
+ {
1160
+ class: "fnc-menu-item",
1161
+ role: "menuitemradio",
1162
+ "aria-checked": (style.width ?? 2) === w3,
1163
+ onClick: () => {
1164
+ setStyle({ width: w3 });
1165
+ setMenu(null);
1166
+ },
1167
+ children: [
1168
+ /* @__PURE__ */ a3("svg", { width: "40", height: "12", viewBox: "0 0 40 12", "aria-hidden": "true", children: /* @__PURE__ */ a3("path", { d: "M2 6h36", stroke: "currentColor", "stroke-width": w3, "stroke-linecap": "round" }) }),
1169
+ /* @__PURE__ */ a3("span", { children: [
1170
+ w3,
1171
+ "px"
1172
+ ] })
1173
+ ]
1174
+ }
1175
+ )) })
1176
+ ] }),
1177
+ /* @__PURE__ */ a3(
1040
1178
  "button",
1041
1179
  {
1042
- class: "fnc-swatch",
1043
- style: { background: color },
1044
- "data-tip": t3.color,
1045
- "aria-label": `${t3.color} ${color}`,
1046
- "aria-pressed": (d3.color ?? SWATCHES[0]) === color,
1047
- onClick: () => ctx.chart.updateDrawing(d3.id, { color })
1180
+ class: "fnc-btn",
1181
+ "data-tip": `${t3.lineStyle}: ${dash === "solid" ? t3.lineSolid : dash === "dashed" ? t3.lineDashed : t3.lineDotted}`,
1182
+ "aria-label": t3.lineStyle,
1183
+ onClick: () => setStyle({ dash: DASH_ORDER[(DASH_ORDER.indexOf(dash) + 1) % DASH_ORDER.length] }),
1184
+ children: /* @__PURE__ */ a3(DashGlyph, { dash })
1185
+ }
1186
+ ),
1187
+ def?.text && /* @__PURE__ */ a3(
1188
+ "input",
1189
+ {
1190
+ class: "fnc-selbar-text",
1191
+ value: style.text ?? "",
1192
+ placeholder: t3.textPlaceholder,
1193
+ "aria-label": t3.drawingText,
1194
+ maxLength: 200,
1195
+ onInput: (e3) => setStyle({ text: e3.target.value }),
1196
+ onKeyDown: (e3) => e3.stopPropagation()
1048
1197
  }
1049
- )),
1198
+ ),
1050
1199
  /* @__PURE__ */ a3("div", { class: "fnc-sep" }),
1200
+ /* @__PURE__ */ a3("button", { class: "fnc-btn", "data-tip": locked ? t3.unlock : t3.lock, "aria-label": locked ? t3.unlock : t3.lock, "aria-pressed": locked, onClick: () => setStyle({ locked: !locked }), children: /* @__PURE__ */ a3(LockIcon, { open: !locked }) }),
1201
+ /* @__PURE__ */ a3(
1202
+ "button",
1203
+ {
1204
+ class: "fnc-btn",
1205
+ "data-tip": t3.duplicate,
1206
+ "aria-label": t3.duplicate,
1207
+ onClick: () => {
1208
+ const id = ctx.chart.addDrawing({ type: d3.type, points: d3.points.map((p3) => ({ ...p3 })), ...d3.color ? { color: d3.color } : {}, style: { ...style, locked: false } });
1209
+ ctx.chart.selectDrawing(id);
1210
+ },
1211
+ children: /* @__PURE__ */ a3(CopyIcon, {})
1212
+ }
1213
+ ),
1051
1214
  /* @__PURE__ */ a3("button", { class: "fnc-btn", "data-tip": t3.remove, "aria-label": t3.remove, onClick: () => ctx.chart.removeDrawing(d3.id), children: /* @__PURE__ */ a3(TrashIcon, {}) })
1052
1215
  ] });
1053
1216
  }
@@ -1255,7 +1418,7 @@ function IndicatorPicker({ ctx }) {
1255
1418
  ] })
1256
1419
  ] });
1257
1420
  }
1258
- var WIDTHS = [1, 2, 3, 4];
1421
+ var WIDTHS2 = [1, 2, 3, 4];
1259
1422
  function defaultSeries(def, params) {
1260
1423
  return def.compute([], params).series.filter((s3) => s3.kind === "line");
1261
1424
  }
@@ -1285,7 +1448,7 @@ function StyleControls({
1285
1448
  onInput: (e3) => onChange({ color: e3.currentTarget.value })
1286
1449
  }
1287
1450
  ),
1288
- /* @__PURE__ */ a3("select", { class: "fnc-select fnc-select-width", value: row2.width, "aria-label": t3.lineWidth, onChange: (e3) => onChange({ width: Number(e3.currentTarget.value) }), children: WIDTHS.map((w3) => /* @__PURE__ */ a3("option", { value: w3, children: [
1451
+ /* @__PURE__ */ a3("select", { class: "fnc-select fnc-select-width", value: row2.width, "aria-label": t3.lineWidth, onChange: (e3) => onChange({ width: Number(e3.currentTarget.value) }), children: WIDTHS2.map((w3) => /* @__PURE__ */ a3("option", { value: w3, children: [
1289
1452
  w3,
1290
1453
  "px"
1291
1454
  ] })) })
@@ -1599,7 +1762,7 @@ function SymbolAvatar({ info, size = 24 }) {
1599
1762
  // src/components/symbol-search.tsx
1600
1763
  import { formatPrice } from "@funnest/charts";
1601
1764
  var DEBOUNCE_MS = 150;
1602
- var RECENT_KEY = "recent-symbols";
1765
+ var RECENT_KEY2 = "recent-symbols";
1603
1766
  var FAVORITE_KEY = "favorite-symbols";
1604
1767
  var MAX_RECENT = 8;
1605
1768
  var displayCode = (symbol) => symbol.slice(symbol.lastIndexOf(":") + 1);
@@ -1638,7 +1801,7 @@ function SymbolSearch({ ctx }) {
1638
1801
  const [exchange, setExchange] = d2(null);
1639
1802
  const [results, setResults] = d2(null);
1640
1803
  const [loading, setLoading] = d2(true);
1641
- const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY, []));
1804
+ const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY2, []));
1642
1805
  const [favorites, setFavorites] = d2(() => ctx.storage.get(FAVORITE_KEY, []));
1643
1806
  const [active, setActive] = d2(0);
1644
1807
  const inputRef = T2(null);
@@ -1682,7 +1845,7 @@ function SymbolSearch({ ctx }) {
1682
1845
  const toggleFavorite = (info) => save(FAVORITE_KEY, isFavorite(info) ? favorites.filter((f4) => f4.symbol !== info.symbol) : [...favorites, toStored(info)], setFavorites);
1683
1846
  const choose = (info) => {
1684
1847
  if (!info) return;
1685
- save(RECENT_KEY, [toStored(info), ...recent.filter((r3) => r3.symbol !== info.symbol)].slice(0, MAX_RECENT), setRecent);
1848
+ save(RECENT_KEY2, [toStored(info), ...recent.filter((r3) => r3.symbol !== info.symbol)].slice(0, MAX_RECENT), setRecent);
1686
1849
  close();
1687
1850
  if (comparing) ctx.chart.addCompare(info.symbol).catch((error) => console.error("funnest-charts: compare failed", error));
1688
1851
  else ctx.selectSymbol(info.symbol);
@@ -1757,7 +1920,7 @@ function SymbolSearch({ ctx }) {
1757
1920
  "data-tip": t3.removeRecent,
1758
1921
  onClick: (e3) => {
1759
1922
  e3.stopPropagation();
1760
- save(RECENT_KEY, recent.filter((r3) => r3.symbol !== info.symbol), setRecent);
1923
+ save(RECENT_KEY2, recent.filter((r3) => r3.symbol !== info.symbol), setRecent);
1761
1924
  },
1762
1925
  children: /* @__PURE__ */ a3(SmallCloseIcon, {})
1763
1926
  }
@@ -1808,7 +1971,7 @@ function SymbolSearch({ ctx }) {
1808
1971
  /* @__PURE__ */ a3("div", { class: "fnc-sr-list fnc-scroll", id: "fnc-search-results", role: "listbox", ref: listRef, children: [
1809
1972
  recentRows.length > 0 && /* @__PURE__ */ a3("div", { class: "fnc-sr-section", children: [
1810
1973
  /* @__PURE__ */ a3("span", { children: t3.recentSearches }),
1811
- /* @__PURE__ */ a3("button", { class: "fnc-link", onClick: () => save(RECENT_KEY, [], setRecent), children: t3.clearRecent })
1974
+ /* @__PURE__ */ a3("button", { class: "fnc-link", onClick: () => save(RECENT_KEY2, [], setRecent), children: t3.clearRecent })
1812
1975
  ] }),
1813
1976
  recentRows.map((info, i3) => row2(info, i3, true)),
1814
1977
  (recentRows.length > 0 || view.kind === "favorites") && /* @__PURE__ */ a3("div", { class: "fnc-sr-section", children: view.kind === "favorites" ? t3.favorites : t3.symbols }),
@@ -2102,7 +2265,7 @@ function Toolbar({ ctx }) {
2102
2265
  "data-tip": t3.addIndicator,
2103
2266
  "aria-label": t3.addIndicator,
2104
2267
  "aria-haspopup": "dialog",
2105
- onClick: () => ctx.store.set({ dialog: { kind: "indicators" } }),
2268
+ onClick: () => ctx.onIndicatorsClick ? ctx.onIndicatorsClick(ctx.chart) : ctx.store.set({ dialog: { kind: "indicators" } }),
2106
2269
  children: [
2107
2270
  /* @__PURE__ */ a3(IndicatorIcon, {}),
2108
2271
  /* @__PURE__ */ a3("span", { children: t3.indicators })
@@ -2715,6 +2878,9 @@ var CSS = (
2715
2878
  /* \u2500\u2500 Drawing tool bar (left) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2716
2879
  .fnc-drawbar-host { flex: none; display: flex; }
2717
2880
  .fnc-drawbar {
2881
+ /* Above the plot, so a tool list tab (it sticks out over the chart) can be clicked. */
2882
+ position: relative;
2883
+ z-index: 5;
2718
2884
  display: flex;
2719
2885
  flex-direction: column;
2720
2886
  align-items: center;
@@ -2748,6 +2914,16 @@ var CSS = (
2748
2914
  }
2749
2915
  .fnc-swatch[aria-pressed='true'] { box-shadow: 0 0 0 2px var(--fnc-bg), 0 0 0 4px var(--fnc-text-2); }
2750
2916
  .fnc-selbar .fnc-btn { width: 32px; height: 32px; }
2917
+ .fnc-selbar { padding-left: 4px; }
2918
+ .fnc-selbar-grip { cursor: grab; touch-action: none; }
2919
+ .fnc-selbar-grip:active { cursor: grabbing; }
2920
+ .fnc-swatch-add { position: relative; display: grid; place-items: center; overflow: hidden; border: 1px dashed var(--fnc-text-2); color: var(--fnc-text-2); font-size: 13px; line-height: 1; cursor: pointer; }
2921
+ .fnc-swatch-add input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
2922
+ .fnc-selbar-menu { position: relative; }
2923
+ .fnc-selbar-pop { top: calc(100% + 6px); left: 50%; transform: translateX(-50%); min-width: 110px; }
2924
+ .fnc-selbar-pop .fnc-menu-item { gap: 10px; }
2925
+ .fnc-selbar-text { width: 150px; height: 28px; padding: 0 8px; border: 1px solid var(--fnc-border); border-radius: var(--fnc-radius); background: var(--fnc-surface); color: var(--fnc-text); font: inherit; font-size: 13px; outline: none; }
2926
+ .fnc-selbar-text:focus { border-color: var(--fnc-text-2); }
2751
2927
  .fnc-overlay { pointer-events: none; }
2752
2928
  .fnc-overlay button { pointer-events: auto; }
2753
2929
 
@@ -3462,20 +3638,48 @@ var CSS = (
3462
3638
  /* \u2500\u2500 Drawing tool groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3463
3639
  .fnc-tool-group { position: relative; display: flex; }
3464
3640
  .fnc-tool-group[data-open='true'] { z-index: 30; }
3641
+ /* The list opener: a corner mark at rest; on hover a tab slides out to the right of the
3642
+ button (a bigger target, like the reference terminals). Touch screens always show the tab. */
3465
3643
  .fnc-tool-more {
3466
3644
  position: absolute;
3645
+ top: 0;
3467
3646
  right: -2px;
3468
- bottom: -2px;
3647
+ bottom: 0;
3469
3648
  display: flex;
3470
3649
  align-items: flex-end;
3471
3650
  justify-content: flex-end;
3472
- width: 14px;
3473
- height: 14px;
3474
- padding: 3px;
3651
+ width: 12px;
3652
+ padding: 0 0 2px;
3653
+ border-radius: 0 var(--fnc-radius) var(--fnc-radius) 0;
3475
3654
  color: var(--fnc-text-2);
3476
- opacity: 0.7;
3655
+ transition: right 0.12s ease, width 0.12s ease, background 0.12s ease;
3656
+ }
3657
+ .fnc-tool-corner { display: flex; opacity: 0.7; }
3658
+ .fnc-tool-chevron { display: none; }
3659
+ .fnc-tool-chevron svg { width: 12px; height: 12px; }
3660
+ .fnc-tool-group:hover .fnc-tool-more,
3661
+ .fnc-tool-more[aria-expanded='true'],
3662
+ .fnc-tool-more:focus-visible {
3663
+ right: -13px;
3664
+ width: 14px;
3665
+ align-items: center;
3666
+ justify-content: center;
3667
+ padding: 0;
3668
+ background: var(--fnc-surface);
3669
+ color: var(--fnc-text);
3670
+ box-shadow: 0 0 0 1px var(--fnc-border);
3671
+ }
3672
+ .fnc-tool-group:hover .fnc-tool-corner,
3673
+ .fnc-tool-more[aria-expanded='true'] .fnc-tool-corner,
3674
+ .fnc-tool-more:focus-visible .fnc-tool-corner { display: none; }
3675
+ .fnc-tool-group:hover .fnc-tool-chevron,
3676
+ .fnc-tool-more[aria-expanded='true'] .fnc-tool-chevron,
3677
+ .fnc-tool-more:focus-visible .fnc-tool-chevron { display: flex; }
3678
+ @media (hover: none) {
3679
+ .fnc-tool-more { right: -12px; width: 14px; align-items: center; justify-content: center; padding: 0; }
3680
+ .fnc-tool-corner { display: none; }
3681
+ .fnc-tool-chevron { display: flex; }
3477
3682
  }
3478
- .fnc-tool-more:hover, .fnc-tool-more[aria-expanded='true'] { opacity: 1; color: var(--fnc-text); }
3479
3683
  .fnc-tool-menu { top: 0; left: calc(100% + 8px); min-width: 200px; max-height: min(640px, calc(100vh - 120px)); overflow-y: auto; }
3480
3684
  .fnc-tool-section { padding: 8px 10px 4px; color: var(--fnc-text-2); font-size: 11px; font-weight: 600; }
3481
3685
  .fnc-tool-section.is-after { margin-top: 4px; padding-top: 10px; border-top: 1px solid var(--fnc-border); }
@@ -3937,6 +4141,7 @@ function createPane(shell, config) {
3937
4141
  drawingToolbar: config.drawingToolbar,
3938
4142
  attribution: config.attribution,
3939
4143
  contextMenuItems: config.contextMenuItems,
4144
+ onIndicatorsClick: config.onIndicatorsClick,
3940
4145
  slots: shell.slots,
3941
4146
  multi: config.multi,
3942
4147
  setDrawbarCollapsed: shell.setDrawbarCollapsed,
@@ -4012,6 +4217,7 @@ function createTradingChart(container, options) {
4012
4217
  attribution = true,
4013
4218
  colorScheme = "system",
4014
4219
  contextMenuItems,
4220
+ onIndicatorsClick,
4015
4221
  theme,
4016
4222
  ...chartOptions
4017
4223
  } = options;
@@ -4036,7 +4242,8 @@ function createTradingChart(container, options) {
4036
4242
  searchHotkey,
4037
4243
  drawingToolbar,
4038
4244
  attribution,
4039
- contextMenuItems
4245
+ contextMenuItems,
4246
+ onIndicatorsClick
4040
4247
  });
4041
4248
  shell.main.insertBefore(pane.stage, shell.slots.bottom);
4042
4249
  shell.setPanes(() => [pane]);
@@ -4108,6 +4315,7 @@ function createMultiChart(container, options) {
4108
4315
  attribution = true,
4109
4316
  colorScheme = "system",
4110
4317
  contextMenuItems,
4318
+ onIndicatorsClick,
4111
4319
  theme,
4112
4320
  ...chartOptions
4113
4321
  } = options;
@@ -4175,6 +4383,7 @@ function createMultiChart(container, options) {
4175
4383
  // One badge for the whole widget, not one per pane.
4176
4384
  attribution: attribution && index === 0,
4177
4385
  contextMenuItems,
4386
+ onIndicatorsClick,
4178
4387
  multi: controls
4179
4388
  });
4180
4389
  const cell = div("fnc-pane");
package/dist/pane.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface PaneConfig {
17
17
  /** Ctrl/⌘+K opens this widget's symbol search (its footer shows the hint only then). */
18
18
  searchHotkey: boolean;
19
19
  contextMenuItems?: ContextMenuItems | undefined;
20
+ onIndicatorsClick?: ((chart: Chart) => void) | undefined;
20
21
  multi?: MultiControls | undefined;
21
22
  }
22
23
  /**
@@ -50,6 +50,11 @@ export interface TradingChartOptions extends Omit<ChartOptions, 'datafeed'> {
50
50
  * e.g. "이 가격에 알림 만들기". Called on every right-click with the price and time under the pointer.
51
51
  */
52
52
  contextMenuItems?: ContextMenuItems;
53
+ /**
54
+ * The toolbar's 지표 button opens this instead of the built-in indicator picker (an app's own
55
+ * indicator browser). Called with the pane's chart; add indicators with chart.addIndicator().
56
+ */
57
+ onIndicatorsClick?: (chart: Chart) => void;
53
58
  }
54
59
  export interface TradingChart {
55
60
  /** The underlying engine, for anything the widget doesn't wrap. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts-ui",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "Pro trading UI (toolbar, drawing tools, sheets) for Funnest Charts. Framework-agnostic.",
5
5
  "keywords": [
6
6
  "chart",
@@ -42,7 +42,7 @@
42
42
  "NOTICE"
43
43
  ],
44
44
  "dependencies": {
45
- "@funnest/charts": "0.6.9"
45
+ "@funnest/charts": "0.6.11"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"