@funnest/charts-ui 0.6.9 → 0.6.12

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,20 @@ export declare const STRINGS: {
75
75
  magnet: string;
76
76
  remove: string;
77
77
  color: string;
78
+ customColor: string;
79
+ partColors: string;
80
+ resetColors: string;
81
+ lineWidth: string;
82
+ lineStyle: string;
83
+ lineSolid: string;
84
+ lineDashed: string;
85
+ lineDotted: string;
86
+ drawingText: string;
87
+ textPlaceholder: string;
88
+ lock: string;
89
+ unlock: string;
90
+ duplicate: string;
91
+ moveBar: string;
78
92
  indicators: string;
79
93
  addIndicator: string;
80
94
  indicatorSettings: string;
@@ -91,7 +105,6 @@ export declare const STRINGS: {
91
105
  removePeriod: string;
92
106
  period: string;
93
107
  source: string;
94
- lineWidth: string;
95
108
  inputs: string;
96
109
  style: string;
97
110
  intervalMinutes: string;
@@ -206,6 +219,20 @@ export declare const STRINGS: {
206
219
  magnet: string;
207
220
  remove: string;
208
221
  color: string;
222
+ customColor: string;
223
+ partColors: string;
224
+ resetColors: string;
225
+ lineWidth: string;
226
+ lineStyle: string;
227
+ lineSolid: string;
228
+ lineDashed: string;
229
+ lineDotted: string;
230
+ drawingText: string;
231
+ textPlaceholder: string;
232
+ lock: string;
233
+ unlock: string;
234
+ duplicate: string;
235
+ moveBar: string;
209
236
  indicators: string;
210
237
  addIndicator: string;
211
238
  indicatorSettings: string;
@@ -222,7 +249,6 @@ export declare const STRINGS: {
222
249
  removePeriod: string;
223
250
  period: string;
224
251
  source: string;
225
- lineWidth: string;
226
252
  inputs: string;
227
253
  style: string;
228
254
  intervalMinutes: string;
package/dist/icons.d.ts CHANGED
@@ -55,3 +55,11 @@ 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
+ /** Several colors (a tool's parts): a palette. */
60
+ export declare const PaletteIcon: () => import("preact").JSX.Element;
61
+ export declare const LockIcon: ({ open }: {
62
+ open?: boolean;
63
+ }) => import("preact").JSX.Element;
64
+ export declare const CopyIcon: () => import("preact").JSX.Element;
65
+ export declare const TextIcon: () => import("preact").JSX.Element;
package/dist/index.js CHANGED
@@ -498,6 +498,20 @@ 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
+ partColors: "\uBD80\uBD84 \uC0C9\uC0C1",
503
+ resetColors: "\uAE30\uBCF8 \uC0C9\uC73C\uB85C",
504
+ lineWidth: "\uC120 \uAD75\uAE30",
505
+ lineStyle: "\uC120 \uBAA8\uC591",
506
+ lineSolid: "\uC2E4\uC120",
507
+ lineDashed: "\uD30C\uC120",
508
+ lineDotted: "\uC810\uC120",
509
+ drawingText: "\uD14D\uC2A4\uD2B8",
510
+ textPlaceholder: "\uD14D\uC2A4\uD2B8 \uC785\uB825",
511
+ lock: "\uC7A0\uADF8\uAE30",
512
+ unlock: "\uC7A0\uAE08 \uD480\uAE30",
513
+ duplicate: "\uBCF5\uC81C",
514
+ moveBar: "\uB04C\uC5B4\uC11C \uC62E\uAE30\uAE30 (\uB450 \uBC88 \uB204\uB974\uBA74 \uC81C\uC790\uB9AC)",
501
515
  indicators: "\uC9C0\uD45C",
502
516
  addIndicator: "\uC9C0\uD45C \uCD94\uAC00",
503
517
  indicatorSettings: "\uC9C0\uD45C \uC124\uC815",
@@ -514,7 +528,6 @@ var STRINGS = {
514
528
  removePeriod: "\uAE30\uAC04 \uC0AD\uC81C",
515
529
  period: "\uAE30\uAC04",
516
530
  source: "\uAC00\uACA9 \uAE30\uC900",
517
- lineWidth: "\uB450\uAED8",
518
531
  inputs: "\uC785\uB825\uAC12",
519
532
  style: "\uC2A4\uD0C0\uC77C",
520
533
  // intervals
@@ -631,6 +644,20 @@ var STRINGS = {
631
644
  magnet: "Magnet (snap to OHLC)",
632
645
  remove: "Remove",
633
646
  color: "Color",
647
+ customColor: "Other color",
648
+ partColors: "Part colors",
649
+ resetColors: "Reset colors",
650
+ lineWidth: "Line width",
651
+ lineStyle: "Line style",
652
+ lineSolid: "Solid",
653
+ lineDashed: "Dashed",
654
+ lineDotted: "Dotted",
655
+ drawingText: "Text",
656
+ textPlaceholder: "Type text",
657
+ lock: "Lock",
658
+ unlock: "Unlock",
659
+ duplicate: "Duplicate",
660
+ moveBar: "Drag to move (double-click to reset)",
634
661
  indicators: "Indicators",
635
662
  addIndicator: "Add indicator",
636
663
  indicatorSettings: "Indicator settings",
@@ -647,7 +674,6 @@ var STRINGS = {
647
674
  removePeriod: "Remove period",
648
675
  period: "Period",
649
676
  source: "Source",
650
- lineWidth: "Width",
651
677
  inputs: "Inputs",
652
678
  style: "Style",
653
679
  intervalMinutes: "Minutes",
@@ -859,6 +885,19 @@ var ToolsIcon = () => /* @__PURE__ */ a3(Icon, { size: 18, children: [
859
885
  /* @__PURE__ */ a3("circle", { cx: "12.5", cy: "6", r: "2" }),
860
886
  /* @__PURE__ */ a3("circle", { cx: "7.5", cy: "14", r: "2" })
861
887
  ] });
888
+ 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" }) });
889
+ var PaletteIcon = () => /* @__PURE__ */ a3(Icon, { children: [
890
+ /* @__PURE__ */ a3("path", { d: "M10 3.5a6.5 6.5 0 1 0 0 13c1 0 1.5-.6 1.5-1.3 0-.9-.8-1.2-.8-2 0-.7.6-1.2 1.4-1.2h1.6a2.8 2.8 0 0 0 2.8-2.8C16.5 6 13.6 3.5 10 3.5Z" }),
891
+ /* @__PURE__ */ a3("path", { d: "M6.6 9.2h.01M8.4 6.6h.01M11.6 6.4h.01M13.6 8.6h.01", "stroke-width": "2.2" })
892
+ ] });
893
+ var LockIcon = ({ open = false }) => /* @__PURE__ */ a3(Icon, { children: [
894
+ /* @__PURE__ */ a3("rect", { x: "4.5", y: "9", width: "11", height: "8", rx: "1.8" }),
895
+ /* @__PURE__ */ a3("path", { d: open ? "M7 9V6.5a3 3 0 0 1 5.8-1" : "M7 9V6.5a3 3 0 0 1 6 0V9" })
896
+ ] });
897
+ var CopyIcon = () => /* @__PURE__ */ a3(Icon, { children: [
898
+ /* @__PURE__ */ a3("rect", { x: "7", y: "7", width: "9.5", height: "9.5", rx: "1.8" }),
899
+ /* @__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" })
900
+ ] });
862
901
 
863
902
  // src/store.ts
864
903
  var Store = class {
@@ -933,8 +972,13 @@ function ToolGroup({
933
972
  A2(() => {
934
973
  if (!open) return;
935
974
  const close = (e3) => !ref.current?.contains(e3.target) && onOpen(false);
975
+ const esc = (e3) => e3.key === "Escape" && onOpen(false);
936
976
  document.addEventListener("pointerdown", close, true);
937
- return () => document.removeEventListener("pointerdown", close, true);
977
+ document.addEventListener("keydown", esc);
978
+ return () => {
979
+ document.removeEventListener("pointerdown", close, true);
980
+ document.removeEventListener("keydown", esc);
981
+ };
938
982
  }, [open]);
939
983
  const pick = (def) => {
940
984
  setChosen(def.type);
@@ -943,7 +987,10 @@ function ToolGroup({
943
987
  };
944
988
  return /* @__PURE__ */ a3("div", { class: "fnc-tool-group", ref, "data-open": open, children: [
945
989
  /* @__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, {}) }),
990
+ 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: [
991
+ /* @__PURE__ */ a3("span", { class: "fnc-tool-corner", children: /* @__PURE__ */ a3(CornerIcon, {}) }),
992
+ /* @__PURE__ */ a3("span", { class: "fnc-tool-chevron", children: /* @__PURE__ */ a3(ChevronRightIcon, {}) })
993
+ ] }),
947
994
  open && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-tool-menu", role: "menu", children: tools.map((def, i3) => {
948
995
  const section = def.section?.[s3.locale];
949
996
  const heading = section && section !== tools[i3 - 1]?.section?.[s3.locale];
@@ -1030,24 +1077,194 @@ function DrawingToolbar({ ctx }) {
1030
1077
  ] });
1031
1078
  }
1032
1079
  var SWATCHES = ["#3182F6", "#F04452", "#12B886", "#FF9F0A", "#7048E8"];
1080
+ var WIDTHS = [1, 2, 3, 4];
1081
+ var DASH_ORDER = ["solid", "dashed", "dotted"];
1082
+ var RECENT_KEY = "fnc-drawing-colors";
1083
+ var POS_KEY = "fnc-selbar-pos";
1084
+ var readJson = (key, fallback) => {
1085
+ try {
1086
+ const raw = localStorage.getItem(key);
1087
+ return raw ? JSON.parse(raw) : fallback;
1088
+ } catch {
1089
+ return fallback;
1090
+ }
1091
+ };
1092
+ var writeJson = (key, value) => {
1093
+ try {
1094
+ localStorage.setItem(key, JSON.stringify(value));
1095
+ } catch {
1096
+ }
1097
+ };
1098
+ function DashGlyph({ dash }) {
1099
+ const pattern = dash === "dashed" ? "4 3" : dash === "dotted" ? "1 3" : void 0;
1100
+ 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 }) });
1101
+ }
1033
1102
  function SelectionBar({ ctx }) {
1034
1103
  const s3 = useStore(ctx.store);
1035
1104
  const t3 = STRINGS[s3.locale];
1036
1105
  const d3 = s3.selection;
1106
+ const [offset, setOffset] = d2(() => readJson(POS_KEY, [0, 0]));
1107
+ const [recent, setRecent] = d2(() => readJson(RECENT_KEY, []));
1108
+ const [menu, setMenu] = d2(null);
1109
+ const colorInput = T2(null);
1037
1110
  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(
1111
+ const def = ctx.chart.drawingDefinitions().find((x2) => x2.type === d3.type);
1112
+ const style = d3.style ?? {};
1113
+ const setStyle = (patch) => ctx.chart.updateDrawing(d3.id, { style: { ...style, ...patch } });
1114
+ const current = d3.color ?? SWATCHES[0];
1115
+ const pickColor = (color) => {
1116
+ if (style.colors) {
1117
+ const { colors: _drop, ...rest } = style;
1118
+ ctx.chart.updateDrawing(d3.id, { color, style: rest });
1119
+ } else ctx.chart.updateDrawing(d3.id, { color });
1120
+ };
1121
+ const slots = def?.colors ?? [];
1122
+ const slotColor = (key, fallback) => style.colors?.[key] ?? d3.color ?? fallback;
1123
+ const addRecent = (color) => {
1124
+ const next = [color, ...recent.filter((c3) => c3 !== color)].slice(0, 3);
1125
+ setRecent(next);
1126
+ writeJson(RECENT_KEY, next);
1127
+ };
1128
+ const locked = !!style.locked;
1129
+ const dash = style.dash ?? "solid";
1130
+ const startMove = (e3) => {
1131
+ e3.preventDefault();
1132
+ const start = [e3.clientX, e3.clientY];
1133
+ const from = offset;
1134
+ const move = (ev) => setOffset([from[0] + ev.clientX - start[0], from[1] + ev.clientY - start[1]]);
1135
+ const up = (ev) => {
1136
+ document.removeEventListener("pointermove", move);
1137
+ document.removeEventListener("pointerup", up);
1138
+ const next = [from[0] + ev.clientX - start[0], from[1] + ev.clientY - start[1]];
1139
+ setOffset(next);
1140
+ writeJson(POS_KEY, next);
1141
+ };
1142
+ document.addEventListener("pointermove", move);
1143
+ document.addEventListener("pointerup", up);
1144
+ };
1145
+ return /* @__PURE__ */ a3("div", { class: "fnc-selbar", role: "toolbar", style: { left: `${s3.layout.plotWidth / 2 + offset[0]}px`, top: `${8 + offset[1]}px` }, children: [
1146
+ /* @__PURE__ */ a3(
1147
+ "button",
1148
+ {
1149
+ class: "fnc-btn fnc-selbar-grip",
1150
+ "data-tip": t3.moveBar,
1151
+ "aria-label": t3.moveBar,
1152
+ onPointerDown: startMove,
1153
+ onDblClick: () => {
1154
+ setOffset([0, 0]);
1155
+ writeJson(POS_KEY, [0, 0]);
1156
+ },
1157
+ children: /* @__PURE__ */ a3(GripIcon, {})
1158
+ }
1159
+ ),
1160
+ [...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) })),
1161
+ /* @__PURE__ */ a3("label", { class: "fnc-swatch fnc-swatch-add", "data-tip": t3.customColor, "aria-label": t3.customColor, children: [
1162
+ "+",
1163
+ /* @__PURE__ */ a3(
1164
+ "input",
1165
+ {
1166
+ ref: colorInput,
1167
+ type: "color",
1168
+ value: /^#[0-9a-f]{6}$/i.test(current) ? current : "#3182f6",
1169
+ onInput: (e3) => pickColor(e3.target.value),
1170
+ onChange: (e3) => addRecent(e3.target.value)
1171
+ }
1172
+ )
1173
+ ] }),
1174
+ slots.length > 0 && /* @__PURE__ */ a3("div", { class: "fnc-selbar-menu", children: [
1175
+ /* @__PURE__ */ a3("button", { class: "fnc-btn", "data-tip": t3.partColors, "aria-label": t3.partColors, "aria-expanded": menu === "colors", onClick: () => setMenu(menu === "colors" ? null : "colors"), children: /* @__PURE__ */ a3(PaletteIcon, {}) }),
1176
+ menu === "colors" && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-selbar-pop fnc-part-menu", role: "menu", children: [
1177
+ slots.map((slot) => {
1178
+ const c3 = slotColor(slot.key, slot.color);
1179
+ return /* @__PURE__ */ a3("label", { class: "fnc-menu-item fnc-part", children: [
1180
+ /* @__PURE__ */ a3("span", { class: "fnc-swatch", style: { background: c3 } }),
1181
+ /* @__PURE__ */ a3("span", { children: slot.name[s3.locale] }),
1182
+ /* @__PURE__ */ a3(
1183
+ "input",
1184
+ {
1185
+ type: "color",
1186
+ "aria-label": `${slot.name[s3.locale]} ${t3.color}`,
1187
+ value: /^#[0-9a-f]{6}$/i.test(c3) ? c3 : "#3182f6",
1188
+ onInput: (e3) => setStyle({ colors: { ...style.colors, [slot.key]: e3.target.value } })
1189
+ }
1190
+ )
1191
+ ] });
1192
+ }),
1193
+ /* @__PURE__ */ a3(
1194
+ "button",
1195
+ {
1196
+ class: "fnc-menu-item fnc-part-reset",
1197
+ role: "menuitem",
1198
+ onClick: () => {
1199
+ const { colors: _drop, ...rest } = style;
1200
+ ctx.chart.updateDrawing(d3.id, { style: rest });
1201
+ setMenu(null);
1202
+ },
1203
+ children: t3.resetColors
1204
+ }
1205
+ )
1206
+ ] })
1207
+ ] }),
1208
+ /* @__PURE__ */ a3("div", { class: "fnc-sep" }),
1209
+ /* @__PURE__ */ a3("div", { class: "fnc-selbar-menu", children: [
1210
+ /* @__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" }) }) }),
1211
+ menu === "width" && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-selbar-pop", role: "menu", children: WIDTHS.map((w3) => /* @__PURE__ */ a3(
1212
+ "button",
1213
+ {
1214
+ class: "fnc-menu-item",
1215
+ role: "menuitemradio",
1216
+ "aria-checked": (style.width ?? 2) === w3,
1217
+ onClick: () => {
1218
+ setStyle({ width: w3 });
1219
+ setMenu(null);
1220
+ },
1221
+ children: [
1222
+ /* @__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" }) }),
1223
+ /* @__PURE__ */ a3("span", { children: [
1224
+ w3,
1225
+ "px"
1226
+ ] })
1227
+ ]
1228
+ }
1229
+ )) })
1230
+ ] }),
1231
+ /* @__PURE__ */ a3(
1040
1232
  "button",
1041
1233
  {
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 })
1234
+ class: "fnc-btn",
1235
+ "data-tip": `${t3.lineStyle}: ${dash === "solid" ? t3.lineSolid : dash === "dashed" ? t3.lineDashed : t3.lineDotted}`,
1236
+ "aria-label": t3.lineStyle,
1237
+ onClick: () => setStyle({ dash: DASH_ORDER[(DASH_ORDER.indexOf(dash) + 1) % DASH_ORDER.length] }),
1238
+ children: /* @__PURE__ */ a3(DashGlyph, { dash })
1048
1239
  }
