@funnest/charts-ui 0.6.8 → 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.
@@ -1,10 +1,14 @@
1
- import type { JSX } from 'preact';
1
+ import { type JSX } from 'preact';
2
2
  import type { WidgetContext } from '../context';
3
3
  /** Left vertical tool bar (TradingView layout). On narrow screens it slides in over the chart. */
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
@@ -13,6 +13,7 @@ export declare const ChevronDownIcon: () => import("preact").JSX.Element;
13
13
  export declare const CursorIcon: () => import("preact").JSX.Element;
14
14
  export declare const TrendLineIcon: () => import("preact").JSX.Element;
15
15
  export declare const HorizontalLineIcon: () => import("preact").JSX.Element;
16
+ /** Retracement levels at their real (uneven) spacing, with the swing they're drawn on. */
16
17
  export declare const FibonacciIcon: () => import("preact").JSX.Element;
17
18
  export declare const TrashIcon: () => import("preact").JSX.Element;
18
19
  export declare const PencilIcon: () => import("preact").JSX.Element;
@@ -54,3 +55,9 @@ export declare const SvgIcon: ({ markup }: {
54
55
  export declare const CornerIcon: () => import("preact").JSX.Element;
55
56
  /** Sliders: the 도구 menu. */
56
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",
@@ -771,7 +793,10 @@ var HorizontalLineIcon = () => /* @__PURE__ */ a3(Icon, { children: [
771
793
  /* @__PURE__ */ a3("path", { d: "M3 10h14" }),
772
794
  /* @__PURE__ */ a3("circle", { cx: "10", cy: "10", r: "1.6", fill: "currentColor" })
773
795
  ] });
774
- var FibonacciIcon = () => /* @__PURE__ */ a3(Icon, { children: /* @__PURE__ */ a3("path", { d: "M3 4.5h14M3 8.2h14M3 11.8h14M3 15.5h14" }) });
796
+ var FibonacciIcon = () => /* @__PURE__ */ a3(Icon, { children: [
797
+ /* @__PURE__ */ a3("path", { d: "M3 3.5h14M3 7.4h14M3 9.9h14M3 12.3h14M3 16.5h14" }),
798
+ /* @__PURE__ */ a3("path", { d: "M4.5 16.5 15.5 3.5", "stroke-dasharray": "1.6 1.8", opacity: "0.7" })
799
+ ] });
775
800
  var TrashIcon = () => /* @__PURE__ */ a3(Icon, { children: /* @__PURE__ */ a3("path", { d: "M4.5 6h11M8 6V4.5h4V6M6 6l.7 9.5h6.6L14 6" }) });
776
801
  var PencilIcon = () => /* @__PURE__ */ a3(Icon, { children: /* @__PURE__ */ a3("path", { d: "M12.8 4.2l3 3L7.5 15.5 4 16l.5-3.5z" }) });
777
802
  var CandlesIcon = () => /* @__PURE__ */ a3(Icon, { children: [
@@ -856,6 +881,15 @@ var ToolsIcon = () => /* @__PURE__ */ a3(Icon, { size: 18, children: [
856
881
  /* @__PURE__ */ a3("circle", { cx: "12.5", cy: "6", r: "2" }),
857
882
  /* @__PURE__ */ a3("circle", { cx: "7.5", cy: "14", r: "2" })
858
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
+ ] });
859
893
 
860
894
  // src/store.ts
861
895
  var Store = class {
@@ -940,11 +974,21 @@ function ToolGroup({
940
974
  };
941
975
  return /* @__PURE__ */ a3("div", { class: "fnc-tool-group", ref, "data-open": open, children: [
942
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 }) }),
943
- 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, {}) }),
944
- open && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-tool-menu", role: "menu", children: tools.map((def) => /* @__PURE__ */ a3("button", { class: "fnc-menu-item", role: "menuitemradio", "aria-checked": def.type === current.type, onClick: () => pick(def), children: [
945
- /* @__PURE__ */ a3(ToolIcon, { def, locale: s3.locale }),
946
- /* @__PURE__ */ a3("span", { children: def.name[s3.locale] })
947
- ] })) })
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
+ ] }),
981
+ open && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-tool-menu", role: "menu", children: tools.map((def, i3) => {
982
+ const section = def.section?.[s3.locale];
983
+ const heading = section && section !== tools[i3 - 1]?.section?.[s3.locale];
984
+ return /* @__PURE__ */ a3($, { children: [
985
+ heading && /* @__PURE__ */ a3("div", { class: `fnc-tool-section${i3 > 0 ? " is-after" : ""}`, role: "presentation", children: section }),
986
+ /* @__PURE__ */ a3("button", { class: "fnc-menu-item", role: "menuitemradio", "aria-checked": def.type === current.type, onClick: () => pick(def), children: [
987
+ /* @__PURE__ */ a3(ToolIcon, { def, locale: s3.locale }),
988
+ /* @__PURE__ */ a3("span", { children: def.name[s3.locale] })
989
+ ] })
990
+ ] }, def.type);
991
+ }) })
948
992
  ] });
