@funnest/charts-ui 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,5 @@
1
1
  // src/trading-chart.tsx
2
- import {
3
- createChart,
4
- darkTheme,
5
- lightTheme
6
- } from "@funnest/charts";
2
+ import "@funnest/charts";
7
3
 
8
4
  // ../../node_modules/.pnpm/preact@11.2.0/node_modules/preact/dist/preact.mjs
9
5
  var n;
@@ -313,6 +309,7 @@ var STRINGS = {
313
309
  settingHighLow: "\uCD5C\uACE0\xB7\uCD5C\uC800 \uD45C\uC2DC",
314
310
  settingLastPrice: "\uD604\uC7AC\uAC00 \uC120",
315
311
  settingVolume: "\uAC70\uB798\uB7C9",
312
+ settingNavButtons: "\uD655\uB300\xB7\uC774\uB3D9 \uBC84\uD2BC",
316
313
  settingGridH: "\uAC00\uB85C \uACA9\uC790",
317
314
  settingGridV: "\uC138\uB85C \uACA9\uC790",
318
315
  resetDefaults: "\uAE30\uBCF8\uAC12\uC73C\uB85C",
@@ -367,7 +364,17 @@ var STRINGS = {
367
364
  symbols: "\uC885\uBAA9",
368
365
  searching: "\uAC80\uC0C9 \uC911",
369
366
  searchHint: "\u2191\u2193 \uC774\uB3D9 \xB7 Enter \uC120\uD0DD \xB7 Esc \uB2EB\uAE30",
370
- current: "\uD604\uC7AC"
367
+ current: "\uD604\uC7AC",
368
+ layout: "\uD654\uBA74 \uBD84\uD560",
369
+ sync: "\uCE78 \uB3D9\uAE30\uD654",
370
+ syncCrosshair: "\uC2ED\uC790\uC120",
371
+ syncTime: "\uC2DC\uAC04 \uBC94\uC704",
372
+ syncSymbol: "\uC885\uBAA9",
373
+ syncInterval: "\uC8FC\uAE30",
374
+ syncHint: "\uCF1C\uBA74 \uBAA8\uB4E0 \uCE78\uC774 \uAC19\uC774 \uC6C0\uC9C1\uC5EC\uC694",
375
+ maximizePane: "\uC774 \uCE78 \uD06C\uAC8C \uBCF4\uAE30",
376
+ restorePane: "\uBD84\uD560 \uD654\uBA74\uC73C\uB85C",
377
+ pane: "\uCE78"
371
378
  },
372
379
  en: {
373
380
  searchSymbol: "Symbol search",
@@ -409,6 +416,7 @@ var STRINGS = {
409
416
  settingHighLow: "High/low markers",
410
417
  settingLastPrice: "Last price line",
411
418
  settingVolume: "Volume",
419
+ settingNavButtons: "Zoom & scroll buttons",
412
420
  settingGridH: "Horizontal grid",
413
421
  settingGridV: "Vertical grid",
414
422
  resetDefaults: "Reset",
@@ -461,7 +469,17 @@ var STRINGS = {
461
469
  symbols: "Symbols",
462
470
  searching: "Searching",
463
471
  searchHint: "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc close",
464
- current: "Current"
472
+ current: "Current",
473
+ layout: "Layout",
474
+ sync: "Sync panes",
475
+ syncCrosshair: "Crosshair",
476
+ syncTime: "Time range",
477
+ syncSymbol: "Symbol",
478
+ syncInterval: "Interval",
479
+ syncHint: "Linked panes move together",
480
+ maximizePane: "Maximize pane",
481
+ restorePane: "Back to split view",
482
+ pane: "Pane"
465
483
  }
466
484
  };
467
485
  function resolutionLabel(resolution, locale, form = "short") {
@@ -595,6 +613,12 @@ var FunnestMark = () => /* @__PURE__ */ a2("svg", { width: "20", height: "20", v
595
613
  /* @__PURE__ */ a2("path", { d: "M7 13.5C9.5 21.5 17 23.5 23.4 11.2", stroke: "#fff", "stroke-width": "3.2", "stroke-linecap": "round" }),
596
614
  /* @__PURE__ */ a2("circle", { cx: "24.6", cy: "8.6", r: "2.9", fill: "#FFD43B" })
597
615
  ] });
616
+ var LinkIcon = () => /* @__PURE__ */ a2(Icon, { children: [
617
+ /* @__PURE__ */ a2("path", { d: "M8.5 11.5a3 3 0 0 0 4.2 0l2.6-2.6a3 3 0 0 0-4.2-4.2l-1 1" }),
618
+ /* @__PURE__ */ a2("path", { d: "M11.5 8.5a3 3 0 0 0-4.2 0l-2.6 2.6a3 3 0 0 0 4.2 4.2l1-1" })
619
+ ] });
620
+ var MaximizeIcon = () => /* @__PURE__ */ a2(Icon, { size: 16, children: /* @__PURE__ */ a2("path", { d: "M11 4h5v5M9 16H4v-5M16 4l-5 5M4 16l5-5" }) });
621
+ var RestoreIcon = () => /* @__PURE__ */ a2(Icon, { size: 16, children: /* @__PURE__ */ a2("path", { d: "M15 9h-4V5M5 11h4v4M11 9l5-5M9 11l-5 5" }) });
598
622
 
599
623
  // ../../node_modules/.pnpm/preact@11.2.0/node_modules/preact/hooks/dist/hooks.mjs
600
624
  var t2;
@@ -885,251 +909,6 @@ function SelectionBar({ ctx }) {
885
909
  ] });
886
910
  }
887
911
 
888
- // src/components/overlay.tsx
889
- import { formatCompact, formatPrice } from "@funnest/charts";
890
-
891
- // src/components/symbol-avatar.tsx
892
- function hue(text) {
893
- let h3 = 0;
894
- for (const ch of text) h3 = (h3 * 31 + ch.charCodeAt(0)) % 360;
895
- return h3;
896
- }
897
- function SymbolAvatar({ info, size = 24 }) {
898
- const [failed, setFailed] = d2(false);
899
- A2(() => setFailed(false), [info.logoUrl]);
900
- const style = { width: `${size}px`, height: `${size}px`, fontSize: `${Math.round(size * 0.46)}px` };
901
- if (info.logoUrl && !failed) {
902
- return /* @__PURE__ */ a2("img", { class: "fnc-avatar", src: info.logoUrl, alt: "", style, onError: () => setFailed(true) });
903
- }
904
- const h3 = hue(info.symbol);
905
- return /* @__PURE__ */ a2(
906
- "span",
907
- {
908
- class: "fnc-avatar fnc-avatar-initial",
909
- "aria-hidden": "true",
910
- style: { ...style, background: `hsl(${h3} 70% 50% / 0.16)`, color: `hsl(${h3} 60% 42%)` },
911
- children: (info.name || info.symbol).trim().charAt(0).toUpperCase()
912
- }
913
- );
914
- }
915
-
916
- // src/components/overlay.tsx
917
- var ZOOM_STEP = 1.25;
918
- var CONTROL_OFFSET = 52;
919
- var dirClass = (v3) => v3 > 0 ? "fnc-up" : v3 < 0 ? "fnc-down" : "fnc-flat";
920
- function IndicatorItem({ ctx, item }) {
921
- const t3 = STRINGS[ctx.store.get().locale];
922
- return /* @__PURE__ */ a2(
923
- "div",
924
- {
925
- class: item.visible ? "fnc-ind" : "fnc-ind fnc-ind-hidden",
926
- role: "button",
927
- tabIndex: 0,
928
- "aria-label": `${item.title} ${t3.indicatorSettings}`,
929
- onClick: (e3) => {
930
- if (e3.target.closest(".fnc-ind-actions")) return;
931
- ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } });
932
- },
933
- onKeyDown: (e3) => e3.key === "Enter" && ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
934
- children: [
935
- /* @__PURE__ */ a2(
936
- "button",
937
- {
938
- class: "fnc-ind-btn fnc-ind-eye",
939
- title: item.visible ? t3.hide : t3.show,
940
- "aria-label": `${item.title} ${item.visible ? t3.hide : t3.show}`,
941
- "aria-pressed": !item.visible,
942
- onClick: (e3) => {
943
- e3.stopPropagation();
944
- ctx.chart.setIndicatorVisible(item.id, !item.visible);
945
- },
946
- children: item.visible ? /* @__PURE__ */ a2(EyeIcon, {}) : /* @__PURE__ */ a2(EyeOffIcon, {})
947
- }
948
- ),
949
- /* @__PURE__ */ a2("span", { class: "fnc-ind-title", children: item.title }),
950
- item.visible && item.values.map((v3) => /* @__PURE__ */ a2("b", { style: { color: v3.color }, title: v3.label, children: v3.value })),
951
- /* @__PURE__ */ a2("span", { class: "fnc-ind-actions", children: [
952
- /* @__PURE__ */ a2(
953
- "button",
954
- {
955
- class: "fnc-ind-btn",
956
- title: t3.indicatorSettings,
957
- "aria-label": `${item.title} ${t3.indicatorSettings}`,
958
- onClick: () => ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
959
- children: /* @__PURE__ */ a2(GearIcon, {})
960
- }
961
- ),
962
- /* @__PURE__ */ a2(
963
- "button",
964
- {
965
- class: "fnc-ind-btn",
966
- title: t3.removeIndicator,
967
- "aria-label": `${item.title} ${t3.removeIndicator}`,
968
- onClick: () => ctx.chart.removeIndicator(item.id),
969
- children: /* @__PURE__ */ a2(SmallCloseIcon, {})
970
- }
971
- )
972
- ] })
973
- ]
974
- }
975
- );
976
- }
977
- function Legend({ ctx }) {
978
- const s3 = useStore(ctx.store);
979
- const t3 = STRINGS[s3.locale];
980
- const [hover, setHover] = d2(null);
981
- const [, refresh] = _2((n2) => n2 + 1, 0);
982
- const [collapsed, setCollapsed] = d2(false);
983
- A2(() => {
984
- const offs = [
985
- ctx.chart.on(
986
- "crosshairMove",
987
- (info) => setHover(info ? { bar: info.bar, prev: ctx.chart.bars[info.index - 1], index: info.index } : null)
988
- ),
989
- ctx.chart.on("dataChange", () => refresh(void 0))
990
- ];
991
- return () => offs.forEach((off) => off());
992
- }, [ctx.chart]);
993
- const ref = T2(null);
994
- A2(() => {
995
- const el = ref.current;
996
- if (!el) return;
997
- const update = () => ctx.chart.setTopInset(el.offsetTop + el.offsetHeight + 4);
998
- const ro = new ResizeObserver(update);
999
- ro.observe(el);
1000
- return () => ro.disconnect();
1001
- }, [ctx.chart]);
1002
- if (!s3.symbol) return /* @__PURE__ */ a2("div", { class: "fnc-legend", ref });
1003
- const bars = ctx.chart.bars;
1004
- const bar = hover?.bar ?? bars[bars.length - 1];
1005
- const prev = hover ? hover.prev : bars[bars.length - 2];
1006
- const fmt = (p3) => formatPrice(p3, s3.symbol.pricePrecision);
1007
- let values = null;
1008
- if (bar) {
1009
- const cls = dirClass(bar.close - bar.open);
1010
- const change = prev ? bar.close - prev.close : 0;
1011
- const pct = prev && prev.close ? change / prev.close * 100 : 0;
1012
- const sign = change > 0 ? "+" : "";
1013
- values = /* @__PURE__ */ a2("div", { class: "fnc-legend-values", children: [
1014
- /* @__PURE__ */ a2("span", { children: [
1015
- t3.open,
1016
- " ",
1017
- /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.open) })
1018
- ] }),
1019
- /* @__PURE__ */ a2("span", { children: [
1020
- t3.high,
1021
- " ",
1022
- /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.high) })
1023
- ] }),
1024
- /* @__PURE__ */ a2("span", { children: [
1025
- t3.low,
1026
- " ",
1027
- /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.low) })
1028
- ] }),
1029
- /* @__PURE__ */ a2("span", { children: [
1030
- t3.close_,
1031
- " ",
1032
- /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.close) })
1033
- ] }),
1034
- /* @__PURE__ */ a2("b", { class: dirClass(change), children: [
1035
- sign,
1036
- fmt(change),
1037
- " (",
1038
- sign,
1039
- pct.toFixed(2),
1040
- "%)"
1041
- ] }),
1042
- bar.volume !== void 0 && /* @__PURE__ */ a2("span", { children: [
1043
- t3.volume,
1044
- " ",
1045
- /* @__PURE__ */ a2("b", { class: "fnc-flat", children: formatCompact(bar.volume, s3.locale) })
1046
- ] })
1047
- ] });
1048
- }
1049
- const items = s3.indicators.length ? ctx.chart.indicatorLegend(hover?.index) : [];
1050
- const overlayItems = items.filter((i3) => i3.overlay);
1051
- const paneLegends = s3.layout.panes.filter((p3) => p3.kind === "indicator").map((pane) => ({ pane, item: items.find((i3) => i3.id === pane.id) }));
1052
- return /* @__PURE__ */ a2($, { children: [
1053
- /* @__PURE__ */ a2("div", { class: "fnc-legend", ref, children: [
1054
- /* @__PURE__ */ a2("div", { class: "fnc-legend-title", children: [
1055
- ctx.showLogo && /* @__PURE__ */ a2(SymbolAvatar, { info: s3.symbol, size: 18 }),
1056
- /* @__PURE__ */ a2("b", { children: s3.symbol.name }),
1057
- /* @__PURE__ */ a2("span", { children: "\xB7" }),
1058
- /* @__PURE__ */ a2("span", { children: resolutionLabel(s3.resolution, s3.locale) }),
1059
- /* @__PURE__ */ a2("span", { children: "\xB7" }),
1060
- /* @__PURE__ */ a2("span", { children: s3.symbol.exchange }),
1061
- s3.loading && /* @__PURE__ */ a2("span", { class: "fnc-spinner", role: "status" })
1062
- ] }),
1063
- values,
1064
- !collapsed && overlayItems.map((item) => /* @__PURE__ */ a2(IndicatorItem, { ctx, item })),
1065
- overlayItems.length > 0 && (collapsed ? /* @__PURE__ */ a2("button", { class: "fnc-ind-handle", "aria-expanded": "false", "aria-label": t3.expand, title: t3.expand, onClick: () => setCollapsed(false), children: /* @__PURE__ */ a2("span", {}) }) : /* @__PURE__ */ a2("button", { class: "fnc-ind-collapse", "aria-expanded": "true", "aria-label": t3.collapse, title: t3.collapse, onClick: () => setCollapsed(true), children: /* @__PURE__ */ a2(CollapseLeftIcon, {}) }))
1066
- ] }),
1067
- paneLegends.map(
1068
- ({ pane, item }) => item && /* @__PURE__ */ a2("div", { class: "fnc-legend fnc-pane-legend", style: { top: `${pane.top + 4}px` }, children: /* @__PURE__ */ a2(IndicatorItem, { ctx, item }) })
1069
- )
1070
- ] });
1071
- }
1072
- function NavControls({ ctx }) {
1073
- const s3 = useStore(ctx.store);
1074
- const t3 = STRINGS[s3.locale];
1075
- const { plotWidth, panesHeight } = s3.layout;
1076
- if (plotWidth <= 0) return null;
1077
- const top = panesHeight - CONTROL_OFFSET;
1078
- const page = () => Math.max(1, Math.round(plotWidth / ctx.chart.timeScale.barSpacing / 3));
1079
- const button = (label, onClick, icon) => /* @__PURE__ */ a2("button", { class: "fnc-btn", title: label, "aria-label": label, onClick, children: icon });
1080
- return /* @__PURE__ */ a2($, { children: [
1081
- /* @__PURE__ */ a2("div", { class: "fnc-nav", style: { left: `${plotWidth / 2}px`, top: `${top}px` }, children: [
1082
- button(t3.zoomOut, () => ctx.chart.zoom(1 / ZOOM_STEP), /* @__PURE__ */ a2(MinusIcon, {})),
1083
- button(t3.zoomIn, () => ctx.chart.zoom(ZOOM_STEP), /* @__PURE__ */ a2(PlusIcon, {})),
1084
- button(t3.scrollBack, () => ctx.chart.scrollBars(-page()), /* @__PURE__ */ a2(ChevronLeftIcon, {})),
1085
- button(t3.scrollForward, () => ctx.chart.scrollBars(page()), /* @__PURE__ */ a2(ChevronRightIcon, {})),
1086
- button(t3.resetView, () => ctx.chart.resetView(), /* @__PURE__ */ a2(ResetIcon, {}))
1087
- ] }),
1088
- !s3.atRealtime && /* @__PURE__ */ a2(
1089
- "button",
1090
- {
1091
- class: "fnc-realtime",
1092
- style: { left: `${plotWidth - 44}px`, top: `${top}px` },
1093
- title: t3.toRealtime,
1094
- "aria-label": t3.toRealtime,
1095
- onClick: () => ctx.chart.scrollToRealtime(),
1096
- children: /* @__PURE__ */ a2(ToRealtimeIcon, {})
1097
- }
1098
- )
1099
- ] });
1100
- }
1101
- var ATTRIBUTION_URL = "https://charts.funnest.xyz/?utm_source=chart&utm_medium=attribution";
1102
- function Attribution({ ctx }) {
1103
- const s3 = useStore(ctx.store);
1104
- const { panesHeight, plotWidth } = s3.layout;
1105
- if (!ctx.attribution || plotWidth <= 0) return null;
1106
- const label = s3.locale === "ko" ? "Funnest \uCC28\uD2B8" : "Charts by Funnest";
1107
- return /* @__PURE__ */ a2(
1108
- "a",
1109
- {
1110
- class: "fnc-attribution",
1111
- href: ATTRIBUTION_URL,
1112
- target: "_blank",
1113
- rel: "noopener",
1114
- title: label,
1115
- "aria-label": label,
1116
- style: { top: `${panesHeight - 36}px` },
1117
- children: [
1118
- /* @__PURE__ */ a2(FunnestMark, {}),
1119
- /* @__PURE__ */ a2("span", { children: "funnest" })
1120
- ]
1121
- }
1122
- );
1123
- }
1124
- function Overlay({ ctx }) {
1125
- return /* @__PURE__ */ a2($, { children: [
1126
- /* @__PURE__ */ a2(Attribution, { ctx }),
1127
- /* @__PURE__ */ a2(Legend, { ctx }),
1128
- /* @__PURE__ */ a2(NavControls, { ctx }),
1129
- /* @__PURE__ */ a2(SelectionBar, { ctx })
1130
- ] });
1131
- }
1132
-
1133
912
  // src/components/chart-type-menu.tsx