1049
- )),
1240
+ ),
1241
+ def?.text && /* @__PURE__ */ a3(
1242
+ "input",
1243
+ {
1244
+ class: "fnc-selbar-text",
1245
+ value: style.text ?? "",
1246
+ placeholder: t3.textPlaceholder,
1247
+ "aria-label": t3.drawingText,
1248
+ maxLength: 200,
1249
+ onInput: (e3) => setStyle({ text: e3.target.value }),
1250
+ onKeyDown: (e3) => e3.stopPropagation()
1251
+ }
1252
+ ),
1050
1253
  /* @__PURE__ */ a3("div", { class: "fnc-sep" }),
1254
+ /* @__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 }) }),
1255
+ /* @__PURE__ */ a3(
1256
+ "button",
1257
+ {
1258
+ class: "fnc-btn",
1259
+ "data-tip": t3.duplicate,
1260
+ "aria-label": t3.duplicate,
1261
+ onClick: () => {
1262
+ const id = ctx.chart.addDrawing({ type: d3.type, points: d3.points.map((p3) => ({ ...p3 })), ...d3.color ? { color: d3.color } : {}, style: { ...style, locked: false } });
1263
+ ctx.chart.selectDrawing(id);
1264
+ },
1265
+ children: /* @__PURE__ */ a3(CopyIcon, {})
1266
+ }
1267
+ ),
1051
1268
  /* @__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
1269
  ] });
1053
1270
  }
@@ -1255,7 +1472,7 @@ function IndicatorPicker({ ctx }) {
1255
1472
  ] })
1256
1473
  ] });
1257
1474
  }
1258
- var WIDTHS = [1, 2, 3, 4];
1475
+ var WIDTHS2 = [1, 2, 3, 4];
1259
1476
  function defaultSeries(def, params) {
1260
1477
  return def.compute([], params).series.filter((s3) => s3.kind === "line");
1261
1478
  }
@@ -1285,7 +1502,7 @@ function StyleControls({
1285
1502
  onInput: (e3) => onChange({ color: e3.currentTarget.value })
1286
1503
  }
1287
1504
  ),
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: [
1505
+ /* @__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
1506
  w3,
1290
1507
  "px"
1291
1508
  ] })) })
@@ -1599,7 +1816,7 @@ function SymbolAvatar({ info, size = 24 }) {
1599
1816
  // src/components/symbol-search.tsx
1600
1817
  import { formatPrice } from "@funnest/charts";
1601
1818
  var DEBOUNCE_MS = 150;
1602
- var RECENT_KEY = "recent-symbols";
1819
+ var RECENT_KEY2 = "recent-symbols";
1603
1820
  var FAVORITE_KEY = "favorite-symbols";
1604
1821
  var MAX_RECENT = 8;
1605
1822
  var displayCode = (symbol) => symbol.slice(symbol.lastIndexOf(":") + 1);
@@ -1638,7 +1855,7 @@ function SymbolSearch({ ctx }) {
1638
1855
  const [exchange, setExchange] = d2(null);
1639
1856
  const [results, setResults] = d2(null);
1640
1857
  const [loading, setLoading] = d2(true);
1641
- const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY, []));
1858
+ const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY2, []));
1642
1859
  const [favorites, setFavorites] = d2(() => ctx.storage.get(FAVORITE_KEY, []));
1643
1860
  const [active, setActive] = d2(0);
1644
1861
  const inputRef = T2(null);
@@ -1682,7 +1899,7 @@ function SymbolSearch({ ctx }) {
1682
1899
  const toggleFavorite = (info) => save(FAVORITE_KEY, isFavorite(info) ? favorites.filter((f4) => f4.symbol !== info.symbol) : [...favorites, toStored(info)], setFavorites);
1683
1900
  const choose = (info) => {
1684
1901
  if (!info) return;
1685
- save(RECENT_KEY, [toStored(info), ...recent.filter((r3) => r3.symbol !== info.symbol)].slice(0, MAX_RECENT), setRecent);
1902
+ save(RECENT_KEY2, [toStored(info), ...recent.filter((r3) => r3.symbol !== info.symbol)].slice(0, MAX_RECENT), setRecent);
1686
1903
  close();
1687
1904
  if (comparing) ctx.chart.addCompare(info.symbol).catch((error) => console.error("funnest-charts: compare failed", error));
1688
1905
  else ctx.selectSymbol(info.symbol);
@@ -1757,7 +1974,7 @@ function SymbolSearch({ ctx }) {
1757
1974
  "data-tip": t3.removeRecent,
1758
1975
  onClick: (e3) => {
1759
1976
  e3.stopPropagation();
1760
- save(RECENT_KEY, recent.filter((r3) => r3.symbol !== info.symbol), setRecent);
1977
+ save(RECENT_KEY2, recent.filter((r3) => r3.symbol !== info.symbol), setRecent);
1761
1978
  },
1762
1979
  children: /* @__PURE__ */ a3(SmallCloseIcon, {})