949
993
  }
950
994
  function DrawingToolbar({ ctx }) {
@@ -1020,24 +1064,153 @@ function DrawingToolbar({ ctx }) {
1020
1064
  ] });
1021
1065
  }
1022
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
+ }
1023
1089
  function SelectionBar({ ctx }) {
1024
1090
  const s3 = useStore(ctx.store);
1025
1091
  const t3 = STRINGS[s3.locale];
1026
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);
1027
1097
  if (!d3 || s3.layout.plotWidth <= 0) return null;
1028
- return /* @__PURE__ */ a3("div", { class: "fnc-selbar", role: "toolbar", style: { left: `${s3.layout.plotWidth / 2}px` }, children: [
1029
- 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(
1030
1127
  "button",
1031
1128
  {
1032
- class: "fnc-swatch",
1033
- style: { background: color },
1034
- "data-tip": t3.color,
1035
- "aria-label": `${t3.color} ${color}`,
1036
- "aria-pressed": (d3.color ?? SWATCHES[0]) === color,
1037
- onClick: () => ctx.chart.updateDrawing(d3.id, { color })
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, {})
1038
1138
  }
1039
- )),
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
+ ] }),
1040
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(
1178
+ "button",
1179
+ {
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()
1197
+ }
1198
+ ),
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
+ ),
1041
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, {}) })
1042
1215
  ] });
1043
1216
  }
@@ -1245,7 +1418,7 @@ function IndicatorPicker({ ctx }) {
1245
1418
  ] })
1246
1419
  ] });
1247
1420
  }
1248
- var WIDTHS = [1, 2, 3, 4];
1421
+ var WIDTHS2 = [1, 2, 3, 4];
1249
1422
  function defaultSeries(def, params) {
1250
1423
  return def.compute([], params).series.filter((s3) => s3.kind === "line");
1251
1424
  }
@@ -1275,7 +1448,7 @@ function StyleControls({
1275
1448
  onInput: (e3) => onChange({ color: e3.currentTarget.value })
1276
1449
  }
1277
1450
  ),