1134
913
  var TYPES = [
1135
914
  { type: "candles", label: "candles", icon: CandlesIcon },
@@ -1213,7 +992,8 @@ var SECTIONS = [
1213
992
  toggles: [
1214
993
  { key: "highLowMarkers", label: "settingHighLow" },
1215
994
  { key: "lastPriceLine", label: "settingLastPrice" },
1216
- { key: "volume", label: "settingVolume" }
995
+ { key: "volume", label: "settingVolume" },
996
+ { key: "navigationButtons", label: "settingNavButtons" }
1217
997
  ]
1218
998
  },
1219
999
  {
@@ -1330,7 +1110,7 @@ function defaultSeries(type, params) {
1330
1110
  }
1331
1111
  function StyleControls({
1332
1112
  t: t3,
1333
- row,
1113
+ row: row2,
1334
1114
  onChange
1335
1115
  }) {
1336
1116
  return /* @__PURE__ */ a2($, { children: [
@@ -1339,8 +1119,8 @@ function StyleControls({
1339
1119
  {
1340
1120
  type: "checkbox",
1341
1121
  class: "fnc-check",
1342
- checked: row.visible,
1343
- "aria-label": row.visible ? t3.hide : t3.show,
1122
+ checked: row2.visible,
1123
+ "aria-label": row2.visible ? t3.hide : t3.show,
1344
1124
  onChange: (e3) => onChange({ visible: e3.currentTarget.checked })
1345
1125
  }
1346
1126
  ),
@@ -1349,12 +1129,12 @@ function StyleControls({
1349
1129
  {
1350
1130
  type: "color",
1351
1131
  class: "fnc-color",
1352
- value: row.color,
1132
+ value: row2.color,
1353
1133
  "aria-label": t3.color,
1354
1134
  onInput: (e3) => onChange({ color: e3.currentTarget.value })
1355
1135
  }
1356
1136
  ),
1357
- /* @__PURE__ */ a2("select", { class: "fnc-select fnc-select-width", value: row.width, "aria-label": t3.lineWidth, onChange: (e3) => onChange({ width: Number(e3.currentTarget.value) }), children: WIDTHS.map((w3) => /* @__PURE__ */ a2("option", { value: w3, children: [
1137
+ /* @__PURE__ */ a2("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__ */ a2("option", { value: w3, children: [
1358
1138
  w3,
1359
1139
  "px"
1360
1140
  ] })) })
@@ -1480,9 +1260,9 @@ function IndicatorSettings({ ctx, id }) {
1480
1260
  )
1481
1261
  ] })),
1482
1262
  styles.length > 0 && /* @__PURE__ */ a2("div", { class: "fnc-section", children: t3.style }),
1483
- styles.map((row, i3) => /* @__PURE__ */ a2("div", { class: "fnc-row", children: [
1484
- /* @__PURE__ */ a2("span", { class: "fnc-row-label fnc-row-label-wide", children: row.label }),
1485
- /* @__PURE__ */ a2(StyleControls, { t: t3, row, onChange: (p3) => setStyles(styles.map((s3, j2) => j2 === i3 ? { ...s3, ...p3 } : s3)) })
1263
+ styles.map((row2, i3) => /* @__PURE__ */ a2("div", { class: "fnc-row", children: [
1264
+ /* @__PURE__ */ a2("span", { class: "fnc-row-label fnc-row-label-wide", children: row2.label }),
1265
+ /* @__PURE__ */ a2(StyleControls, { t: t3, row: row2, onChange: (p3) => setStyles(styles.map((s3, j2) => j2 === i3 ? { ...s3, ...p3 } : s3)) })
1486
1266
  ] }))
1487
1267
  ] }),
1488
1268
  /* @__PURE__ */ a2("div", { class: "fnc-form-actions", children: [
@@ -1603,18 +1383,216 @@ function IntervalPicker({ ctx }) {
1603
1383
  ] });
1604
1384
  }
1605
1385
 
1606
- // src/components/slot.tsx
1607
- function Slot({ el, fixed = false }) {
1608
- const ref = T2(null);
1609
- F2(() => {
1610
- ref.current?.appendChild(el);
1611
- return () => el.remove();
1612
- }, [el]);
1613
- return /* @__PURE__ */ a2("div", { class: fixed ? "fnc-slot-host fnc-slot-host-fixed" : "fnc-slot-host", ref });
1386
+ // src/layouts.ts
1387
+ var row = (...children) => ({ dir: "row", children });
1388
+ var col = (...children) => ({ dir: "col", children });
1389
+ var LAYOUTS = {
1390
+ "1": 0,
1391
+ "2-cols": row(0, 1),
1392
+ "2-rows": col(0, 1),
1393
+ "3-cols": row(0, 1, 2),
1394
+ "3-rows": col(0, 1, 2),
1395
+ "3-left": row(0, col(1, 2)),
1396
+ "3-top": col(0, row(1, 2)),
1397
+ "4-grid": col(row(0, 1), row(2, 3)),
1398
+ "4-cols": row(0, 1, 2, 3),
1399
+ "4-left": row(0, col(1, 2, 3)),
1400
+ "6-grid": col(row(0, 1, 2), row(3, 4, 5)),
1401
+ "8-grid": col(row(0, 1, 2, 3), row(4, 5, 6, 7))
1402
+ };
1403
+ var LAYOUT_GROUPS = [
1404
+ ["1"],
1405
+ ["2-cols", "2-rows"],
1406
+ ["3-cols", "3-rows", "3-left", "3-top"],
1407
+ ["4-grid", "4-cols", "4-left"],
1408
+ ["6-grid", "8-grid"]
1409
+ ];
1410
+ var MAX_PANES = 8;
1411
+ function isLayoutId(value) {
1412
+ return typeof value === "string" && value in LAYOUTS;
1413
+ }
1414
+ function paneCount(node) {
1415
+ if (typeof node === "number") return 1;
1416
+ let n2 = 0;
1417
+ for (const child of node.children) n2 += paneCount(child);
1418
+ return n2;
1419
+ }
1420
+ function sizesFor(sizes, key, count) {
1421
+ const s3 = sizes[key];
1422
+ return s3 && s3.length === count && s3.every((v3) => Number.isFinite(v3) && v3 > 0) ? s3 : Array(count).fill(1);
1423
+ }
1424
+ function resizePair(weights, i3, deltaFraction, min = 0.12) {
1425
+ const total = weights.reduce((a5, b2) => a5 + b2, 0);
1426
+ const a4 = weights[i3];
1427
+ const b = weights[i3 + 1];
1428
+ const pair = a4 + b;
1429
+ const delta = deltaFraction * total;
1430
+ const nextA = Math.min(pair * (1 - min), Math.max(pair * min, a4 + delta));
1431
+ const next = [...weights];
1432
+ next[i3] = nextA;
1433
+ next[i3 + 1] = pair - nextA;
1434
+ return next;
1435
+ }
1436
+ function layoutRects(node, x2 = 0, y3 = 0, w3 = 20, h3 = 16, gap = 1.6) {
1437
+ if (typeof node === "number") return [{ x: x2, y: y3, w: w3, h: h3 }];
1438
+ const n2 = node.children.length;
1439
+ const out = [];
1440
+ node.children.forEach((child, i3) => {
1441
+ if (node.dir === "row") {
1442
+ const cw = (w3 - gap * (n2 - 1)) / n2;
1443
+ out.push(...layoutRects(child, x2 + i3 * (cw + gap), y3, cw, h3, gap));
1444
+ } else {
1445
+ const ch = (h3 - gap * (n2 - 1)) / n2;
1446
+ out.push(...layoutRects(child, x2, y3 + i3 * (ch + gap), w3, ch, gap));
1447
+ }
1448
+ });
1449
+ return out;
1450
+ }
1451
+
1452
+ // src/components/layout-menu.tsx
1453
+ var LAYOUT_PANES = (id) => paneCount(LAYOUTS[id]);
1454
+ function LayoutGlyph({ id }) {
1455
+ return /* @__PURE__ */ a2("svg", { width: "20", height: "16", viewBox: "0 0 20 16", "aria-hidden": "true", children: layoutRects(LAYOUTS[id]).map((r3) => /* @__PURE__ */ a2("rect", { x: r3.x + 0.5, y: r3.y + 0.5, width: Math.max(0, r3.w - 1), height: Math.max(0, r3.h - 1), rx: "1.5", fill: "none", stroke: "currentColor", "stroke-width": "1.2" })) });
1456
+ }
1457
+ function usePopover() {
1458
+ const [open, setOpen] = d2(false);
1459
+ const ref = T2(null);
1460
+ A2(() => {
1461
+ if (!open) return;
1462
+ const onDown = (e3) => !ref.current?.contains(e3.target) && setOpen(false);
1463
+ const onKey = (e3) => e3.key === "Escape" && setOpen(false);
1464
+ document.addEventListener("pointerdown", onDown);
1465
+ document.addEventListener("keydown", onKey);
1466
+ return () => {
1467
+ document.removeEventListener("pointerdown", onDown);
1468
+ document.removeEventListener("keydown", onKey);
1469
+ };
1470
+ }, [open]);
1471
+ return { open, setOpen, ref };
1472
+ }
1473
+ var SYNC_KEYS = [
1474
+ { key: "crosshair", label: "syncCrosshair" },
1475
+ { key: "time", label: "syncTime" },
1476
+ { key: "symbol", label: "syncSymbol" },
1477
+ { key: "interval", label: "syncInterval" }
1478
+ ];
1479
+ function LayoutMenu({ ctx, multi }) {
1480
+ const s3 = useStore(ctx.store);
1481
+ const m3 = useStore(multi.store);
1482
+ const t3 = STRINGS[s3.locale];
1483
+ const layout = usePopover();
1484
+ const sync = usePopover();
1485
+ const anySync = Object.values(m3.sync).some(Boolean);
1486
+ return /* @__PURE__ */ a2($, { children: [
1487
+ /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: layout.ref, children: [
1488
+ /* @__PURE__ */ a2(
1489
+ "button",
1490
+ {
1491
+ class: "fnc-btn fnc-btn-wide fnc-btn-layout",
1492
+ title: t3.layout,
1493
+ "aria-label": t3.layout,
1494
+ "aria-haspopup": "menu",
1495
+ "aria-expanded": layout.open,
1496
+ onClick: () => layout.setOpen(!layout.open),
1497
+ children: [
1498
+ /* @__PURE__ */ a2(LayoutGlyph, { id: m3.layout }),
1499
+ /* @__PURE__ */ a2(ChevronDownIcon, {})
1500
+ ]
1501
+ }
1502
+ ),
1503
+ layout.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-layout-menu", role: "menu", "aria-label": t3.layout, children: LAYOUT_GROUPS.map((group) => /* @__PURE__ */ a2("div", { class: "fnc-layout-row", children: [
1504
+ /* @__PURE__ */ a2("span", { class: "fnc-layout-count", children: group[0].split("-")[0] }),
1505
+ group.map((id) => /* @__PURE__ */ a2(
1506
+ "button",
1507
+ {
1508
+ class: "fnc-layout-opt",
1509
+ role: "menuitemradio",
1510
+ "aria-checked": id === m3.layout,
1511
+ "aria-label": `${t3.layout} ${id}`,
1512
+ title: id,
1513
+ onClick: () => {
1514
+ multi.setLayout(id);
1515
+ layout.setOpen(false);
1516
+ },
1517
+ children: /* @__PURE__ */ a2(LayoutGlyph, { id })
1518
+ }
1519
+ ))
1520
+ ] })) })
1521
+ ] }),
1522
+ LAYOUT_PANES(m3.layout) > 1 && /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: sync.ref, children: [
1523
+ /* @__PURE__ */ a2(
1524
+ "button",
1525
+ {
1526
+ class: "fnc-btn fnc-btn-sync",
1527
+ title: t3.sync,
1528
+ "aria-label": t3.sync,
1529
+ "aria-haspopup": "menu",
1530
+ "aria-expanded": sync.open,
1531
+ "aria-pressed": anySync,
1532
+ onClick: () => sync.setOpen(!sync.open),
1533
+ children: /* @__PURE__ */ a2(LinkIcon, {})
1534
+ }
1535
+ ),
1536
+ sync.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-sync-menu", role: "menu", "aria-label": t3.sync, children: [
1537
+ /* @__PURE__ */ a2("div", { class: "fnc-menu-title", children: [
1538
+ t3.sync,
1539
+ /* @__PURE__ */ a2("small", { children: t3.syncHint })
1540
+ ] }),
1541
+ SYNC_KEYS.map(({ key, label }) => /* @__PURE__ */ a2(
1542
+ "button",
1543
+ {
1544
+ class: "fnc-menu-item fnc-sync-item",
1545
+ role: "menuitemcheckbox",
1546
+ "aria-checked": m3.sync[key],
1547
+ onClick: () => multi.setSync({ [key]: !m3.sync[key] }),
1548
+ children: [
1549
+ /* @__PURE__ */ a2("span", { children: t3[label] }),
1550
+ /* @__PURE__ */ a2("span", { class: "fnc-switch", "data-on": m3.sync[key] ? "true" : "false", "aria-hidden": "true" })
1551
+ ]
1552
+ }
1553
+ ))
1554
+ ] })
1555
+ ] })
1556
+ ] });
1557
+ }
1558
+
1559
+ // src/components/slot.tsx
1560
+ function Slot({ el, fixed = false }) {
1561
+ const ref = T2(null);
1562
+ F2(() => {
1563
+ ref.current?.appendChild(el);
1564
+ return () => el.remove();
1565
+ }, [el]);
1566
+ return /* @__PURE__ */ a2("div", { class: fixed ? "fnc-slot-host fnc-slot-host-fixed" : "fnc-slot-host", ref });
1567
+ }
1568
+
1569
+ // src/components/symbol-avatar.tsx
1570
+ function hue(text) {
1571
+ let h3 = 0;
1572
+ for (const ch of text) h3 = (h3 * 31 + ch.charCodeAt(0)) % 360;
1573
+ return h3;
1574
+ }
1575
+ function SymbolAvatar({ info, size = 24 }) {
1576
+ const [failed, setFailed] = d2(false);
1577
+ A2(() => setFailed(false), [info.logoUrl]);
1578
+ const style = { width: `${size}px`, height: `${size}px`, fontSize: `${Math.round(size * 0.46)}px` };
1579
+ if (info.logoUrl && !failed) {
1580
+ return /* @__PURE__ */ a2("img", { class: "fnc-avatar", src: info.logoUrl, alt: "", style, onError: () => setFailed(true) });
1581
+ }
1582
+ const h3 = hue(info.symbol);
1583
+ return /* @__PURE__ */ a2(
1584
+ "span",
1585
+ {
1586
+ class: "fnc-avatar fnc-avatar-initial",
1587
+ "aria-hidden": "true",
1588
+ style: { ...style, background: `hsl(${h3} 70% 50% / 0.16)`, color: `hsl(${h3} 60% 42%)` },
1589
+ children: (info.name || info.symbol).trim().charAt(0).toUpperCase()
1590
+ }
1591
+ );
1614
1592
  }