1763
1980
  }
@@ -1808,7 +2025,7 @@ function SymbolSearch({ ctx }) {
1808
2025
  /* @__PURE__ */ a3("div", { class: "fnc-sr-list fnc-scroll", id: "fnc-search-results", role: "listbox", ref: listRef, children: [
1809
2026
  recentRows.length > 0 && /* @__PURE__ */ a3("div", { class: "fnc-sr-section", children: [
1810
2027
  /* @__PURE__ */ a3("span", { children: t3.recentSearches }),
1811
- /* @__PURE__ */ a3("button", { class: "fnc-link", onClick: () => save(RECENT_KEY, [], setRecent), children: t3.clearRecent })
2028
+ /* @__PURE__ */ a3("button", { class: "fnc-link", onClick: () => save(RECENT_KEY2, [], setRecent), children: t3.clearRecent })
1812
2029
  ] }),
1813
2030
  recentRows.map((info, i3) => row2(info, i3, true)),
1814
2031
  (recentRows.length > 0 || view.kind === "favorites") && /* @__PURE__ */ a3("div", { class: "fnc-sr-section", children: view.kind === "favorites" ? t3.favorites : t3.symbols }),
@@ -2102,7 +2319,7 @@ function Toolbar({ ctx }) {
2102
2319
  "data-tip": t3.addIndicator,
2103
2320
  "aria-label": t3.addIndicator,
2104
2321
  "aria-haspopup": "dialog",
2105
- onClick: () => ctx.store.set({ dialog: { kind: "indicators" } }),
2322
+ onClick: () => ctx.onIndicatorsClick ? ctx.onIndicatorsClick(ctx.chart) : ctx.store.set({ dialog: { kind: "indicators" } }),
2106
2323
  children: [
2107
2324
  /* @__PURE__ */ a3(IndicatorIcon, {}),
2108
2325
  /* @__PURE__ */ a3("span", { children: t3.indicators })
@@ -2715,6 +2932,9 @@ var CSS = (
2715
2932
  /* \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
2933
  .fnc-drawbar-host { flex: none; display: flex; }
2717
2934
  .fnc-drawbar {
2935
+ /* Above the plot, so a tool list tab (it sticks out over the chart) can be clicked. */
2936
+ position: relative;
2937
+ z-index: 5;
2718
2938
  display: flex;
2719
2939
  flex-direction: column;
2720
2940
  align-items: center;
@@ -2748,6 +2968,21 @@ var CSS = (
2748
2968
  }
2749
2969
  .fnc-swatch[aria-pressed='true'] { box-shadow: 0 0 0 2px var(--fnc-bg), 0 0 0 4px var(--fnc-text-2); }
2750
2970
  .fnc-selbar .fnc-btn { width: 32px; height: 32px; }
2971
+ .fnc-selbar { padding-left: 4px; }
2972
+ .fnc-selbar-grip { cursor: grab; touch-action: none; }
2973
+ .fnc-selbar-grip:active { cursor: grabbing; }
2974
+ .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; }
2975
+ .fnc-swatch-add input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
2976
+ .fnc-selbar-menu { position: relative; }
2977
+ .fnc-selbar-pop { top: calc(100% + 6px); left: 50%; transform: translateX(-50%); min-width: 110px; }
2978
+ .fnc-selbar-pop .fnc-menu-item { gap: 10px; }
2979
+ .fnc-part-menu { min-width: 150px; max-height: 320px; overflow-y: auto; }
2980
+ .fnc-part { position: relative; cursor: pointer; }
2981
+ .fnc-part .fnc-swatch { width: 14px; height: 14px; box-shadow: 0 0 0 1px var(--fnc-border); }
2982
+ .fnc-part input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
2983
+ .fnc-part-reset { margin-top: 4px; border-top: 1px solid var(--fnc-border); color: var(--fnc-text-2); }
2984
+ .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; }
2985
+ .fnc-selbar-text:focus { border-color: var(--fnc-text-2); }
2751
2986
  .fnc-overlay { pointer-events: none; }
2752
2987
  .fnc-overlay button { pointer-events: auto; }
2753
2988
 
@@ -3462,20 +3697,50 @@ var CSS = (
3462
3697
  /* \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
3698
  .fnc-tool-group { position: relative; display: flex; }
3464
3699
  .fnc-tool-group[data-open='true'] { z-index: 30; }
3700
+ /* The list opener: a corner mark at rest; on hover a tab slides out to the right of the
3701
+ button (a bigger target, like the reference terminals). Touch screens always show the tab. */
3465
3702
  .fnc-tool-more {
3466
3703
  position: absolute;
3704
+ top: 0;
3467
3705
  right: -2px;
3468
- bottom: -2px;
3706
+ bottom: 0;
3469
3707
  display: flex;
3470
3708
  align-items: flex-end;
3471
3709
  justify-content: flex-end;
3472
- width: 14px;
3473
- height: 14px;
3474
- padding: 3px;
3710
+ width: 12px;
3711
+ padding: 0 0 2px;
3712
+ border-radius: 0 var(--fnc-radius) var(--fnc-radius) 0;
3475
3713
  color: var(--fnc-text-2);
3476
- opacity: 0.7;
3714
+ transition: right 0.12s ease, width 0.12s ease, background 0.12s ease;
3715
+ }
3716
+ .fnc-tool-corner { display: flex; opacity: 0.7; }
3717
+ .fnc-tool-chevron { display: none; }
3718
+ .fnc-tool-chevron svg { width: 12px; height: 12px; }
3719
+ .fnc-tool-group:hover .fnc-tool-more,
3720
+ .fnc-tool-more[aria-expanded='true'],
3721
+ .fnc-tool-more:focus-visible {
3722
+ right: -13px;
3723
+ width: 14px;
3724
+ align-items: center;
3725
+ justify-content: center;
3726
+ padding: 0;
3727
+ background: var(--fnc-surface);
3728
+ color: var(--fnc-text);
3729
+ box-shadow: 0 0 0 1px var(--fnc-border);
3730
+ }
3731
+ .fnc-tool-group:hover .fnc-tool-corner,
3732
+ .fnc-tool-more[aria-expanded='true'] .fnc-tool-corner,
3733
+ .fnc-tool-more:focus-visible .fnc-tool-corner { display: none; }
3734
+ .fnc-tool-group:hover .fnc-tool-chevron,
3735
+ .fnc-tool-more[aria-expanded='true'] .fnc-tool-chevron,
3736
+ .fnc-tool-more:focus-visible .fnc-tool-chevron { display: flex; }
3737
+ /* Once the list is open the tab has done its job: it fades out (clicking its spot still closes the list). */
3738
+ .fnc-tool-group[data-open='true'] .fnc-tool-more { opacity: 0; }
3739
+ @media (hover: none) {
3740
+ .fnc-tool-more { right: -12px; width: 14px; align-items: center; justify-content: center; padding: 0; }
3741
+ .fnc-tool-corner { display: none; }
3742
+ .fnc-tool-chevron { display: flex; }
3477
3743
  }
3478
- .fnc-tool-more:hover, .fnc-tool-more[aria-expanded='true'] { opacity: 1; color: var(--fnc-text); }
3479
3744
  .fnc-tool-menu { top: 0; left: calc(100% + 8px); min-width: 200px; max-height: min(640px, calc(100vh - 120px)); overflow-y: auto; }
3480
3745
  .fnc-tool-section { padding: 8px 10px 4px; color: var(--fnc-text-2); font-size: 11px; font-weight: 600; }
3481
3746
  .fnc-tool-section.is-after { margin-top: 4px; padding-top: 10px; border-top: 1px solid var(--fnc-border); }
@@ -3937,6 +4202,7 @@ function createPane(shell, config) {
3937
4202
  drawingToolbar: config.drawingToolbar,
3938
4203
  attribution: config.attribution,
3939
4204
  contextMenuItems: config.contextMenuItems,
4205
+ onIndicatorsClick: config.onIndicatorsClick,
3940
4206
  slots: shell.slots,
3941
4207
  multi: config.multi,
3942
4208
  setDrawbarCollapsed: shell.setDrawbarCollapsed,
@@ -4012,6 +4278,7 @@ function createTradingChart(container, options) {
4012
4278
  attribution = true,
4013
4279
  colorScheme = "system",
4014
4280
  contextMenuItems,
4281
+ onIndicatorsClick,
4015
4282
  theme,
4016
4283
  ...chartOptions
4017
4284
  } = options;
@@ -4036,7 +4303,8 @@ function createTradingChart(container, options) {
4036
4303
  searchHotkey,
4037
4304
  drawingToolbar,
4038
4305
  attribution,
4039
- contextMenuItems
4306
+ contextMenuItems,
4307
+ onIndicatorsClick
4040
4308
  });
4041
4309
  shell.main.insertBefore(pane.stage, shell.slots.bottom);
4042
4310
  shell.setPanes(() => [pane]);
@@ -4108,6 +4376,7 @@ function createMultiChart(container, options) {
4108
4376
  attribution = true,
4109
4377
  colorScheme = "system",
4110
4378
  contextMenuItems,
4379
+ onIndicatorsClick,
4111
4380
  theme,
4112
4381
  ...chartOptions
4113
4382
  } = options;
@@ -4175,6 +4444,7 @@ function createMultiChart(container, options) {
4175
4444
  // One badge for the whole widget, not one per pane.
4176
4445
  attribution: attribution && index === 0,
4177
4446
  contextMenuItems,
4447
+ onIndicatorsClick,
4178
4448
  multi: controls
4179
4449
  });
4180
4450
  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.12",
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.12"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"