1278
- /* @__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: [
1279
1452
  w3,
1280
1453
  "px"
1281
1454
  ] })) })
@@ -1589,7 +1762,7 @@ function SymbolAvatar({ info, size = 24 }) {
1589
1762
  // src/components/symbol-search.tsx
1590
1763
  import { formatPrice } from "@funnest/charts";
1591
1764
  var DEBOUNCE_MS = 150;
1592
- var RECENT_KEY = "recent-symbols";
1765
+ var RECENT_KEY2 = "recent-symbols";
1593
1766
  var FAVORITE_KEY = "favorite-symbols";
1594
1767
  var MAX_RECENT = 8;
1595
1768
  var displayCode = (symbol) => symbol.slice(symbol.lastIndexOf(":") + 1);
@@ -1628,7 +1801,7 @@ function SymbolSearch({ ctx }) {
1628
1801
  const [exchange, setExchange] = d2(null);
1629
1802
  const [results, setResults] = d2(null);
1630
1803
  const [loading, setLoading] = d2(true);
1631
- const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY, []));
1804
+ const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY2, []));
1632
1805
  const [favorites, setFavorites] = d2(() => ctx.storage.get(FAVORITE_KEY, []));
1633
1806
  const [active, setActive] = d2(0);
1634
1807
  const inputRef = T2(null);
@@ -1672,7 +1845,7 @@ function SymbolSearch({ ctx }) {
1672
1845
  const toggleFavorite = (info) => save(FAVORITE_KEY, isFavorite(info) ? favorites.filter((f4) => f4.symbol !== info.symbol) : [...favorites, toStored(info)], setFavorites);
1673
1846
  const choose = (info) => {
1674
1847
  if (!info) return;
1675
- 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);
1676
1849
  close();
1677
1850
  if (comparing) ctx.chart.addCompare(info.symbol).catch((error) => console.error("funnest-charts: compare failed", error));
1678
1851
  else ctx.selectSymbol(info.symbol);
@@ -1747,7 +1920,7 @@ function SymbolSearch({ ctx }) {
1747
1920
  "data-tip": t3.removeRecent,
1748
1921
  onClick: (e3) => {
1749
1922
  e3.stopPropagation();
1750
- save(RECENT_KEY, recent.filter((r3) => r3.symbol !== info.symbol), setRecent);
1923
+ save(RECENT_KEY2, recent.filter((r3) => r3.symbol !== info.symbol), setRecent);
1751
1924
  },
1752
1925
  children: /* @__PURE__ */ a3(SmallCloseIcon, {})
1753
1926
  }