1615
1593
 
1616
1594
  // src/components/symbol-search.tsx
1617
- import { formatPrice as formatPrice2 } from "@funnest/charts";
1595
+ import { formatPrice } from "@funnest/charts";
1618
1596
  var DEBOUNCE_MS = 150;
1619
1597
  var RECENT_KEY = "recent-symbols";
1620
1598
  var FAVORITE_KEY = "favorite-symbols";
@@ -1712,7 +1690,7 @@ function SymbolSearch({ ctx }) {
1712
1690
  else return;
1713
1691
  e3.preventDefault();
1714
1692
  };
1715
- const row = (info, index, isRecent) => {
1693
+ const row2 = (info, index, isRecent) => {
1716
1694
  const cat = categoryOf(info);
1717
1695
  const fav = isFavorite(info);
1718
1696
  const change = info.changePercent ?? 0;
@@ -1757,7 +1735,7 @@ function SymbolSearch({ ctx }) {
1757
1735
  ] }),
1758
1736
  info.sparkline && /* @__PURE__ */ a2(Sparkline, { values: info.sparkline, up }),
1759
1737
  info.price !== void 0 && /* @__PURE__ */ a2("span", { class: "fnc-sr-quote", children: [
1760
- /* @__PURE__ */ a2("b", { children: formatPrice2(info.price, info.pricePrecision) }),
1738
+ /* @__PURE__ */ a2("b", { children: formatPrice(info.price, info.pricePrecision) }),
1761
1739
  info.changePercent !== void 0 && /* @__PURE__ */ a2("span", { class: trend, children: [
1762
1740
  change > 0 ? "\u2191 " : change < 0 ? "\u2193 " : "",
1763
1741
  Math.abs(info.changePercent).toFixed(2),
@@ -1807,7 +1785,7 @@ function SymbolSearch({ ctx }) {
1807
1785
  }
1808
1786
  ),
1809
1787
  query && /* @__PURE__ */ a2("button", { class: "fnc-ind-btn", "aria-label": t3.cancel, onClick: () => (setQuery(""), inputRef.current?.focus()), children: /* @__PURE__ */ a2(SmallCloseIcon, {}) }),
1810
- /* @__PURE__ */ a2("kbd", { class: "fnc-kbd fnc-hide-mobile", children: "ESC" })
1788
+ /* @__PURE__ */ a2("button", { class: "fnc-kbd fnc-kbd-btn fnc-hide-mobile", "aria-label": t3.close, title: t3.close, onClick: close, children: "ESC" })
1811
1789
  ] }),
1812
1790
  /* @__PURE__ */ a2("div", { class: "fnc-tabs", role: "tablist", children: ctx.searchCategories.map((c3) => /* @__PURE__ */ a2("button", { class: "fnc-tab", role: "tab", "aria-selected": c3.id === category, onClick: () => setCategory(c3.id), children: c3.label[locale] })) }),
1813
1791
  /* @__PURE__ */ a2("div", { class: "fnc-sr-body", children: [
@@ -1825,9 +1803,9 @@ function SymbolSearch({ ctx }) {
1825
1803
  /* @__PURE__ */ a2("span", { children: t3.recentSearches }),
1826
1804
  /* @__PURE__ */ a2("button", { class: "fnc-link", onClick: () => save(RECENT_KEY, [], setRecent), children: t3.clearRecent })
1827
1805
  ] }),
1828
- recentRows.map((info, i3) => row(info, i3, true)),
1806
+ recentRows.map((info, i3) => row2(info, i3, true)),
1829
1807
  (recentRows.length > 0 || view.kind === "favorites") && /* @__PURE__ */ a2("div", { class: "fnc-sr-section", children: view.kind === "favorites" ? t3.favorites : t3.symbols }),
1830
- loading && !results && view.kind === "all" ? Array.from({ length: 6 }, () => /* @__PURE__ */ a2("div", { class: "fnc-sr-skeleton", "aria-hidden": "true" })) : resultRows.map((info, i3) => row(info, recentRows.length + i3, false)),
1808
+ loading && !results && view.kind === "all" ? Array.from({ length: 6 }, () => /* @__PURE__ */ a2("div", { class: "fnc-sr-skeleton", "aria-hidden": "true" })) : resultRows.map((info, i3) => row2(info, recentRows.length + i3, false)),
1831
1809
  !loading && rows.length === 0 && /* @__PURE__ */ a2("div", { class: "fnc-empty", children: view.kind === "favorites" && favorites.length === 0 ? t3.noFavorites : t3.noResults })
1832
1810
  ] })
1833
1811
  ] }),
@@ -1842,7 +1820,7 @@ function SymbolSearch({ ctx }) {
1842
1820
  " ",
1843
1821
  t3.openAction
1844
1822
  ] }),