@@ -1798,7 +1971,7 @@ function SymbolSearch({ ctx }) {
1798
1971
  /* @__PURE__ */ a3("div", { class: "fnc-sr-list fnc-scroll", id: "fnc-search-results", role: "listbox", ref: listRef, children: [
1799
1972
  recentRows.length > 0 && /* @__PURE__ */ a3("div", { class: "fnc-sr-section", children: [
1800
1973
  /* @__PURE__ */ a3("span", { children: t3.recentSearches }),
1801
- /* @__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 })
1802
1975
  ] }),
1803
1976
  recentRows.map((info, i3) => row2(info, i3, true)),
1804
1977
  (recentRows.length > 0 || view.kind === "favorites") && /* @__PURE__ */ a3("div", { class: "fnc-sr-section", children: view.kind === "favorites" ? t3.favorites : t3.symbols }),
@@ -2092,7 +2265,7 @@ function Toolbar({ ctx }) {
2092
2265
  "data-tip": t3.addIndicator,
2093
2266
  "aria-label": t3.addIndicator,
2094
2267
  "aria-haspopup": "dialog",
2095
- onClick: () => ctx.store.set({ dialog: { kind: "indicators" } }),
2268
+ onClick: () => ctx.onIndicatorsClick ? ctx.onIndicatorsClick(ctx.chart) : ctx.store.set({ dialog: { kind: "indicators" } }),
2096
2269
  children: [
2097
2270
  /* @__PURE__ */ a3(IndicatorIcon, {}),
2098
2271
  /* @__PURE__ */ a3("span", { children: t3.indicators })
@@ -2705,6 +2878,9 @@ var CSS = (
2705
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 */
2706
2879
  .fnc-drawbar-host { flex: none; display: flex; }
2707
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;
2708
2884
  display: flex;
2709
2885
  flex-direction: column;
2710
2886
  align-items: center;
@@ -2738,6 +2914,16 @@ var CSS = (
2738
2914
  }
2739
2915
  .fnc-swatch[aria-pressed='true'] { box-shadow: 0 0 0 2px var(--fnc-bg), 0 0 0 4px var(--fnc-text-2); }
2740
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); }
2741
2927
  .fnc-overlay { pointer-events: none; }
2742
2928
  .fnc-overlay button { pointer-events: auto; }
2743
2929
 
@@ -3452,21 +3638,51 @@ var CSS = (
3452
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 */
3453
3639
  .fnc-tool-group { position: relative; display: flex; }
3454
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. */
3455
3643
  .fnc-tool-more {
3456
3644
  position: absolute;
3645
+ top: 0;
3457
3646
  right: -2px;
3458
- bottom: -2px;
3647
+ bottom: 0;
3459
3648
  display: flex;
3460
3649
  align-items: flex-end;
3461
3650
  justify-content: flex-end;
3462
- width: 14px;
3463
- height: 14px;
3464
- padding: 3px;
3651
+ width: 12px;
3652
+ padding: 0 0 2px;
3653
+ border-radius: 0 var(--fnc-radius) var(--fnc-radius) 0;
3465
3654
  color: var(--fnc-text-2);
3466
- 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; }
3467
3682
  }
3468
- .fnc-tool-more:hover, .fnc-tool-more[aria-expanded='true'] { opacity: 1; color: var(--fnc-text); }
3469
- .fnc-tool-menu { top: 0; left: calc(100% + 8px); min-width: 180px; }
3683
+ .fnc-tool-menu { top: 0; left: calc(100% + 8px); min-width: 200px; max-height: min(640px, calc(100vh - 120px)); overflow-y: auto; }
3684
+ .fnc-tool-section { padding: 8px 10px 4px; color: var(--fnc-text-2); font-size: 11px; font-weight: 600; }
3685
+ .fnc-tool-section.is-after { margin-top: 4px; padding-top: 10px; border-top: 1px solid var(--fnc-border); }
3470
3686
  .fnc-tool-menu .fnc-menu-item { white-space: nowrap; }
3471
3687
  .fnc-tool-letter { font-size: 13px; font-weight: 700; }
3472
3688
  .fnc-svg-icon { display: inline-flex; }
@@ -3925,6 +4141,7 @@ function createPane(shell, config) {
3925
4141
  drawingToolbar: config.drawingToolbar,
3926
4142
  attribution: config.attribution,
3927
4143
  contextMenuItems: config.contextMenuItems,
4144
+ onIndicatorsClick: config.onIndicatorsClick,
3928
4145
  slots: shell.slots,
3929
4146
  multi: config.multi,
3930
4147
  setDrawbarCollapsed: shell.setDrawbarCollapsed,
@@ -4000,6 +4217,7 @@ function createTradingChart(container, options) {
4000
4217
  attribution = true,
4001
4218
  colorScheme = "system",
4002
4219
  contextMenuItems,
4220
+ onIndicatorsClick,
4003
4221
  theme,
4004
4222
  ...chartOptions
4005
4223
  } = options;
@@ -4024,7 +4242,8 @@ function createTradingChart(container, options) {
4024
4242
  searchHotkey,
4025
4243
  drawingToolbar,
4026
4244
  attribution,
4027
- contextMenuItems
4245
+ contextMenuItems,
4246
+ onIndicatorsClick
4028
4247
  });
4029
4248
  shell.main.insertBefore(pane.stage, shell.slots.bottom);
4030
4249
  shell.setPanes(() => [pane]);
@@ -4096,6 +4315,7 @@ function createMultiChart(container, options) {
4096
4315
  attribution = true,
4097
4316
  colorScheme = "system",
4098
4317
  contextMenuItems,
4318
+ onIndicatorsClick,
4099
4319
  theme,
4100
4320
  ...chartOptions
4101
4321
  } = options;
@@ -4163,6 +4383,7 @@ function createMultiChart(container, options) {
4163
4383
  // One badge for the whole widget, not one per pane.
4164
4384
  attribution: attribution && index === 0,
4165
4385
  contextMenuItems,
4386
+ onIndicatorsClick,
4166
4387
  multi: controls
4167
4388
  });
4168
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.8",
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.8"
45
+ "@funnest/charts": "0.6.11"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"