1845
- /* @__PURE__ */ a2("span", { children: [
1823
+ /* @__PURE__ */ a2("button", { class: "fnc-sr-foot-btn", onClick: close, children: [
1846
1824
  /* @__PURE__ */ a2("kbd", { class: "fnc-kbd", children: "ESC" }),
1847
1825
  " ",
1848
1826
  t3.close
@@ -1884,23 +1862,26 @@ function Toolbar({ ctx }) {
1884
1862
  /* @__PURE__ */ a2("div", { class: "fnc-sep" }),
1885
1863
  /* @__PURE__ */ a2(IntervalPicker, { ctx }),
1886
1864
  /* @__PURE__ */ a2(Slot, { el: ctx.slots.toolbar }),
1887
- /* @__PURE__ */ a2("div", { class: "fnc-sep" }),
1888
- /* @__PURE__ */ a2(ChartTypeMenu, { ctx }),
1889
- /* @__PURE__ */ a2(
1890
- "button",
1891
- {
1892
- class: "fnc-btn fnc-btn-wide fnc-btn-label",
1893
- title: t3.addIndicator,
1894
- "aria-label": t3.addIndicator,
1895
- "aria-haspopup": "dialog",
1896
- onClick: () => ctx.store.set({ dialog: { kind: "indicators" } }),
1897
- children: [
1898
- /* @__PURE__ */ a2(IndicatorIcon, {}),
1899
- /* @__PURE__ */ a2("span", { children: t3.indicators })
1900
- ]
1901
- }
1902
- ),
1903
1865
  /* @__PURE__ */ a2("div", { class: "fnc-spacer" }),
1866
+ /* @__PURE__ */ a2("div", { class: "fnc-tools", children: [
1867
+ /* @__PURE__ */ a2(ChartTypeMenu, { ctx }),
1868
+ /* @__PURE__ */ a2(
1869
+ "button",
1870
+ {
1871
+ class: "fnc-btn fnc-btn-wide fnc-btn-label",
1872
+ title: t3.addIndicator,
1873
+ "aria-label": t3.addIndicator,
1874
+ "aria-haspopup": "dialog",
1875
+ onClick: () => ctx.store.set({ dialog: { kind: "indicators" } }),
1876
+ children: [
1877
+ /* @__PURE__ */ a2(IndicatorIcon, {}),
1878
+ /* @__PURE__ */ a2("span", { children: t3.indicators })
1879
+ ]
1880
+ }
1881
+ ),
1882
+ ctx.multi && /* @__PURE__ */ a2(LayoutMenu, { ctx, multi: ctx.multi })
1883
+ ] }),
1884
+ /* @__PURE__ */ a2("div", { class: "fnc-sep" }),
1904
1885
  ctx.drawingToolbar && /* @__PURE__ */ a2(
1905
1886
  "button",
1906
1887
  {
@@ -1940,109 +1921,362 @@ function Toolbar({ ctx }) {
1940
1921
  ] });
1941
1922
  }
1942
1923
 
1943
- // src/storage.ts
1944
- function createStorage(prefix) {
1945
- const memory = /* @__PURE__ */ new Map();
1946
- const ls = (() => {
1947
- try {
1948
- return prefix && typeof localStorage !== "undefined" ? localStorage : null;
1949
- } catch {
1950
- return null;
1951
- }
1952
- })();
1953
- const full = (key) => `${prefix}:${key}`;
1954
- return {
1955
- get(key, fallback) {
1956
- try {
1957
- const raw = ls ? ls.getItem(full(key)) : memory.get(key) ?? null;
1958
- return raw === null ? fallback : JSON.parse(raw);
1959
- } catch {
1960
- return fallback;
1961
- }
1962
- },
1963
- set(key, value) {
1964
- const raw = JSON.stringify(value);
1965
- try {
1966
- if (ls) ls.setItem(full(key), raw);
1967
- else memory.set(key, raw);
1968
- } catch {
1969
- memory.set(key, raw);
1970
- }
1971
- }
1972
- };
1973
- }
1974
-
1975
- // src/styles.ts
1976
- var CSS = (
1977
- /* css */
1978
- `
1979
- .fnc-root {
1980
- position: relative;
1981
- display: flex;
1982
- flex-direction: column;
1983
- width: 100%;
1984
- height: 100%;
1985
- min-height: 0;
1986
- background: var(--fnc-bg);
1987
- color: var(--fnc-text);
1988
- font-family: var(--fnc-font);
1989
- font-size: 14px;
1990
- line-height: 1.4;
1991
- -webkit-font-smoothing: antialiased;
1992
- overflow: hidden;
1993
- }
1994
- .fnc-root *, .fnc-root *::before, .fnc-root *::after { box-sizing: border-box; }
1995
- .fnc-root.fnc-pseudo-fullscreen { position: fixed; inset: 0; z-index: 2147483000; }
1996
- :where(.fnc-root) button {
1997
- font: inherit;
1998
- color: inherit;
1999
- background: none;
2000
- border: 0;
2001
- margin: 0;
2002
- padding: 0;
2003
- cursor: pointer;
2004
- -webkit-tap-highlight-color: transparent;
2005
- }
2006
- .fnc-root button:focus-visible { outline: 2px solid var(--fnc-accent); outline-offset: 1px; }
1924
+ // src/pane.tsx
1925
+ import { createChart } from "@funnest/charts";
2007
1926
 
2008
- /* \u2500\u2500 Toolbar \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2009
- .fnc-toolbar {
2010
- display: flex;
2011
- align-items: center;
2012
- gap: 4px;
2013
- height: 48px;
2014
- padding: 0 8px;
2015
- border-bottom: 1px solid var(--fnc-border);
2016
- flex: none;
2017
- }
2018
- .fnc-symbol {
2019
- display: flex;
2020
- align-items: center;
2021
- gap: 6px;
2022
- height: 36px;
2023
- max-width: 220px;
2024
- padding: 0 10px 0 8px;
2025
- border-radius: var(--fnc-radius);
2026
- font-weight: 700;
2027
- font-size: 15px;
2028
- /* The symbol gives way first when the toolbar is tight: its name truncates, never overlaps. */
2029
- flex: 0 1 auto;
2030
- min-width: 36px;
2031
- overflow: hidden;
2032
- }
2033
- .fnc-symbol .fnc-icon { color: var(--fnc-text-2); display: flex; }
2034
- .fnc-symbol-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2035
- .fnc-symbol:not(:disabled):hover, .fnc-btn:hover, .fnc-chip:hover { background: var(--fnc-surface); }
2036
- .fnc-symbol:disabled { cursor: default; }
2037
- .fnc-sep { width: 1px; height: 20px; background: var(--fnc-border); margin: 0 4px; flex: none; }
2038
- .fnc-intervals {
2039
- display: flex;
2040
- gap: 2px;
2041
- overflow-x: auto;
2042
- scrollbar-width: none;
2043
- min-width: 0;
1927
+ // src/components/overlay.tsx
1928
+ import { formatCompact, formatPrice as formatPrice2 } from "@funnest/charts";
1929
+ var ZOOM_STEP = 1.25;
1930
+ var CONTROL_OFFSET = 52;
1931
+ var dirClass = (v3) => v3 > 0 ? "fnc-up" : v3 < 0 ? "fnc-down" : "fnc-flat";
1932
+ function IndicatorItem({ ctx, item }) {
1933
+ const t3 = STRINGS[ctx.store.get().locale];
1934
+ return /* @__PURE__ */ a2(
1935
+ "div",
1936
+ {
1937
+ class: item.visible ? "fnc-ind" : "fnc-ind fnc-ind-hidden",
1938
+ role: "button",
1939
+ tabIndex: 0,
1940
+ "aria-label": `${item.title} ${t3.indicatorSettings}`,
1941
+ onClick: (e3) => {
1942
+ if (e3.target.closest(".fnc-ind-actions")) return;
1943
+ ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } });
1944
+ },
1945
+ onKeyDown: (e3) => e3.key === "Enter" && ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
1946
+ children: [
1947
+ /* @__PURE__ */ a2(
1948
+ "button",
1949
+ {
1950
+ class: "fnc-ind-btn fnc-ind-eye",
1951
+ title: item.visible ? t3.hide : t3.show,
1952
+ "aria-label": `${item.title} ${item.visible ? t3.hide : t3.show}`,
1953
+ "aria-pressed": !item.visible,
1954
+ onClick: (e3) => {
1955
+ e3.stopPropagation();
1956
+ ctx.chart.setIndicatorVisible(item.id, !item.visible);
1957
+ },
1958
+ children: item.visible ? /* @__PURE__ */ a2(EyeIcon, {}) : /* @__PURE__ */ a2(EyeOffIcon, {})
1959
+ }
1960
+ ),
1961
+ /* @__PURE__ */ a2("span", { class: "fnc-ind-title", children: item.title }),
1962
+ item.visible && item.values.map((v3) => /* @__PURE__ */ a2("b", { style: { color: v3.color }, title: v3.label, children: v3.value })),
1963
+ /* @__PURE__ */ a2("span", { class: "fnc-ind-actions", children: [
1964
+ /* @__PURE__ */ a2(
1965
+ "button",
1966
+ {
1967
+ class: "fnc-ind-btn",
1968
+ title: t3.indicatorSettings,
1969
+ "aria-label": `${item.title} ${t3.indicatorSettings}`,
1970
+ onClick: () => ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
1971
+ children: /* @__PURE__ */ a2(GearIcon, {})
1972
+ }
1973
+ ),
1974
+ /* @__PURE__ */ a2(
1975
+ "button",
1976
+ {
1977
+ class: "fnc-ind-btn",
1978
+ title: t3.removeIndicator,
1979
+ "aria-label": `${item.title} ${t3.removeIndicator}`,
1980
+ onClick: () => ctx.chart.removeIndicator(item.id),
1981
+ children: /* @__PURE__ */ a2(SmallCloseIcon, {})
1982
+ }
1983
+ )
1984
+ ] })
1985
+ ]
1986
+ }
1987
+ );
2044
1988
  }
2045
- .fnc-intervals::-webkit-scrollbar { display: none; }
1989
+ function Legend({ ctx }) {
1990
+ const s3 = useStore(ctx.store);
1991
+ const t3 = STRINGS[s3.locale];
1992
+ const [hover, setHover] = d2(null);
1993
+ const [, refresh] = _2((n2) => n2 + 1, 0);
1994
+ const [collapsed, setCollapsed] = d2(false);
1995
+ A2(() => {
1996
+ const offs = [
1997
+ ctx.chart.on(
1998
+ "crosshairMove",
1999
+ (info) => setHover(info ? { bar: info.bar, prev: ctx.chart.bars[info.index - 1], index: info.index } : null)
2000
+ ),
2001
+ ctx.chart.on("dataChange", () => refresh(void 0))
2002
+ ];
2003
+ return () => offs.forEach((off) => off());
2004
+ }, [ctx.chart]);
2005
+ const ref = T2(null);
2006
+ A2(() => {
2007
+ const el = ref.current;
2008
+ if (!el) return;
2009
+ const update = () => ctx.chart.setTopInset(el.offsetTop + el.offsetHeight + 4);
2010
+ const ro = new ResizeObserver(update);
2011
+ ro.observe(el);
2012
+ return () => ro.disconnect();
2013
+ }, [ctx.chart]);
2014
+ if (!s3.symbol) return /* @__PURE__ */ a2("div", { class: "fnc-legend", ref });
2015
+ const bars = ctx.chart.bars;
2016
+ const bar = hover?.bar ?? bars[bars.length - 1];
2017
+ const prev = hover ? hover.prev : bars[bars.length - 2];
2018
+ const fmt = (p3) => formatPrice2(p3, s3.symbol.pricePrecision);
2019
+ let values = null;
2020
+ if (bar) {
2021
+ const cls = dirClass(bar.close - bar.open);
2022
+ const change = prev ? bar.close - prev.close : 0;
2023
+ const pct = prev && prev.close ? change / prev.close * 100 : 0;
2024
+ const sign = change > 0 ? "+" : "";
2025
+ values = /* @__PURE__ */ a2("div", { class: "fnc-legend-values", children: [
2026
+ /* @__PURE__ */ a2("span", { children: [
2027
+ t3.open,
2028
+ " ",
2029
+ /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.open) })
2030
+ ] }),
2031
+ /* @__PURE__ */ a2("span", { children: [
2032
+ t3.high,
2033
+ " ",
2034
+ /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.high) })
2035
+ ] }),
2036
+ /* @__PURE__ */ a2("span", { children: [
2037
+ t3.low,
2038
+ " ",
2039
+ /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.low) })
2040
+ ] }),
2041
+ /* @__PURE__ */ a2("span", { children: [
2042
+ t3.close_,
2043
+ " ",
2044
+ /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.close) })
2045
+ ] }),
2046
+ /* @__PURE__ */ a2("b", { class: dirClass(change), children: [
2047
+ sign,
2048
+ fmt(change),
2049
+ " (",
2050
+ sign,
2051
+ pct.toFixed(2),
2052
+ "%)"
2053
+ ] }),
2054
+ bar.volume !== void 0 && /* @__PURE__ */ a2("span", { children: [
2055
+ t3.volume,
2056
+ " ",
2057
+ /* @__PURE__ */ a2("b", { class: "fnc-flat", children: formatCompact(bar.volume, s3.locale) })
2058
+ ] })
2059
+ ] });
2060
+ }
2061
+ const items = s3.indicators.length ? ctx.chart.indicatorLegend(hover?.index) : [];
2062
+ const overlayItems = items.filter((i3) => i3.overlay);
2063
+ const paneLegends = s3.layout.panes.filter((p3) => p3.kind === "indicator").map((pane) => ({ pane, item: items.find((i3) => i3.id === pane.id) }));
2064
+ return /* @__PURE__ */ a2($, { children: [
2065
+ /* @__PURE__ */ a2("div", { class: "fnc-legend", ref, children: [
2066
+ /* @__PURE__ */ a2("div", { class: "fnc-legend-title", children: [
2067
+ ctx.showLogo && /* @__PURE__ */ a2(SymbolAvatar, { info: s3.symbol, size: 18 }),
2068
+ /* @__PURE__ */ a2("b", { children: s3.symbol.name }),
2069
+ /* @__PURE__ */ a2("span", { children: "\xB7" }),
2070
+ /* @__PURE__ */ a2("span", { children: resolutionLabel(s3.resolution, s3.locale) }),
2071
+ /* @__PURE__ */ a2("span", { children: "\xB7" }),
2072
+ /* @__PURE__ */ a2("span", { children: s3.symbol.exchange }),
2073
+ s3.loading && /* @__PURE__ */ a2("span", { class: "fnc-spinner", role: "status" })
2074
+ ] }),
2075
+ values,
2076
+ !collapsed && overlayItems.map((item) => /* @__PURE__ */ a2(IndicatorItem, { ctx, item })),
2077
+ overlayItems.length > 0 && (collapsed ? /* @__PURE__ */ a2("button", { class: "fnc-ind-handle", "aria-expanded": "false", "aria-label": t3.expand, title: t3.expand, onClick: () => setCollapsed(false), children: /* @__PURE__ */ a2("span", {}) }) : /* @__PURE__ */ a2("button", { class: "fnc-ind-collapse", "aria-expanded": "true", "aria-label": t3.collapse, title: t3.collapse, onClick: () => setCollapsed(true), children: /* @__PURE__ */ a2(CollapseLeftIcon, {}) }))
2078
+ ] }),
2079
+ paneLegends.map(
2080
+ ({ pane, item }) => item && /* @__PURE__ */ a2("div", { class: "fnc-legend fnc-pane-legend", style: { top: `${pane.top + 4}px` }, children: /* @__PURE__ */ a2(IndicatorItem, { ctx, item }) })
2081
+ )
2082
+ ] });
2083
+ }
2084
+ function NavControls({ ctx }) {
2085
+ const s3 = useStore(ctx.store);
2086
+ const t3 = STRINGS[s3.locale];
2087
+ const [showButtons, setShowButtons] = d2(ctx.chart.settings.navigationButtons);
2088
+ A2(() => ctx.chart.on("settingsChange", (settings) => setShowButtons(settings.navigationButtons)), [ctx.chart]);
2089
+ const { plotWidth, panesHeight } = s3.layout;
2090
+ if (plotWidth <= 0) return null;
2091
+ const top = panesHeight - CONTROL_OFFSET;
2092
+ const page = () => Math.max(1, Math.round(plotWidth / ctx.chart.timeScale.barSpacing / 3));
2093
+ const button = (label, onClick, icon) => /* @__PURE__ */ a2("button", { class: "fnc-btn", title: label, "aria-label": label, onClick, children: icon });
2094
+ return /* @__PURE__ */ a2($, { children: [
2095
+ showButtons && /* @__PURE__ */ a2("div", { class: "fnc-nav", style: { left: `${plotWidth / 2}px`, top: `${top}px` }, children: [
2096
+ button(t3.zoomOut, () => ctx.chart.zoom(1 / ZOOM_STEP), /* @__PURE__ */ a2(MinusIcon, {})),
2097
+ button(t3.zoomIn, () => ctx.chart.zoom(ZOOM_STEP), /* @__PURE__ */ a2(PlusIcon, {})),
2098
+ button(t3.scrollBack, () => ctx.chart.scrollBars(-page()), /* @__PURE__ */ a2(ChevronLeftIcon, {})),
2099
+ button(t3.scrollForward, () => ctx.chart.scrollBars(page()), /* @__PURE__ */ a2(ChevronRightIcon, {})),
2100
+ button(t3.resetView, () => ctx.chart.resetView(), /* @__PURE__ */ a2(ResetIcon, {}))
2101
+ ] }),
2102
+ !s3.atRealtime && /* @__PURE__ */ a2(
2103
+ "button",
2104
+ {
2105
+ class: "fnc-realtime",
2106
+ style: { left: `${plotWidth - 44}px`, top: `${top}px` },
2107
+ title: t3.toRealtime,
2108
+ "aria-label": t3.toRealtime,
2109
+ onClick: () => ctx.chart.scrollToRealtime(),
2110
+ children: /* @__PURE__ */ a2(ToRealtimeIcon, {})
2111
+ }
2112
+ )
2113
+ ] });
2114
+ }
2115
+ var ATTRIBUTION_URL = "https://charts.funnest.xyz/?utm_source=chart&utm_medium=attribution";
2116
+ function Attribution({ ctx }) {
2117
+ const s3 = useStore(ctx.store);
2118
+ const { panesHeight, plotWidth } = s3.layout;
2119
+ if (!ctx.attribution || plotWidth <= 0) return null;
2120
+ const label = s3.locale === "ko" ? "Funnest \uCC28\uD2B8" : "Charts by Funnest";
2121
+ return /* @__PURE__ */ a2(
2122
+ "a",
2123
+ {
2124
+ class: "fnc-attribution",
2125
+ href: ATTRIBUTION_URL,
2126
+ target: "_blank",
2127
+ rel: "noopener",
2128
+ title: label,
2129
+ "aria-label": label,
2130
+ style: { top: `${panesHeight - 36}px` },
2131
+ children: [
2132
+ /* @__PURE__ */ a2(FunnestMark, {}),
2133
+ /* @__PURE__ */ a2("span", { children: "funnest" })
2134
+ ]
2135
+ }
2136
+ );
2137
+ }
2138
+ function PaneTools({ ctx }) {
2139
+ return ctx.multi ? /* @__PURE__ */ a2(PaneMaximize, { ctx, multi: ctx.multi }) : null;
2140
+ }
2141
+ function PaneMaximize({ ctx, multi }) {
2142
+ const s3 = useStore(ctx.store);
2143
+ const m3 = useStore(multi.store);
2144
+ if (s3.layout.plotWidth <= 0 || m3.narrow || m3.layout === "1" && m3.maximized === null) return null;
2145
+ const t3 = STRINGS[s3.locale];
2146
+ const maximized = m3.maximized !== null;
2147
+ const label = maximized ? t3.restorePane : t3.maximizePane;
2148
+ return /* @__PURE__ */ a2(
2149
+ "button",
2150
+ {
2151
+ class: "fnc-btn fnc-pane-max",
2152
+ style: { left: `${s3.layout.plotWidth - 36}px` },
2153
+ title: `${label} (Alt+Enter)`,
2154
+ "aria-label": label,
2155
+ "aria-pressed": maximized,
2156
+ onClick: () => {
2157
+ const index = multi.store.get().active;
2158
+ multi.toggleMaximize(index);
2159
+ },
2160
+ children: maximized ? /* @__PURE__ */ a2(RestoreIcon, {}) : /* @__PURE__ */ a2(MaximizeIcon, {})
2161
+ }
2162
+ );
2163
+ }
2164
+ function Overlay({ ctx }) {
2165
+ return /* @__PURE__ */ a2($, { children: [
2166
+ /* @__PURE__ */ a2(Attribution, { ctx }),
2167
+ /* @__PURE__ */ a2(Legend, { ctx }),
2168
+ /* @__PURE__ */ a2(NavControls, { ctx }),
2169
+ /* @__PURE__ */ a2(SelectionBar, { ctx }),
2170
+ /* @__PURE__ */ a2(PaneTools, { ctx })
2171
+ ] });
2172
+ }
2173
+
2174
+ // src/shell.ts
2175
+ import { darkTheme, lightTheme } from "@funnest/charts";
2176
+
2177
+ // src/storage.ts
2178
+ function createStorage(prefix) {
2179
+ const memory = /* @__PURE__ */ new Map();
2180
+ const ls = (() => {
2181
+ try {
2182
+ return prefix && typeof localStorage !== "undefined" ? localStorage : null;
2183
+ } catch {
2184
+ return null;
2185
+ }
2186
+ })();
2187
+ const full = (key) => `${prefix}:${key}`;
2188
+ return {
2189
+ get(key, fallback) {
2190
+ try {
2191
+ const raw = ls ? ls.getItem(full(key)) : memory.get(key) ?? null;
2192
+ return raw === null ? fallback : JSON.parse(raw);
2193
+ } catch {
2194
+ return fallback;
2195
+ }
2196
+ },
2197
+ set(key, value) {
2198
+ const raw = JSON.stringify(value);
2199
+ try {
2200
+ if (ls) ls.setItem(full(key), raw);
2201
+ else memory.set(key, raw);
2202
+ } catch {
2203
+ memory.set(key, raw);
2204
+ }
2205
+ }
2206
+ };
2207
+ }
2208
+
2209
+ // src/styles.ts
2210
+ var CSS = (
2211
+ /* css */
2212
+ `
2213
+ .fnc-root {
2214
+ position: relative;
2215
+ display: flex;
2216
+ flex-direction: column;
2217
+ width: 100%;
2218
+ height: 100%;
2219
+ min-height: 0;
2220
+ background: var(--fnc-bg);
2221
+ color: var(--fnc-text);
2222
+ font-family: var(--fnc-font);
2223
+ font-size: 14px;
2224
+ line-height: 1.4;
2225
+ -webkit-font-smoothing: antialiased;
2226
+ overflow: hidden;
2227
+ }
2228
+ .fnc-root *, .fnc-root *::before, .fnc-root *::after { box-sizing: border-box; }
2229
+ .fnc-root.fnc-pseudo-fullscreen { position: fixed; inset: 0; z-index: 2147483000; }
2230
+ :where(.fnc-root) button {
2231
+ font: inherit;
2232
+ color: inherit;
2233
+ background: none;
2234
+ border: 0;
2235
+ margin: 0;
2236
+ padding: 0;
2237
+ cursor: pointer;
2238
+ -webkit-tap-highlight-color: transparent;
2239
+ }
2240
+ .fnc-root button:focus-visible { outline: 2px solid var(--fnc-accent); outline-offset: 1px; }
2241
+
2242
+ /* \u2500\u2500 Toolbar \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2243
+ .fnc-toolbar {
2244
+ display: flex;
2245
+ align-items: center;
2246
+ gap: 4px;
2247
+ height: 48px;
2248
+ padding: 0 8px;
2249
+ border-bottom: 1px solid var(--fnc-border);
2250
+ flex: none;
2251
+ }
2252
+ .fnc-symbol {
2253
+ display: flex;
2254
+ align-items: center;
2255
+ gap: 6px;
2256
+ height: 36px;
2257
+ max-width: 220px;
2258
+ padding: 0 10px 0 8px;
2259
+ border-radius: var(--fnc-radius);
2260
+ font-weight: 700;
2261
+ font-size: 15px;
2262
+ /* The symbol gives way first when the toolbar is tight: its name truncates, never overlaps. */
2263
+ flex: 0 1 auto;
2264
+ min-width: 36px;
2265
+ overflow: hidden;
2266
+ }
2267
+ .fnc-symbol .fnc-icon { color: var(--fnc-text-2); display: flex; }
2268
+ .fnc-symbol-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2269
+ .fnc-symbol:not(:disabled):hover, .fnc-btn:hover, .fnc-chip:hover { background: var(--fnc-surface); }
2270
+ .fnc-symbol:disabled { cursor: default; }
2271
+ .fnc-sep { width: 1px; height: 20px; background: var(--fnc-border); margin: 0 4px; flex: none; }
2272
+ .fnc-intervals {
2273
+ display: flex;
2274
+ gap: 2px;
2275
+ overflow-x: auto;
2276
+ scrollbar-width: none;
2277
+ min-width: 0;
2278
+ }
2279
+ .fnc-intervals::-webkit-scrollbar { display: none; }
2046
2280
  .fnc-chip {
2047
2281
  height: 32px;
2048
2282
  padding: 0 10px;
@@ -2114,7 +2348,9 @@ var CSS = (
2114
2348
 
2115
2349
  /* \u2500\u2500 Chart body & overlays \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 */
2116
2350
  .fnc-body { position: relative; display: flex; flex: 1; min-height: 0; }
2117
- .fnc-stage { position: relative; flex: 1; min-width: 0; }
2351
+ .fnc-main { position: relative; display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; }
2352
+ .fnc-stage { position: relative; flex: 1; min-width: 0; min-height: 0; }
2353
+ .fnc-slot-bottom { flex: none; min-width: 0; }
2118
2354
  .fnc-chart-host, .fnc-overlay { position: absolute; inset: 0; }
2119
2355
 
2120
2356
  /* \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 */
@@ -2460,6 +2696,13 @@ var CSS = (
2460
2696
  flex: none;
2461
2697
  }
2462
2698
 
2699
+ .fnc-tools { display: flex; align-items: center; gap: 4px; flex: none; }
2700
+ /* Right-hand menus open toward the left so they stay on screen. */
2701
+ .fnc-tools .fnc-menu { left: auto; right: 0; }
2702
+ .fnc-kbd-btn { cursor: pointer; }
2703
+ .fnc-kbd-btn:hover, .fnc-sr-foot-btn:hover { color: var(--fnc-text); }
2704
+ .fnc-sr-foot-btn { display: inline-flex; align-items: center; gap: 4px; font-size: inherit; color: inherit; cursor: pointer; }
2705
+
2463
2706
  .fnc-up { color: var(--fnc-up); }
2464
2707
  .fnc-down { color: var(--fnc-down); }
2465
2708
  .fnc-flat { color: var(--fnc-text); }
@@ -2699,6 +2942,102 @@ var CSS = (
2699
2942
  .fnc-toolbar > .fnc-spacer { min-width: 0; }
2700
2943
  }
2701
2944
 
2945
+ /* \u2500\u2500 Multi-chart (split panes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2946
+ .fnc-grid { display: flex; flex: 1; min-width: 0; min-height: 0; }
2947
+ .fnc-split { display: flex; min-width: 0; min-height: 0; }
2948
+ .fnc-split-row { flex-direction: row; }
2949
+ .fnc-split-col { flex-direction: column; }
2950
+ .fnc-pane { position: relative; display: flex; min-width: 0; min-height: 0; }
2951
+ .fnc-pane > .fnc-stage { flex: 1; min-height: 0; }
2952
+ .fnc-split-on .fnc-pane-active::after {
2953
+ content: '';
2954
+ position: absolute;
2955
+ inset: 0;
2956
+ border: 1px solid var(--fnc-text-2);
2957
+ pointer-events: none;
2958
+ z-index: 3;
2959
+ }
2960
+ .fnc-splitter { position: relative; flex: none; background: var(--fnc-border); z-index: 4; touch-action: none; }
2961
+ .fnc-splitter::after { content: ''; position: absolute; }
2962
+ .fnc-splitter-row { width: 1px; cursor: col-resize; }
2963
+ .fnc-splitter-row::after { inset: 0 -4px; }
2964
+ .fnc-splitter-col { height: 1px; cursor: row-resize; }
2965
+ .fnc-splitter-col::after { inset: -4px 0; }
2966
+ .fnc-splitter:hover { background: var(--fnc-text-2); }
2967
+ .fnc-resizing-x, .fnc-resizing-x * { cursor: col-resize !important; user-select: none; }
2968
+ .fnc-resizing-y, .fnc-resizing-y * { cursor: row-resize !important; user-select: none; }
2969
+ .fnc-parking { display: none; }
2970
+ /* Navigation controls follow the hovered pane, not the whole widget. */
2971
+ .fnc-multi .fnc-body:hover .fnc-nav { opacity: 0; }
2972
+ .fnc-multi .fnc-stage:hover .fnc-nav, .fnc-multi .fnc-nav:focus-within { opacity: 1; }
2973
+ .fnc-pane-max {
2974
+ position: absolute;
2975
+ top: 6px;
2976
+ width: 28px;
2977
+ height: 28px;
2978
+ opacity: 0;
2979
+ background: var(--fnc-bg);
2980
+ pointer-events: auto;
2981
+ z-index: 2;
2982
+ }
2983
+ .fnc-stage:hover .fnc-pane-max, .fnc-pane-max:focus-visible, .fnc-pane-max[aria-pressed='true'] { opacity: 1; }
2984
+ @media (hover: none) { .fnc-pane-max { opacity: 0.7; } }
2985
+ .fnc-pane-tabs {
2986
+ display: flex;
2987
+ gap: 4px;
2988
+ padding: 4px 8px;
2989
+ overflow-x: auto;
2990
+ scrollbar-width: none;
2991
+ border-bottom: 1px solid var(--fnc-border);
2992
+ }
2993
+ .fnc-pane-tab {
2994
+ display: flex;
2995
+ align-items: center;
2996
+ gap: 6px;
2997
+ flex: none;
2998
+ height: 30px;
2999
+ padding: 0 10px 0 6px;
3000
+ border-radius: var(--fnc-radius);
3001
+ font-size: 13px;
3002
+ font-weight: 600;
3003
+ color: var(--fnc-text-2);
3004
+ white-space: nowrap;
3005
+ }
3006
+ .fnc-pane-tab small { font-size: 12px; font-weight: 400; color: var(--fnc-text-2); }
3007
+ .fnc-pane-tab[aria-selected='true'] { background: var(--fnc-surface); color: var(--fnc-text); }
3008
+ .fnc-pane-tab-num {
3009
+ display: grid;
3010
+ place-items: center;
3011
+ width: 18px;
3012
+ height: 18px;
3013
+ border-radius: 5px;
3014
+ background: var(--fnc-surface);
3015
+ font-size: 11px;
3016
+ }
3017
+ .fnc-pane-tab[aria-selected='true'] .fnc-pane-tab-num { background: var(--fnc-text-2); color: var(--fnc-bg); }
3018
+ .fnc-layout-menu { min-width: 0; gap: 2px; padding: 8px; }
3019
+ .fnc-layout-row { display: flex; align-items: center; gap: 2px; }
3020
+ .fnc-layout-count { width: 16px; flex: none; font-size: 12px; color: var(--fnc-text-2); text-align: center; }
3021
+ .fnc-layout-opt {
3022
+ display: grid;
3023
+ place-items: center;
3024
+ width: 36px;
3025
+ height: 32px;
3026
+ border-radius: var(--fnc-radius);
3027
+ color: var(--fnc-text-2);
3028
+ }
3029
+ .fnc-layout-opt:hover { background: var(--fnc-surface); color: var(--fnc-text); }
3030
+ .fnc-layout-opt[aria-checked='true'] { background: var(--fnc-surface); color: var(--fnc-text); }
3031
+ .fnc-sync-menu { min-width: 220px; }
3032
+ .fnc-menu-title { display: flex; flex-direction: column; gap: 2px; padding: 6px 10px 8px; font-size: 13px; font-weight: 600; color: var(--fnc-text); }
3033
+ .fnc-menu-title small { font-size: 12px; font-weight: 400; color: var(--fnc-text-2); }
3034
+ .fnc-sync-item { justify-content: space-between; }
3035
+ .fnc-sync-item[aria-checked='true'] { background: none; color: var(--fnc-text); }
3036
+ .fnc-sync-item:hover { background: var(--fnc-surface); }
3037
+ .fnc-sync-item .fnc-switch { display: inline-block; transform: scale(0.8); transform-origin: right center; }
3038
+ .fnc-switch[data-on='true'] { background: var(--fnc-text-2); }
3039
+ .fnc-switch[data-on='true']::after { transform: translateX(16px); }
3040
+
2702
3041
  /* \u2500\u2500 Mobile \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2703
3042
  @media (max-width: 640px) {
2704
3043
  .fnc-toolbar { padding: 0 4px; gap: 2px; }
@@ -2711,6 +3050,7 @@ var CSS = (
2711
3050
  .fnc-chip { padding: 0 8px; }
2712
3051
  .fnc-btn-screenshot { display: none; }
2713
3052
  .fnc-btn-label span { display: none; }
3053
+ .fnc-btn-sync { display: none; }
2714
3054
  .fnc-intervals { display: none; }
2715
3055
  .fnc-interval-current { display: inline; }
2716
3056
  .fnc-interval-menu { left: auto; right: -40px; }
@@ -2752,21 +3092,7 @@ function injectStyles(doc = document) {
2752
3092
  doc.head.appendChild(style);
2753
3093
  }
2754
3094
 
2755
- // src/trading-chart.tsx
2756
- var DEFAULT_INTERVAL_GROUPS = [
2757
- { id: "minutes", intervals: ["1", "5", "15", "30"] },
2758
- { id: "hours", intervals: ["60", "240"] },
2759
- { id: "days", intervals: ["1D", "1W", "1M", "3M", "6M", "12M"] }
2760
- ];
2761
- var DEFAULT_FAVORITE_INTERVALS = ["1", "5", "15", "60", "240", "1D"];
2762
- var stockLike = (i3) => i3.type === "stock" || i3.type === "etf" || i3.type === "index";
2763
- var DEFAULT_SEARCH_CATEGORIES = [
2764
- { id: "all", label: { ko: "\uC804\uCCB4", en: "All" }, match: () => true },
2765
- { id: "kr-stock", label: { ko: "\uAD6D\uB0B4\uC8FC\uC2DD", en: "KR stocks" }, match: (i3) => stockLike(i3) && i3.country === "KR" },
2766
- { id: "global-stock", label: { ko: "\uD574\uC678\uC8FC\uC2DD", en: "Global stocks" }, match: (i3) => stockLike(i3) && i3.country !== "KR" },
2767
- { id: "crypto", label: { ko: "\uCF54\uC778", en: "Crypto" }, match: (i3) => i3.type === "crypto" },
2768
- { id: "futures", label: { ko: "\uC120\uBB3C", en: "Futures" }, match: (i3) => i3.type === "futures" }
2769
- ];
3095
+ // src/shell.ts
2770
3096
  var THEME_VARS = {
2771
3097
  background: "--fnc-bg",
2772
3098
  surface: "--fnc-surface",
@@ -2785,37 +3111,19 @@ function applyThemeVars(el, theme) {
2785
3111
  el.style.setProperty("--fnc-radius", `${theme.radius}px`);
2786
3112
  }
2787
3113
  function div(className, parent) {
2788
- const el = document.createElement("div");
2789
- el.className = className;
2790
- parent?.appendChild(el);
2791
- return el;
2792
- }
2793
- function createTradingChart(container, options) {
2794
- const {
2795
- datafeed,
2796
- symbol,
2797
- resolution = "1D",
2798
- intervalGroups = DEFAULT_INTERVAL_GROUPS,
2799
- favoriteIntervals,
2800
- onFavoriteIntervalsChange,
2801
- searchCategories = DEFAULT_SEARCH_CATEGORIES,
2802
- storageKey = "funnest-charts",
2803
- searchHotkey = true,
2804
- symbolLogo = true,
2805
- drawingToolbar = true,
2806
- chartType = "candles",
2807
- attribution = true,
2808
- colorScheme = "system",
2809
- ...chartOptions
2810
- } = options;
2811
- const locale = chartOptions.locale ?? "ko";
2812
- const displayTimezone = chartOptions.displayTimezone ?? (locale === "ko" ? "Asia/Seoul" : Intl.DateTimeFormat().resolvedOptions().timeZone);
2813
- const storage = createStorage(storageKey);
3114
+ const el = document.createElement("div");
3115
+ el.className = className;
3116
+ parent?.appendChild(el);
3117
+ return el;
3118
+ }
3119
+ function createShell(container, options) {
2814
3120
  injectStyles(container.ownerDocument);
3121
+ const storage = createStorage(options.storageKey);
2815
3122
  const win = container.ownerDocument.defaultView;
2816
3123
  const darkQuery = win?.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
2817
- let scheme = colorScheme;
2818
- let themePatch = { ...chartOptions.theme };
3124
+ let scheme = options.colorScheme;
3125
+ let themePatch = { ...options.theme };
3126
+ let panes = () => [];
2819
3127
  const isDark = () => scheme === "system" ? !!darkQuery?.matches : scheme === "dark";
2820
3128
  const resolveTheme = () => ({ ...isDark() ? darkTheme : lightTheme, ...themePatch });
2821
3129
  let theme = resolveTheme();
@@ -2831,33 +3139,138 @@ function createTradingChart(container, options) {
2831
3139
  toolbar: div("fnc-slot fnc-slot-toolbar"),
2832
3140
  toolbarEnd: div("fnc-slot fnc-slot-toolbar-end"),
2833
3141
  belowToolbar: div("fnc-slot fnc-slot-below-toolbar", root),
3142
+ bottom: div("fnc-slot fnc-slot-bottom"),
2834
3143
  footer: div("fnc-slot fnc-slot-footer")
2835
3144
  };
2836
3145
  const body = div("fnc-body", root);
2837
- const drawbarHost = drawingToolbar ? div("fnc-drawbar-host", body) : null;
2838
- const stage = div("fnc-stage", body);
2839
- const chartHost = div("fnc-chart-host", stage);
2840
- const overlayHost = div("fnc-overlay", stage);
3146
+ const drawbarHost = options.drawingToolbar ? div("fnc-drawbar-host", body) : null;
3147
+ const main = div("fnc-main", body);
3148
+ main.appendChild(slots.bottom);
2841
3149
  root.appendChild(slots.footer);
2842
- const chart = createChart(chartHost, { ...chartOptions, theme, datafeed, displayTimezone });
2843
3150
  const refreshScheme = () => {
2844
3151
  theme = resolveTheme();
2845
3152
  applyScheme();
2846
- chart.setTheme(theme);
3153
+ for (const p3 of panes()) p3.chart.setTheme(theme);
2847
3154
  };
2848
3155
  const onSchemeChange = () => scheme === "system" && refreshScheme();
2849
3156
  darkQuery?.addEventListener?.("change", onSchemeChange);
2850
- chart.setChartType(chartType);
3157
+ let favoriteIntervals = storage.get("favorite-intervals", [...options.favoriteIntervals]);
3158
+ let drawbarCollapsed = storage.get("drawbar-collapsed", false);
3159
+ const broadcast = (patch) => {
3160
+ for (const p3 of panes()) p3.store.set(patch);
3161
+ };
3162
+ const isNativeFullscreen = () => document.fullscreenElement === root;
3163
+ const isFullscreen = () => isNativeFullscreen() || root.classList.contains("fnc-pseudo-fullscreen");
3164
+ const onFullscreenChange = () => broadcast({ fullscreen: isFullscreen() });
3165
+ document.addEventListener("fullscreenchange", onFullscreenChange);
3166
+ const pseudo = (on) => {
3167
+ root.classList.toggle("fnc-pseudo-fullscreen", on);
3168
+ onFullscreenChange();
3169
+ };
3170
+ return {
3171
+ root,
3172
+ toolbarHost,
3173
+ body,
3174
+ drawbarHost,
3175
+ main,
3176
+ slots,
3177
+ storage,
3178
+ theme: () => theme,
3179
+ sharedState: () => ({ fullscreen: isFullscreen(), favoriteIntervals, drawbarCollapsed }),
3180
+ setPanes: (get) => {
3181
+ panes = get;
3182
+ },
3183
+ setTheme: (patch) => {
3184
+ themePatch = { ...themePatch, ...patch };
3185
+ refreshScheme();
3186
+ },
3187
+ setColorScheme: (next) => {
3188
+ scheme = next;
3189
+ refreshScheme();
3190
+ },
3191
+ isFullscreen,
3192
+ toggleFullscreen: () => {
3193
+ if (isNativeFullscreen()) void document.exitFullscreen();
3194
+ else if (root.classList.contains("fnc-pseudo-fullscreen")) pseudo(false);
3195
+ else if (root.requestFullscreen && document.fullscreenEnabled) root.requestFullscreen().catch(() => pseudo(true));
3196
+ else pseudo(true);
3197
+ },
3198
+ setDrawbarCollapsed: (collapsed) => {
3199
+ drawbarCollapsed = collapsed;
3200
+ storage.set("drawbar-collapsed", collapsed);
3201
+ broadcast({ drawbarCollapsed });
3202
+ },
3203
+ toggleFavoriteInterval: (res) => {
3204
+ favoriteIntervals = favoriteIntervals.includes(res) ? favoriteIntervals.filter((r3) => r3 !== res) : [...favoriteIntervals, res];
3205
+ storage.set("favorite-intervals", favoriteIntervals);
3206
+ broadcast({ favoriteIntervals });
3207
+ options.onFavoriteIntervalsChange?.(favoriteIntervals);
3208
+ },
3209
+ destroy: () => {
3210
+ document.removeEventListener("fullscreenchange", onFullscreenChange);
3211
+ darkQuery?.removeEventListener?.("change", onSchemeChange);
3212
+ if (isNativeFullscreen()) void document.exitFullscreen();
3213
+ root.remove();
3214
+ }
3215
+ };
3216
+ }
3217
+ function bindWidgetKeys(shell, active, searchHotkey, extra) {
3218
+ const { root } = shell;
3219
+ const onKeyDown = (e3) => {
3220
+ const pane = active();
3221
+ if (!pane) return;
3222
+ const s3 = pane.store.get();
3223
+ if (searchHotkey && (e3.ctrlKey || e3.metaKey) && e3.key.toLowerCase() === "k" && pane.canSearch && !s3.dialog) {
3224
+ e3.preventDefault();
3225
+ pane.store.set({ searchOpen: true });
3226
+ return;
3227
+ }
3228
+ if (e3.key === "Escape" && !e3.defaultPrevented && (s3.searchOpen || s3.dialog)) {
3229
+ e3.preventDefault();
3230
+ pane.store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
3231
+ return;
3232
+ }
3233
+ if (e3.key === "Escape" && root.classList.contains("fnc-pseudo-fullscreen") && !s3.searchOpen && !s3.dialog) {
3234
+ shell.toggleFullscreen();
3235
+ return;
3236
+ }
3237
+ if (extra?.(e3)) e3.preventDefault();
3238
+ };
3239
+ const onKeyUp = (e3) => {
3240
+ const pane = active();
3241
+ if (!pane) return;
3242
+ const s3 = pane.store.get();
3243
+ if (e3.key === "Escape" && (s3.searchOpen || s3.dialog)) pane.store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
3244
+ };
3245
+ document.addEventListener("keydown", onKeyDown);
3246
+ document.addEventListener("keyup", onKeyUp);
3247
+ return () => {
3248
+ document.removeEventListener("keydown", onKeyDown);
3249
+ document.removeEventListener("keyup", onKeyUp);
3250
+ };
3251
+ }
3252
+
3253
+ // src/pane.tsx
3254
+ function createPane(shell, config) {
3255
+ const stage = div("fnc-stage");
3256
+ const chartHost = div("fnc-chart-host", stage);
3257
+ const overlayHost = div("fnc-overlay", stage);
3258
+ const chart = createChart(chartHost, {
3259
+ ...config.chartOptions,
3260
+ theme: shell.theme(),
3261
+ datafeed: config.datafeed,
3262
+ displayTimezone: config.displayTimezone
3263
+ });
3264
+ chart.setChartType(config.chartType);
2851
3265
  const store = new Store({
2852
- locale,
3266
+ locale: config.chartOptions.locale ?? "ko",
2853
3267
  symbol: null,
2854
- resolution,
3268
+ resolution: config.resolution,
2855
3269
  loading: false,
2856
3270
  layout: chart.layout,
2857
3271
  atRealtime: true,
2858
- fullscreen: false,
2859
3272
  searchOpen: false,
2860
- chartType,
3273
+ chartType: config.chartType,
2861
3274
  drawingTool: null,
2862
3275
  selection: null,
2863
3276
  drawingCount: 0,
@@ -2865,12 +3278,11 @@ function createTradingChart(container, options) {
2865
3278
  magnet: chart.magnetEnabled,
2866
3279
  indicators: chart.getIndicators(),
2867
3280
  dialog: null,
2868
- favoriteIntervals: storage.get("favorite-intervals", [...favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS]),
2869
- drawbarCollapsed: storage.get("drawbar-collapsed", false)
3281
+ ...shell.sharedState()
2870
3282
  });
2871
3283
  const syncRealtime = () => store.set({ atRealtime: chart.isAtRealtime() });
2872
3284
  const offs = [
2873
- chart.on("symbolChange", (info) => store.set({ symbol: info, resolution: chart.currentResolution ?? resolution })),
3285
+ chart.on("symbolChange", (info) => store.set({ symbol: info, resolution: chart.currentResolution ?? store.get().resolution })),
2874
3286
  chart.on("loadingChange", (loading) => store.set({ loading })),
2875
3287
  chart.on("layoutChange", (layout) => store.set({ layout })),
2876
3288
  chart.on("visibleRangeChange", syncRealtime),
@@ -2880,7 +3292,7 @@ function createTradingChart(container, options) {
2880
3292
  chart.on("drawingsChange", (drawings) => store.set({ drawingCount: drawings.length })),
2881
3293
  chart.on("indicatorsChange", (indicators) => store.set({ indicators })),
2882
3294
  chart.on("magnetChange", (magnet) => store.set({ magnet })),
2883
- chart.on("chartTypeChange", (chartType2) => store.set({ chartType: chartType2 }))
3295
+ chart.on("chartTypeChange", (chartType) => store.set({ chartType }))
2884
3296
  ];
2885
3297
  const load = (sym, res) => {
2886
3298
  store.set({ resolution: res });
@@ -2889,61 +3301,27 @@ function createTradingChart(container, options) {
2889
3301
  store.set({ loading: false });
2890
3302
  });
2891
3303
  };
2892
- const isNativeFullscreen = () => document.fullscreenElement === root;
2893
- const onFullscreenChange = () => store.set({ fullscreen: isNativeFullscreen() || root.classList.contains("fnc-pseudo-fullscreen") });
2894
- const onKeyDown = (e3) => {
2895
- const s3 = store.get();
2896
- if (searchHotkey && (e3.ctrlKey || e3.metaKey) && e3.key.toLowerCase() === "k" && datafeed.searchSymbols && !s3.dialog) {
2897
- e3.preventDefault();
2898
- store.set({ searchOpen: true });
2899
- return;
2900
- }
2901
- if (e3.key === "Escape" && !e3.defaultPrevented && (s3.searchOpen || s3.dialog)) {
2902
- e3.preventDefault();
2903
- store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
2904
- return;
2905
- }
2906
- if (e3.key === "Escape" && root.classList.contains("fnc-pseudo-fullscreen") && !s3.searchOpen && !s3.dialog) {
2907
- root.classList.remove("fnc-pseudo-fullscreen");
2908
- onFullscreenChange();
2909
- }
2910
- };
2911
- const onKeyUp = (e3) => {
2912
- const s3 = store.get();
2913
- if (e3.key === "Escape" && (s3.searchOpen || s3.dialog)) store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
2914
- };
2915
- document.addEventListener("fullscreenchange", onFullscreenChange);
2916
- document.addEventListener("keydown", onKeyDown);
2917
- document.addEventListener("keyup", onKeyUp);
2918
3304
  const ctx = {
2919
- root,
3305
+ root: shell.root,
2920
3306
  chart,
2921
- datafeed,
3307
+ datafeed: config.datafeed,
2922
3308
  store,
2923
- storage,
2924
- intervalGroups,
2925
- searchCategories,
2926
- displayTimezone,
2927
- showLogo: symbolLogo,
2928
- drawingToolbar,
2929
- attribution,
2930
- slots,
2931
- setDrawbarCollapsed: (collapsed) => {
2932
- store.set({ drawbarCollapsed: collapsed });
2933
- storage.set("drawbar-collapsed", collapsed);
2934
- },
3309
+ storage: shell.storage,
3310
+ intervalGroups: config.intervalGroups,
3311
+ searchCategories: config.searchCategories,
3312
+ displayTimezone: config.displayTimezone,
3313
+ showLogo: config.symbolLogo,
3314
+ drawingToolbar: config.drawingToolbar,
3315
+ attribution: config.attribution,
3316
+ slots: shell.slots,
3317
+ multi: config.multi,
3318
+ setDrawbarCollapsed: shell.setDrawbarCollapsed,
2935
3319
  selectSymbol: (sym) => void load(sym, store.get().resolution),
2936
3320
  selectResolution: (res) => {
2937
3321
  const current = store.get().symbol;
2938
3322
  if (current) void load(current.symbol, res);
2939
3323
  },
2940
- toggleFavoriteInterval: (res) => {
2941
- const current = store.get().favoriteIntervals;
2942
- const next = current.includes(res) ? current.filter((r3) => r3 !== res) : [...current, res];
2943
- store.set({ favoriteIntervals: next });
2944
- storage.set("favorite-intervals", next);
2945
- onFavoriteIntervalsChange?.(next);
2946
- },
3324
+ toggleFavoriteInterval: shell.toggleFavoriteInterval,
2947
3325
  setSearchOpen: (open) => store.set({ searchOpen: open }),
2948
3326
  screenshot: () => {
2949
3327
  const s3 = store.get();
@@ -2956,61 +3334,491 @@ function createTradingChart(container, options) {
2956
3334
  setTimeout(() => URL.revokeObjectURL(a4.href), 1e3);
2957
3335
  });
2958
3336
  },
2959
- toggleFullscreen: () => {
2960
- if (isNativeFullscreen()) {
2961
- void document.exitFullscreen();
2962
- } else if (root.classList.contains("fnc-pseudo-fullscreen")) {
2963
- root.classList.remove("fnc-pseudo-fullscreen");
2964
- onFullscreenChange();
2965
- } else if (root.requestFullscreen && document.fullscreenEnabled) {
2966
- root.requestFullscreen().catch(() => {
2967
- root.classList.add("fnc-pseudo-fullscreen");
2968
- onFullscreenChange();
2969
- });
2970
- } else {
2971
- root.classList.add("fnc-pseudo-fullscreen");
2972
- onFullscreenChange();
2973
- }
2974
- }
3337
+ toggleFullscreen: shell.toggleFullscreen
2975
3338
  };
2976
- Q(/* @__PURE__ */ a2(Toolbar, { ctx }), toolbarHost);
2977
3339
  Q(/* @__PURE__ */ a2(Overlay, { ctx }), overlayHost);
2978
- if (drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx }), drawbarHost);
2979
- void load(symbol, resolution);
2980
3340
  return {
3341
+ stage,
2981
3342
  chart,
2982
- slots,
2983
- setSymbol: (sym, res) => load(sym, res ?? store.get().resolution),
3343
+ store,
3344
+ ctx,
3345
+ canSearch: typeof config.datafeed.searchSymbols === "function",
3346
+ load,
3347
+ destroy: () => {
3348
+ offs.forEach((off) => off());
3349
+ Q(null, overlayHost);
3350
+ chart.destroy();
3351
+ stage.remove();
3352
+ }
3353
+ };
3354
+ }
3355
+
3356
+ // src/trading-chart.tsx
3357
+ var DEFAULT_INTERVAL_GROUPS = [
3358
+ { id: "minutes", intervals: ["1", "5", "15", "30"] },
3359
+ { id: "hours", intervals: ["60", "240"] },
3360
+ { id: "days", intervals: ["1D", "1W", "1M", "3M", "6M", "12M"] }
3361
+ ];
3362
+ var DEFAULT_FAVORITE_INTERVALS = ["1", "5", "15", "60", "240", "1D"];
3363
+ var stockLike = (i3) => i3.type === "stock" || i3.type === "etf" || i3.type === "index";
3364
+ var DEFAULT_SEARCH_CATEGORIES = [
3365
+ { id: "all", label: { ko: "\uC804\uCCB4", en: "All" }, match: () => true },
3366
+ { id: "kr-stock", label: { ko: "\uAD6D\uB0B4\uC8FC\uC2DD", en: "KR stocks" }, match: (i3) => stockLike(i3) && i3.country === "KR" },
3367
+ { id: "global-stock", label: { ko: "\uD574\uC678\uC8FC\uC2DD", en: "Global stocks" }, match: (i3) => stockLike(i3) && i3.country !== "KR" },
3368
+ { id: "crypto", label: { ko: "\uCF54\uC778", en: "Crypto" }, match: (i3) => i3.type === "crypto" },
3369
+ { id: "futures", label: { ko: "\uC120\uBB3C", en: "Futures" }, match: (i3) => i3.type === "futures" }
3370
+ ];
3371
+ function defaultDisplayTimezone(locale, explicit) {
3372
+ return explicit ?? (locale === "ko" ? "Asia/Seoul" : Intl.DateTimeFormat().resolvedOptions().timeZone);
3373
+ }
3374
+ function createTradingChart(container, options) {
3375
+ const {
3376
+ datafeed,
3377
+ symbol,
3378
+ resolution = "1D",
3379
+ intervalGroups = DEFAULT_INTERVAL_GROUPS,
3380
+ favoriteIntervals,
3381
+ onFavoriteIntervalsChange,
3382
+ searchCategories = DEFAULT_SEARCH_CATEGORIES,
3383
+ storageKey = "funnest-charts",
3384
+ searchHotkey = true,
3385
+ symbolLogo = true,
3386
+ drawingToolbar = true,
3387
+ chartType = "candles",
3388
+ attribution = true,
3389
+ colorScheme = "system",
3390
+ theme,
3391
+ ...chartOptions
3392
+ } = options;
3393
+ const displayTimezone = defaultDisplayTimezone(chartOptions.locale ?? "ko", chartOptions.displayTimezone);
3394
+ const shell = createShell(container, {
3395
+ colorScheme,
3396
+ theme,
3397
+ storageKey,
3398
+ drawingToolbar,
3399
+ favoriteIntervals: favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS,
3400
+ onFavoriteIntervalsChange
3401
+ });
3402
+ const pane = createPane(shell, {
3403
+ datafeed,
3404
+ chartOptions,
3405
+ displayTimezone,
3406
+ resolution,
3407
+ chartType,
3408
+ intervalGroups,
3409
+ searchCategories,
3410
+ symbolLogo,
3411
+ drawingToolbar,
3412
+ attribution
3413
+ });
3414
+ shell.main.insertBefore(pane.stage, shell.slots.bottom);
3415
+ shell.setPanes(() => [pane]);
3416
+ const unbindKeys = bindWidgetKeys(shell, () => pane, searchHotkey);
3417
+ Q(/* @__PURE__ */ a2(Toolbar, { ctx: pane.ctx }), shell.toolbarHost);
3418
+ if (shell.drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx: pane.ctx }), shell.drawbarHost);
3419
+ void pane.load(symbol, resolution);
3420
+ return {
3421
+ chart: pane.chart,
3422
+ slots: shell.slots,
3423
+ setSymbol: (sym, res) => pane.load(sym, res ?? pane.store.get().resolution),
2984
3424
  setResolution: (res) => {
2985
- const current = store.get().symbol;
2986
- return current ? load(current.symbol, res) : Promise.resolve();
3425
+ const current = pane.store.get().symbol;
3426
+ return current ? pane.load(current.symbol, res) : Promise.resolve();
2987
3427
  },
2988
- setLocale: (locale2) => {
2989
- chart.setLocale(locale2);
2990
- store.set({ locale: locale2 });
3428
+ setLocale: (locale) => {
3429
+ pane.chart.setLocale(locale);
3430
+ pane.store.set({ locale });
2991
3431
  },
2992
- save: () => chart.save(),
2993
- load: (state) => chart.load(state),
2994
- setTheme: (patch) => {
2995
- themePatch = { ...themePatch, ...patch };
2996
- refreshScheme();
3432
+ save: () => pane.chart.save(),
3433
+ load: (state) => pane.chart.load(state),
3434
+ setTheme: shell.setTheme,
3435
+ setColorScheme: shell.setColorScheme,
3436
+ destroy: () => {
3437
+ unbindKeys();
3438
+ Q(null, shell.toolbarHost);
3439
+ if (shell.drawbarHost) Q(null, shell.drawbarHost);
3440
+ pane.destroy();
3441
+ shell.destroy();
3442
+ }
3443
+ };
3444
+ }
3445
+
3446
+ // src/multi-chart.tsx
3447
+ import { parseChartState } from "@funnest/charts";
3448
+ var DEFAULT_SYNC = { crosshair: true, time: false, symbol: false, interval: false };
3449
+ function PaneTab({ pane, index, active, onSelect }) {
3450
+ const s3 = useStore(pane.store);
3451
+ const t3 = STRINGS[s3.locale];
3452
+ return /* @__PURE__ */ a2("button", { class: "fnc-pane-tab", role: "tab", "aria-selected": active, onClick: onSelect, children: [
3453
+ /* @__PURE__ */ a2("span", { class: "fnc-pane-tab-num", children: index + 1 }),
3454
+ s3.symbol?.name ?? `${t3.pane} ${index + 1}`,
3455
+ /* @__PURE__ */ a2("small", { children: resolutionLabel(s3.resolution, s3.locale) })
3456
+ ] });
3457
+ }
3458
+ function PaneTabs({ multi, panes }) {
3459
+ const m3 = useStore(multi.store);
3460
+ const visible = panes().slice(0, paneCount(LAYOUTS[m3.layout]));
3461
+ if (!m3.narrow || visible.length < 2) return null;
3462
+ return /* @__PURE__ */ a2("div", { class: "fnc-pane-tabs", role: "tablist", children: visible.map((p3, i3) => /* @__PURE__ */ a2(PaneTab, { pane: p3, index: i3, active: i3 === m3.active, onSelect: () => multi.activate(i3) })) });
3463
+ }
3464
+ function createMultiChart(container, options) {
3465
+ const {
3466
+ datafeed,
3467
+ panes: paneSpecs,
3468
+ layout: initialLayout = "1",
3469
+ sync: initialSync,
3470
+ narrowWidth = 640,
3471
+ resolution = "1D",
3472
+ intervalGroups = DEFAULT_INTERVAL_GROUPS,
3473
+ favoriteIntervals,
3474
+ onFavoriteIntervalsChange,
3475
+ searchCategories = DEFAULT_SEARCH_CATEGORIES,
3476
+ storageKey = "funnest-charts",
3477
+ searchHotkey = true,
3478
+ symbolLogo = true,
3479
+ drawingToolbar = true,
3480
+ chartType = "candles",
3481
+ attribution = true,
3482
+ colorScheme = "system",
3483
+ theme,
3484
+ ...chartOptions
3485
+ } = options;
3486
+ if (paneSpecs.length === 0) throw new Error("funnest-charts: createMultiChart needs at least one pane");
3487
+ const displayTimezone = defaultDisplayTimezone(chartOptions.locale ?? "ko", chartOptions.displayTimezone);
3488
+ const shell = createShell(container, {
3489
+ colorScheme,
3490
+ theme,
3491
+ storageKey,
3492
+ drawingToolbar,
3493
+ favoriteIntervals: favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS,
3494
+ onFavoriteIntervalsChange
3495
+ });
3496
+ shell.root.classList.add("fnc-multi");
3497
+ const tabsHost = div("fnc-pane-tabs-host");
3498
+ shell.root.insertBefore(tabsHost, shell.body);
3499
+ const grid = div("fnc-grid");
3500
+ shell.main.insertBefore(grid, shell.slots.bottom);
3501
+ const parking = div("fnc-parking", shell.root);
3502
+ const listeners = /* @__PURE__ */ new Map();
3503
+ const emit = (event, payload) => {
3504
+ for (const l3 of listeners.get(event) ?? []) l3(payload);
3505
+ };
3506
+ let stateQueued = false;
3507
+ const queueState = () => {
3508
+ if (stateQueued) return;
3509
+ stateQueued = true;
3510
+ queueMicrotask(() => {
3511
+ stateQueued = false;
3512
+ emit("stateChange", void 0);
3513
+ });
3514
+ };
3515
+ const store = new Store({
3516
+ layout: isLayoutId(initialLayout) ? initialLayout : "1",
3517
+ sync: { ...DEFAULT_SYNC, ...initialSync },
3518
+ active: 0,
3519
+ maximized: null,
3520
+ narrow: false
3521
+ });
3522
+ let sizes = {};
3523
+ const panes = [];
3524
+ const cells = [];
3525
+ let driver = -1;
3526
+ const visibleCount = () => Math.min(paneCount(LAYOUTS[store.get().layout]), MAX_PANES);
3527
+ const visible = () => panes.slice(0, visibleCount());
3528
+ const controls = {
3529
+ store,
3530
+ setLayout: (layout) => setLayout(layout),
3531
+ setSync: (patch) => setSync(patch),
3532
+ activate: (index) => activate(index),
3533
+ toggleMaximize: (index) => toggleMaximize(index)
3534
+ };
3535
+ function makePane(index, spec) {
3536
+ const pane = createPane(shell, {
3537
+ datafeed,
3538
+ chartOptions,
3539
+ displayTimezone,
3540
+ resolution: spec.resolution ?? resolution,
3541
+ chartType,
3542
+ intervalGroups,
3543
+ searchCategories,
3544
+ symbolLogo,
3545
+ drawingToolbar,
3546
+ // One badge for the whole widget, not one per pane.
3547
+ attribution: attribution && index === 0,
3548
+ multi: controls
3549
+ });
3550
+ const cell = div("fnc-pane");
3551
+ cell.appendChild(pane.stage);
3552
+ const touch = () => {
3553
+ driver = panes.indexOf(pane);
3554
+ if (driver >= 0 && driver !== store.get().active) activate(driver);
3555
+ };
3556
+ cell.addEventListener("pointerdown", touch, true);
3557
+ cell.addEventListener("wheel", () => driver = panes.indexOf(pane), { capture: true, passive: true });
3558
+ const c3 = pane.chart;
3559
+ c3.on("crosshairMove", (info) => {
3560
+ if (!store.get().sync.crosshair || info?.source === "sync") return;
3561
+ for (const other of visible()) if (other !== pane) other.chart.setSyncedCrosshair(info ? info.time : null);
3562
+ });
3563
+ c3.on("visibleRangeChange", () => {
3564
+ if (!store.get().sync.time || panes[driver] !== pane) return;
3565
+ broadcastTime(pane);
3566
+ });
3567
+ c3.on("symbolChange", (info) => {
3568
+ queueState();
3569
+ if (panes[store.get().active] !== pane) return;
3570
+ const res = c3.currentResolution;
3571
+ const { sync } = store.get();
3572
+ if (!sync.symbol && !sync.interval) return;
3573
+ for (const other of visible()) {
3574
+ if (other === pane) continue;
3575
+ const sym = sync.symbol ? info.symbol : other.chart.symbolInfo?.symbol;
3576
+ const r3 = sync.interval && res ? res : other.chart.currentResolution ?? other.store.get().resolution;
3577
+ if (sym && (sym !== other.chart.symbolInfo?.symbol || r3 !== other.chart.currentResolution)) void other.load(sym, r3);
3578
+ }
3579
+ });
3580
+ c3.on("stateChange", queueState);
3581
+ panes[index] = pane;
3582
+ cells[index] = cell;
3583
+ void pane.load(spec.symbol, spec.resolution ?? resolution);
3584
+ return pane;
3585
+ }
3586
+ function broadcastTime(from) {
3587
+ const range = from.chart.getVisibleTimeRange();
3588
+ if (!range) return;
3589
+ const res = from.chart.currentResolution;
3590
+ for (const other of visible()) {
3591
+ if (other !== from && other.chart.currentResolution === res) other.chart.setVisibleTimeRange(range);
3592
+ }
3593
+ }
3594
+ function ensurePanes(count) {
3595
+ for (let i3 = panes.length; i3 < count; i3++) {
3596
+ const src = panes[store.get().active] ?? panes[0];
3597
+ const spec = paneSpecs[i3] ?? {
3598
+ symbol: src?.chart.symbolInfo?.symbol ?? paneSpecs[0].symbol,
3599
+ resolution: src?.chart.currentResolution ?? src?.store.get().resolution ?? resolution
3600
+ };
3601
+ makePane(i3, spec);
3602
+ }
3603
+ }
3604
+ function buildNode(node, path, layout) {
3605
+ if (typeof node === "number") return cells[node];
3606
+ const box = div(`fnc-split fnc-split-${node.dir}`);
3607
+ const key = `${layout}:${path}`;
3608
+ const weights = sizesFor(sizes, key, node.children.length);
3609
+ const children = node.children.map((child, i3) => {
3610
+ const el = buildNode(child, path ? `${path}.${i3}` : String(i3), layout);
3611
+ el.style.flex = `${weights[i3]} 1 0px`;
3612
+ return el;
3613
+ });
3614
+ children.forEach((el, i3) => {
3615
+ box.appendChild(el);
3616
+ if (i3 < children.length - 1) box.appendChild(makeSplitter(box, node.dir, key, i3, children));
3617
+ });
3618
+ return box;
3619
+ }
3620
+ function makeSplitter(box, dir, key, i3, children) {
3621
+ const bar = div(`fnc-splitter fnc-splitter-${dir}`);
3622
+ bar.setAttribute("role", "separator");
3623
+ bar.setAttribute("aria-orientation", dir === "row" ? "vertical" : "horizontal");
3624
+ let last = null;
3625
+ const apply = (next) => {
3626
+ sizes = { ...sizes, [key]: next };
3627
+ next.forEach((w3, j2) => children[j2].style.flex = `${w3} 1 0px`);
3628
+ queueState();
3629
+ };
3630
+ bar.addEventListener("pointerdown", (e3) => {
3631
+ bar.setPointerCapture(e3.pointerId);
3632
+ last = dir === "row" ? e3.clientX : e3.clientY;
3633
+ shell.root.classList.add(dir === "row" ? "fnc-resizing-x" : "fnc-resizing-y");
3634
+ });
3635
+ bar.addEventListener("pointermove", (e3) => {
3636
+ if (last === null) return;
3637
+ const pos = dir === "row" ? e3.clientX : e3.clientY;
3638
+ const length = dir === "row" ? box.clientWidth : box.clientHeight;
3639
+ if (length <= 0) return;
3640
+ apply(resizePair(sizesFor(sizes, key, children.length), i3, (pos - last) / length));
3641
+ last = pos;
3642
+ });
3643
+ const end = () => {
3644
+ last = null;
3645
+ shell.root.classList.remove("fnc-resizing-x", "fnc-resizing-y");
3646
+ };
3647
+ bar.addEventListener("pointerup", end);
3648
+ bar.addEventListener("pointercancel", end);
3649
+ bar.addEventListener("dblclick", () => apply(Array(children.length).fill(1)));
3650
+ return bar;
3651
+ }
3652
+ function renderGrid() {
3653
+ const s3 = store.get();
3654
+ const count = visibleCount();
3655
+ for (let i3 = 0; i3 < panes.length; i3++) {
3656
+ if (i3 >= count) parking.appendChild(cells[i3]);
3657
+ cells[i3].classList.toggle("fnc-pane-active", i3 === s3.active && count > 1);
3658
+ }
3659
+ const single = s3.maximized ?? (s3.narrow ? s3.active : null);
3660
+ const top = single !== null && single < count ? cells[single] : buildNode(LAYOUTS[s3.layout], "", s3.layout);
3661
+ top.style.flex = "1 1 0px";
3662
+ grid.replaceChildren(top);
3663
+ shell.root.classList.toggle("fnc-split-on", count > 1 && single === null);
3664
+ }
3665
+ function bindToolbar() {
3666
+ const pane = panes[store.get().active];
3667
+ Q(/* @__PURE__ */ a2(Toolbar, { ctx: pane.ctx }, store.get().active), shell.toolbarHost);
3668
+ if (shell.drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx: pane.ctx }, store.get().active), shell.drawbarHost);
3669
+ }
3670
+ function activate(index) {
3671
+ const s3 = store.get();
3672
+ if (index < 0 || index >= visibleCount() || index === s3.active) return;
3673
+ panes[s3.active]?.chart.setDrawingTool(null);
3674
+ store.set({ active: index, maximized: s3.maximized === null ? null : index });
3675
+ bindToolbar();
3676
+ renderGrid();
3677
+ emit("activeChange", { index, chart: panes[index].chart });
3678
+ queueState();
3679
+ }
3680
+ function setLayout(layout) {
3681
+ if (!isLayoutId(layout)) return;
3682
+ const count = paneCount(LAYOUTS[layout]);
3683
+ ensurePanes(count);
3684
+ const s3 = store.get();
3685
+ for (const p3 of panes.slice(count)) p3.chart.setSyncedCrosshair(null);
3686
+ store.set({ layout, maximized: null, active: Math.min(s3.active, count - 1) });
3687
+ if (s3.active !== store.get().active) bindToolbar();
3688
+ renderGrid();
3689
+ emit("layoutChange", { layout, panes: count });
3690
+ queueState();
3691
+ }
3692
+ function setSync(patch) {
3693
+ const before = store.get().sync;
3694
+ const sync = { ...before, ...patch };
3695
+ store.set({ sync });
3696
+ const active = panes[store.get().active];
3697
+ if (!sync.crosshair) for (const p3 of panes) p3.chart.setSyncedCrosshair(null);
3698
+ if (sync.time && !before.time) broadcastTime(active);
3699
+ if (sync.symbol && !before.symbol || sync.interval && !before.interval) {
3700
+ const info = active.chart.symbolInfo;
3701
+ const res = active.chart.currentResolution;
3702
+ for (const other of visible()) {
3703
+ if (other === active || !info) continue;
3704
+ const sym = sync.symbol ? info.symbol : other.chart.symbolInfo?.symbol ?? info.symbol;
3705
+ const r3 = sync.interval && res ? res : other.chart.currentResolution ?? other.store.get().resolution;
3706
+ if (sym !== other.chart.symbolInfo?.symbol || r3 !== other.chart.currentResolution) void other.load(sym, r3);
3707
+ }
3708
+ }
3709
+ queueState();
3710
+ }
3711
+ function toggleMaximize(index) {
3712
+ const s3 = store.get();
3713
+ const target = index ?? s3.active;
3714
+ if (s3.maximized !== null) store.set({ maximized: null });
3715
+ else if (visibleCount() > 1) {
3716
+ if (target !== s3.active) activate(target);
3717
+ store.set({ maximized: target });
3718
+ }
3719
+ renderGrid();
3720
+ }
3721
+ makePane(0, paneSpecs[0]);
3722
+ ensurePanes(Math.max(visibleCount(), Math.min(paneSpecs.length, MAX_PANES)));
3723
+ shell.setPanes(() => panes);
3724
+ bindToolbar();
3725
+ Q(/* @__PURE__ */ a2(PaneTabs, { multi: controls, panes: () => panes }), tabsHost);
3726
+ renderGrid();
3727
+ const resize = new ResizeObserver(() => {
3728
+ const narrow = shell.root.clientWidth > 0 && shell.root.clientWidth < narrowWidth;
3729
+ if (narrow === store.get().narrow) return;
3730
+ store.set({ narrow });
3731
+ renderGrid();
3732
+ });
3733
+ resize.observe(shell.root);
3734
+ const unbindKeys = bindWidgetKeys(
3735
+ shell,
3736
+ () => panes[store.get().active] ?? null,
3737
+ searchHotkey,
3738
+ (e3) => {
3739
+ if (!e3.altKey || e3.ctrlKey || e3.metaKey) return false;
3740
+ if (e3.key === "Enter") {
3741
+ toggleMaximize();
3742
+ return true;
3743
+ }
3744
+ const digit = /^Digit([1-8])$/.exec(e3.code)?.[1];
3745
+ if (!digit) return false;
3746
+ activate(Number(digit) - 1);
3747
+ return true;
3748
+ }
3749
+ );
3750
+ const save = () => {
3751
+ const s3 = store.get();
3752
+ return { version: 1, layout: s3.layout, sync: { ...s3.sync }, active: s3.active, sizes: { ...sizes }, panes: panes.map((p3) => p3.chart.save()) };
3753
+ };
3754
+ return {
3755
+ slots: shell.slots,
3756
+ get charts() {
3757
+ return visible().map((p3) => p3.chart);
2997
3758
  },
2998
- setColorScheme: (next) => {
2999
- scheme = next;
3000
- refreshScheme();
3759
+ get activeChart() {
3760
+ return panes[store.get().active].chart;
3761
+ },
3762
+ get activeIndex() {
3763
+ return store.get().active;
3764
+ },
3765
+ get layout() {
3766
+ return store.get().layout;
3767
+ },
3768
+ get sync() {
3769
+ return { ...store.get().sync };
3770
+ },
3771
+ setLayout,
3772
+ setSync,
3773
+ activate,
3774
+ toggleMaximize,
3775
+ setSymbol: (sym, res) => {
3776
+ const pane = panes[store.get().active];
3777
+ return pane.load(sym, res ?? pane.store.get().resolution);
3778
+ },
3779
+ setResolution: (res) => {
3780
+ const pane = panes[store.get().active];
3781
+ const current = pane.store.get().symbol;
3782
+ return current ? pane.load(current.symbol, res) : Promise.resolve();
3783
+ },
3784
+ setLocale: (locale) => {
3785
+ for (const p3 of panes) {
3786
+ p3.chart.setLocale(locale);
3787
+ p3.store.set({ locale });
3788
+ }
3789
+ },
3790
+ setTheme: shell.setTheme,
3791
+ setColorScheme: shell.setColorScheme,
3792
+ save,
3793
+ load: async (input) => {
3794
+ const raw = input;
3795
+ if (!raw || typeof raw !== "object" || raw.version !== 1 || !Array.isArray(raw.panes) || raw.panes.length === 0) {
3796
+ throw new Error("funnest-charts: unsupported multi-chart state (expected version 1)");
3797
+ }
3798
+ const states = raw.panes.slice(0, MAX_PANES).map((p3) => parseChartState(p3));
3799
+ sizes = raw.sizes && typeof raw.sizes === "object" ? { ...raw.sizes } : {};
3800
+ const layout = isLayoutId(raw.layout) ? raw.layout : "1";
3801
+ ensurePanes(Math.max(states.length, paneCount(LAYOUTS[layout])));
3802
+ store.set({ sync: { ...DEFAULT_SYNC, ...raw.sync } });
3803
+ setLayout(layout);
3804
+ activate(Math.min(Math.max(0, Number(raw.active) || 0), visibleCount() - 1));
3805
+ await Promise.all(states.map((state, i3) => panes[i3].chart.load(state)));
3806
+ },
3807
+ on: (event, listener) => {
3808
+ let set = listeners.get(event);
3809
+ if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
3810
+ set.add(listener);
3811
+ return () => set.delete(listener);
3001
3812
  },
3002
3813
  destroy: () => {
3003
- offs.forEach((off) => off());
3004
- document.removeEventListener("fullscreenchange", onFullscreenChange);
3005
- document.removeEventListener("keydown", onKeyDown);
3006
- document.removeEventListener("keyup", onKeyUp);
3007
- darkQuery?.removeEventListener?.("change", onSchemeChange);
3008
- if (isNativeFullscreen()) void document.exitFullscreen();
3009
- Q(null, toolbarHost);
3010
- Q(null, overlayHost);
3011
- if (drawbarHost) Q(null, drawbarHost);
3012
- chart.destroy();
3013
- root.remove();
3814
+ unbindKeys();
3815
+ resize.disconnect();
3816
+ Q(null, shell.toolbarHost);
3817
+ if (shell.drawbarHost) Q(null, shell.drawbarHost);
3818
+ Q(null, tabsHost);
3819
+ for (const p3 of panes) p3.destroy();
3820
+ listeners.clear();
3821
+ shell.destroy();
3014
3822
  }
3015
3823
  };
3016
3824
  }
@@ -3018,6 +3826,11 @@ export {
3018
3826
  DEFAULT_FAVORITE_INTERVALS,
3019
3827
  DEFAULT_INTERVAL_GROUPS,
3020
3828
  DEFAULT_SEARCH_CATEGORIES,
3829
+ LAYOUTS,
3830
+ LAYOUT_GROUPS,
3831
+ MAX_PANES,
3832
+ createMultiChart,
3021
3833
  createTradingChart,
3834
+ paneCount,
3022
3835
  resolutionLabel
3023
3836
  };