@funnest/charts-ui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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,215 @@ 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
+ function LayoutGlyph({ id }) {
1454
+ 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" })) });
1455
+ }
1456
+ function usePopover() {
1457
+ const [open, setOpen] = d2(false);
1458
+ const ref = T2(null);
1459
+ A2(() => {
1460
+ if (!open) return;
1461
+ const onDown = (e3) => !ref.current?.contains(e3.target) && setOpen(false);
1462
+ const onKey = (e3) => e3.key === "Escape" && setOpen(false);
1463
+ document.addEventListener("pointerdown", onDown);
1464
+ document.addEventListener("keydown", onKey);
1465
+ return () => {
1466
+ document.removeEventListener("pointerdown", onDown);
1467
+ document.removeEventListener("keydown", onKey);
1468
+ };
1469
+ }, [open]);
1470
+ return { open, setOpen, ref };
1471
+ }
1472
+ var SYNC_KEYS = [
1473
+ { key: "crosshair", label: "syncCrosshair" },
1474
+ { key: "time", label: "syncTime" },
1475
+ { key: "symbol", label: "syncSymbol" },
1476
+ { key: "interval", label: "syncInterval" }
1477
+ ];
1478
+ function LayoutMenu({ ctx, multi }) {
1479
+ const s3 = useStore(ctx.store);
1480
+ const m3 = useStore(multi.store);
1481
+ const t3 = STRINGS[s3.locale];
1482
+ const layout = usePopover();
1483
+ const sync = usePopover();
1484
+ const anySync = Object.values(m3.sync).some(Boolean);
1485
+ return /* @__PURE__ */ a2($, { children: [
1486
+ /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: layout.ref, children: [
1487
+ /* @__PURE__ */ a2(
1488
+ "button",
1489
+ {
1490
+ class: "fnc-btn fnc-btn-wide fnc-btn-layout",
1491
+ title: t3.layout,
1492
+ "aria-label": t3.layout,
1493
+ "aria-haspopup": "menu",
1494
+ "aria-expanded": layout.open,
1495
+ onClick: () => layout.setOpen(!layout.open),
1496
+ children: [
1497
+ /* @__PURE__ */ a2(LayoutGlyph, { id: m3.layout }),
1498
+ /* @__PURE__ */ a2(ChevronDownIcon, {})
1499
+ ]
1500
+ }
1501
+ ),
1502
+ 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: [
1503
+ /* @__PURE__ */ a2("span", { class: "fnc-layout-count", children: group[0].split("-")[0] }),
1504
+ group.map((id) => /* @__PURE__ */ a2(
1505
+ "button",
1506
+ {
1507
+ class: "fnc-layout-opt",
1508
+ role: "menuitemradio",
1509
+ "aria-checked": id === m3.layout,
1510
+ "aria-label": `${t3.layout} ${id}`,
1511
+ title: id,
1512
+ onClick: () => {
1513
+ multi.setLayout(id);
1514
+ layout.setOpen(false);
1515
+ },
1516
+ children: /* @__PURE__ */ a2(LayoutGlyph, { id })
1517
+ }
1518
+ ))
1519
+ ] })) })
1520
+ ] }),
1521
+ /* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: sync.ref, children: [
1522
+ /* @__PURE__ */ a2(
1523
+ "button",
1524
+ {
1525
+ class: "fnc-btn fnc-btn-sync",
1526
+ title: t3.sync,
1527
+ "aria-label": t3.sync,
1528
+ "aria-haspopup": "menu",
1529
+ "aria-expanded": sync.open,
1530
+ "aria-pressed": anySync,
1531
+ onClick: () => sync.setOpen(!sync.open),
1532
+ children: /* @__PURE__ */ a2(LinkIcon, {})
1533
+ }
1534
+ ),
1535
+ sync.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-sync-menu", role: "menu", "aria-label": t3.sync, children: [
1536
+ /* @__PURE__ */ a2("div", { class: "fnc-menu-title", children: [
1537
+ t3.sync,
1538
+ /* @__PURE__ */ a2("small", { children: t3.syncHint })
1539
+ ] }),
1540
+ SYNC_KEYS.map(({ key, label }) => /* @__PURE__ */ a2(
1541
+ "button",
1542
+ {
1543
+ class: "fnc-menu-item fnc-sync-item",
1544
+ role: "menuitemcheckbox",
1545
+ "aria-checked": m3.sync[key],
1546
+ onClick: () => multi.setSync({ [key]: !m3.sync[key] }),
1547
+ children: [
1548
+ /* @__PURE__ */ a2("span", { children: t3[label] }),
1549
+ /* @__PURE__ */ a2("span", { class: "fnc-switch", "data-on": m3.sync[key] ? "true" : "false", "aria-hidden": "true" })
1550
+ ]
1551
+ }
1552
+ ))
1553
+ ] })
1554
+ ] })
1555
+ ] });
1556
+ }
1557
+
1558
+ // src/components/slot.tsx
1559
+ function Slot({ el, fixed = false }) {
1560
+ const ref = T2(null);
1561
+ F2(() => {
1562
+ ref.current?.appendChild(el);
1563
+ return () => el.remove();
1564
+ }, [el]);
1565
+ return /* @__PURE__ */ a2("div", { class: fixed ? "fnc-slot-host fnc-slot-host-fixed" : "fnc-slot-host", ref });
1566
+ }
1567
+
1568
+ // src/components/symbol-avatar.tsx
1569
+ function hue(text) {
1570
+ let h3 = 0;
1571
+ for (const ch of text) h3 = (h3 * 31 + ch.charCodeAt(0)) % 360;
1572
+ return h3;
1573
+ }
1574
+ function SymbolAvatar({ info, size = 24 }) {
1575
+ const [failed, setFailed] = d2(false);
1576
+ A2(() => setFailed(false), [info.logoUrl]);
1577
+ const style = { width: `${size}px`, height: `${size}px`, fontSize: `${Math.round(size * 0.46)}px` };
1578
+ if (info.logoUrl && !failed) {
1579
+ return /* @__PURE__ */ a2("img", { class: "fnc-avatar", src: info.logoUrl, alt: "", style, onError: () => setFailed(true) });
1580
+ }
1581
+ const h3 = hue(info.symbol);
1582
+ return /* @__PURE__ */ a2(
1583
+ "span",
1584
+ {
1585
+ class: "fnc-avatar fnc-avatar-initial",
1586
+ "aria-hidden": "true",
1587
+ style: { ...style, background: `hsl(${h3} 70% 50% / 0.16)`, color: `hsl(${h3} 60% 42%)` },
1588
+ children: (info.name || info.symbol).trim().charAt(0).toUpperCase()
1589
+ }
1590
+ );
1614
1591
  }
1615
1592
 
1616
1593
  // src/components/symbol-search.tsx
1617
- import { formatPrice as formatPrice2 } from "@funnest/charts";
1594
+ import { formatPrice } from "@funnest/charts";
1618
1595
  var DEBOUNCE_MS = 150;
1619
1596
  var RECENT_KEY = "recent-symbols";
1620
1597
  var FAVORITE_KEY = "favorite-symbols";
@@ -1712,7 +1689,7 @@ function SymbolSearch({ ctx }) {
1712
1689
  else return;
1713
1690
  e3.preventDefault();
1714
1691
  };
1715
- const row = (info, index, isRecent) => {
1692
+ const row2 = (info, index, isRecent) => {
1716
1693
  const cat = categoryOf(info);
1717
1694
  const fav = isFavorite(info);
1718
1695
  const change = info.changePercent ?? 0;
@@ -1757,7 +1734,7 @@ function SymbolSearch({ ctx }) {
1757
1734
  ] }),
1758
1735
  info.sparkline && /* @__PURE__ */ a2(Sparkline, { values: info.sparkline, up }),
1759
1736
  info.price !== void 0 && /* @__PURE__ */ a2("span", { class: "fnc-sr-quote", children: [
1760
- /* @__PURE__ */ a2("b", { children: formatPrice2(info.price, info.pricePrecision) }),
1737
+ /* @__PURE__ */ a2("b", { children: formatPrice(info.price, info.pricePrecision) }),
1761
1738
  info.changePercent !== void 0 && /* @__PURE__ */ a2("span", { class: trend, children: [
1762
1739
  change > 0 ? "\u2191 " : change < 0 ? "\u2193 " : "",
1763
1740
  Math.abs(info.changePercent).toFixed(2),
@@ -1825,9 +1802,9 @@ function SymbolSearch({ ctx }) {
1825
1802
  /* @__PURE__ */ a2("span", { children: t3.recentSearches }),
1826
1803
  /* @__PURE__ */ a2("button", { class: "fnc-link", onClick: () => save(RECENT_KEY, [], setRecent), children: t3.clearRecent })
1827
1804
  ] }),
1828
- recentRows.map((info, i3) => row(info, i3, true)),
1805
+ recentRows.map((info, i3) => row2(info, i3, true)),
1829
1806
  (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)),
1807
+ 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
1808
  !loading && rows.length === 0 && /* @__PURE__ */ a2("div", { class: "fnc-empty", children: view.kind === "favorites" && favorites.length === 0 ? t3.noFavorites : t3.noResults })
1832
1809
  ] })
1833
1810
  ] }),
@@ -1900,6 +1877,7 @@ function Toolbar({ ctx }) {
1900
1877
  ]
1901
1878
  }
1902
1879
  ),
1880
+ ctx.multi && /* @__PURE__ */ a2(LayoutMenu, { ctx, multi: ctx.multi }),
1903
1881
  /* @__PURE__ */ a2("div", { class: "fnc-spacer" }),
1904
1882
  ctx.drawingToolbar && /* @__PURE__ */ a2(
1905
1883
  "button",
@@ -1940,126 +1918,379 @@ function Toolbar({ ctx }) {
1940
1918
  ] });
1941
1919
  }
1942
1920
 
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; }
1921
+ // src/pane.tsx
1922
+ import { createChart } from "@funnest/charts";
2007
1923
 
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;
2044
- }
2045
- .fnc-intervals::-webkit-scrollbar { display: none; }
2046
- .fnc-chip {
2047
- height: 32px;
2048
- padding: 0 10px;
2049
- border-radius: var(--fnc-radius);
2050
- font-size: 13px;
2051
- font-weight: 600;
2052
- color: var(--fnc-text-2);
2053
- white-space: nowrap;
2054
- flex: none;
1924
+ // src/components/overlay.tsx
1925
+ import { formatCompact, formatPrice as formatPrice2 } from "@funnest/charts";
1926
+ var ZOOM_STEP = 1.25;
1927
+ var CONTROL_OFFSET = 52;
1928
+ var dirClass = (v3) => v3 > 0 ? "fnc-up" : v3 < 0 ? "fnc-down" : "fnc-flat";
1929
+ function IndicatorItem({ ctx, item }) {
1930
+ const t3 = STRINGS[ctx.store.get().locale];
1931
+ return /* @__PURE__ */ a2(
1932
+ "div",
1933
+ {
1934
+ class: item.visible ? "fnc-ind" : "fnc-ind fnc-ind-hidden",
1935
+ role: "button",
1936
+ tabIndex: 0,
1937
+ "aria-label": `${item.title} ${t3.indicatorSettings}`,
1938
+ onClick: (e3) => {
1939
+ if (e3.target.closest(".fnc-ind-actions")) return;
1940
+ ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } });
1941
+ },
1942
+ onKeyDown: (e3) => e3.key === "Enter" && ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
1943
+ children: [
1944
+ /* @__PURE__ */ a2(
1945
+ "button",
1946
+ {
1947
+ class: "fnc-ind-btn fnc-ind-eye",
1948
+ title: item.visible ? t3.hide : t3.show,
1949
+ "aria-label": `${item.title} ${item.visible ? t3.hide : t3.show}`,
1950
+ "aria-pressed": !item.visible,
1951
+ onClick: (e3) => {
1952
+ e3.stopPropagation();
1953
+ ctx.chart.setIndicatorVisible(item.id, !item.visible);
1954
+ },
1955
+ children: item.visible ? /* @__PURE__ */ a2(EyeIcon, {}) : /* @__PURE__ */ a2(EyeOffIcon, {})
1956
+ }
1957
+ ),
1958
+ /* @__PURE__ */ a2("span", { class: "fnc-ind-title", children: item.title }),
1959
+ item.visible && item.values.map((v3) => /* @__PURE__ */ a2("b", { style: { color: v3.color }, title: v3.label, children: v3.value })),
1960
+ /* @__PURE__ */ a2("span", { class: "fnc-ind-actions", children: [
1961
+ /* @__PURE__ */ a2(
1962
+ "button",
1963
+ {
1964
+ class: "fnc-ind-btn",
1965
+ title: t3.indicatorSettings,
1966
+ "aria-label": `${item.title} ${t3.indicatorSettings}`,
1967
+ onClick: () => ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
1968
+ children: /* @__PURE__ */ a2(GearIcon, {})
1969
+ }
1970
+ ),
1971
+ /* @__PURE__ */ a2(
1972
+ "button",
1973
+ {
1974
+ class: "fnc-ind-btn",
1975
+ title: t3.removeIndicator,
1976
+ "aria-label": `${item.title} ${t3.removeIndicator}`,
1977
+ onClick: () => ctx.chart.removeIndicator(item.id),
1978
+ children: /* @__PURE__ */ a2(SmallCloseIcon, {})
1979
+ }
1980
+ )
1981
+ ] })
1982
+ ]
1983
+ }
1984
+ );
2055
1985
  }
2056
- .fnc-chip[aria-pressed='true'] { color: var(--fnc-text); background: var(--fnc-surface); }
2057
- .fnc-spacer { flex: 1; min-width: 8px; }
2058
- .fnc-btn {
2059
- display: flex;
2060
- align-items: center;
2061
- justify-content: center;
2062
- width: 36px;
1986
+ function Legend({ ctx }) {
1987
+ const s3 = useStore(ctx.store);
1988
+ const t3 = STRINGS[s3.locale];
1989
+ const [hover, setHover] = d2(null);
1990
+ const [, refresh] = _2((n2) => n2 + 1, 0);
1991
+ const [collapsed, setCollapsed] = d2(false);
1992
+ A2(() => {
1993
+ const offs = [
1994
+ ctx.chart.on(
1995
+ "crosshairMove",
1996
+ (info) => setHover(info ? { bar: info.bar, prev: ctx.chart.bars[info.index - 1], index: info.index } : null)
1997
+ ),
1998
+ ctx.chart.on("dataChange", () => refresh(void 0))
1999
+ ];
2000
+ return () => offs.forEach((off) => off());
2001
+ }, [ctx.chart]);
2002
+ const ref = T2(null);
2003
+ A2(() => {
2004
+ const el = ref.current;
2005
+ if (!el) return;
2006
+ const update = () => ctx.chart.setTopInset(el.offsetTop + el.offsetHeight + 4);
2007
+ const ro = new ResizeObserver(update);
2008
+ ro.observe(el);
2009
+ return () => ro.disconnect();
2010
+ }, [ctx.chart]);
2011
+ if (!s3.symbol) return /* @__PURE__ */ a2("div", { class: "fnc-legend", ref });
2012
+ const bars = ctx.chart.bars;
2013
+ const bar = hover?.bar ?? bars[bars.length - 1];
2014
+ const prev = hover ? hover.prev : bars[bars.length - 2];
2015
+ const fmt = (p3) => formatPrice2(p3, s3.symbol.pricePrecision);
2016
+ let values = null;
2017
+ if (bar) {
2018
+ const cls = dirClass(bar.close - bar.open);
2019
+ const change = prev ? bar.close - prev.close : 0;
2020
+ const pct = prev && prev.close ? change / prev.close * 100 : 0;
2021
+ const sign = change > 0 ? "+" : "";
2022
+ values = /* @__PURE__ */ a2("div", { class: "fnc-legend-values", children: [
2023
+ /* @__PURE__ */ a2("span", { children: [
2024
+ t3.open,
2025
+ " ",
2026
+ /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.open) })
2027
+ ] }),
2028
+ /* @__PURE__ */ a2("span", { children: [
2029
+ t3.high,
2030
+ " ",
2031
+ /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.high) })
2032
+ ] }),
2033
+ /* @__PURE__ */ a2("span", { children: [
2034
+ t3.low,
2035
+ " ",
2036
+ /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.low) })
2037
+ ] }),
2038
+ /* @__PURE__ */ a2("span", { children: [
2039
+ t3.close_,
2040
+ " ",
2041
+ /* @__PURE__ */ a2("b", { class: cls, children: fmt(bar.close) })
2042
+ ] }),
2043
+ /* @__PURE__ */ a2("b", { class: dirClass(change), children: [
2044
+ sign,
2045
+ fmt(change),
2046
+ " (",
2047
+ sign,
2048
+ pct.toFixed(2),
2049
+ "%)"
2050
+ ] }),
2051
+ bar.volume !== void 0 && /* @__PURE__ */ a2("span", { children: [
2052
+ t3.volume,
2053
+ " ",
2054
+ /* @__PURE__ */ a2("b", { class: "fnc-flat", children: formatCompact(bar.volume, s3.locale) })
2055
+ ] })
2056
+ ] });
2057
+ }
2058
+ const items = s3.indicators.length ? ctx.chart.indicatorLegend(hover?.index) : [];
2059
+ const overlayItems = items.filter((i3) => i3.overlay);
2060
+ const paneLegends = s3.layout.panes.filter((p3) => p3.kind === "indicator").map((pane) => ({ pane, item: items.find((i3) => i3.id === pane.id) }));
2061
+ return /* @__PURE__ */ a2($, { children: [
2062
+ /* @__PURE__ */ a2("div", { class: "fnc-legend", ref, children: [
2063
+ /* @__PURE__ */ a2("div", { class: "fnc-legend-title", children: [
2064
+ ctx.showLogo && /* @__PURE__ */ a2(SymbolAvatar, { info: s3.symbol, size: 18 }),
2065
+ /* @__PURE__ */ a2("b", { children: s3.symbol.name }),
2066
+ /* @__PURE__ */ a2("span", { children: "\xB7" }),
2067
+ /* @__PURE__ */ a2("span", { children: resolutionLabel(s3.resolution, s3.locale) }),
2068
+ /* @__PURE__ */ a2("span", { children: "\xB7" }),
2069
+ /* @__PURE__ */ a2("span", { children: s3.symbol.exchange }),
2070
+ s3.loading && /* @__PURE__ */ a2("span", { class: "fnc-spinner", role: "status" })
2071
+ ] }),
2072
+ values,
2073
+ !collapsed && overlayItems.map((item) => /* @__PURE__ */ a2(IndicatorItem, { ctx, item })),
2074
+ 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, {}) }))
2075
+ ] }),
2076
+ paneLegends.map(
2077
+ ({ pane, item }) => item && /* @__PURE__ */ a2("div", { class: "fnc-legend fnc-pane-legend", style: { top: `${pane.top + 4}px` }, children: /* @__PURE__ */ a2(IndicatorItem, { ctx, item }) })
2078
+ )
2079
+ ] });
2080
+ }
2081
+ function NavControls({ ctx }) {
2082
+ const s3 = useStore(ctx.store);
2083
+ const t3 = STRINGS[s3.locale];
2084
+ const [showButtons, setShowButtons] = d2(ctx.chart.settings.navigationButtons);
2085
+ A2(() => ctx.chart.on("settingsChange", (settings) => setShowButtons(settings.navigationButtons)), [ctx.chart]);
2086
+ const { plotWidth, panesHeight } = s3.layout;
2087
+ if (plotWidth <= 0) return null;
2088
+ const top = panesHeight - CONTROL_OFFSET;
2089
+ const page = () => Math.max(1, Math.round(plotWidth / ctx.chart.timeScale.barSpacing / 3));
2090
+ const button = (label, onClick, icon) => /* @__PURE__ */ a2("button", { class: "fnc-btn", title: label, "aria-label": label, onClick, children: icon });
2091
+ return /* @__PURE__ */ a2($, { children: [
2092
+ showButtons && /* @__PURE__ */ a2("div", { class: "fnc-nav", style: { left: `${plotWidth / 2}px`, top: `${top}px` }, children: [
2093
+ button(t3.zoomOut, () => ctx.chart.zoom(1 / ZOOM_STEP), /* @__PURE__ */ a2(MinusIcon, {})),
2094
+ button(t3.zoomIn, () => ctx.chart.zoom(ZOOM_STEP), /* @__PURE__ */ a2(PlusIcon, {})),
2095
+ button(t3.scrollBack, () => ctx.chart.scrollBars(-page()), /* @__PURE__ */ a2(ChevronLeftIcon, {})),
2096
+ button(t3.scrollForward, () => ctx.chart.scrollBars(page()), /* @__PURE__ */ a2(ChevronRightIcon, {})),
2097
+ button(t3.resetView, () => ctx.chart.resetView(), /* @__PURE__ */ a2(ResetIcon, {}))
2098
+ ] }),
2099
+ !s3.atRealtime && /* @__PURE__ */ a2(
2100
+ "button",
2101
+ {
2102
+ class: "fnc-realtime",
2103
+ style: { left: `${plotWidth - 44}px`, top: `${top}px` },
2104
+ title: t3.toRealtime,
2105
+ "aria-label": t3.toRealtime,
2106
+ onClick: () => ctx.chart.scrollToRealtime(),
2107
+ children: /* @__PURE__ */ a2(ToRealtimeIcon, {})
2108
+ }
2109
+ )
2110
+ ] });
2111
+ }
2112
+ var ATTRIBUTION_URL = "https://charts.funnest.xyz/?utm_source=chart&utm_medium=attribution";
2113
+ function Attribution({ ctx }) {
2114
+ const s3 = useStore(ctx.store);
2115
+ const { panesHeight, plotWidth } = s3.layout;
2116
+ if (!ctx.attribution || plotWidth <= 0) return null;
2117
+ const label = s3.locale === "ko" ? "Funnest \uCC28\uD2B8" : "Charts by Funnest";
2118
+ return /* @__PURE__ */ a2(
2119
+ "a",
2120
+ {
2121
+ class: "fnc-attribution",
2122
+ href: ATTRIBUTION_URL,
2123
+ target: "_blank",
2124
+ rel: "noopener",
2125
+ title: label,
2126
+ "aria-label": label,
2127
+ style: { top: `${panesHeight - 36}px` },
2128
+ children: [
2129
+ /* @__PURE__ */ a2(FunnestMark, {}),
2130
+ /* @__PURE__ */ a2("span", { children: "funnest" })
2131
+ ]
2132
+ }
2133
+ );
2134
+ }
2135
+ function PaneTools({ ctx }) {
2136
+ return ctx.multi ? /* @__PURE__ */ a2(PaneMaximize, { ctx, multi: ctx.multi }) : null;
2137
+ }
2138
+ function PaneMaximize({ ctx, multi }) {
2139
+ const s3 = useStore(ctx.store);
2140
+ const m3 = useStore(multi.store);
2141
+ if (s3.layout.plotWidth <= 0 || m3.narrow || m3.layout === "1" && m3.maximized === null) return null;
2142
+ const t3 = STRINGS[s3.locale];
2143
+ const maximized = m3.maximized !== null;
2144
+ const label = maximized ? t3.restorePane : t3.maximizePane;
2145
+ return /* @__PURE__ */ a2(
2146
+ "button",
2147
+ {
2148
+ class: "fnc-btn fnc-pane-max",
2149
+ style: { left: `${s3.layout.plotWidth - 36}px` },
2150
+ title: `${label} (Alt+Enter)`,
2151
+ "aria-label": label,
2152
+ "aria-pressed": maximized,
2153
+ onClick: () => {
2154
+ const index = multi.store.get().active;
2155
+ multi.toggleMaximize(index);
2156
+ },
2157
+ children: maximized ? /* @__PURE__ */ a2(RestoreIcon, {}) : /* @__PURE__ */ a2(MaximizeIcon, {})
2158
+ }
2159
+ );
2160
+ }
2161
+ function Overlay({ ctx }) {
2162
+ return /* @__PURE__ */ a2($, { children: [
2163
+ /* @__PURE__ */ a2(Attribution, { ctx }),
2164
+ /* @__PURE__ */ a2(Legend, { ctx }),
2165
+ /* @__PURE__ */ a2(NavControls, { ctx }),
2166
+ /* @__PURE__ */ a2(SelectionBar, { ctx }),
2167
+ /* @__PURE__ */ a2(PaneTools, { ctx })
2168
+ ] });
2169
+ }
2170
+
2171
+ // src/shell.ts
2172
+ import { darkTheme, lightTheme } from "@funnest/charts";
2173
+
2174
+ // src/storage.ts
2175
+ function createStorage(prefix) {
2176
+ const memory = /* @__PURE__ */ new Map();
2177
+ const ls = (() => {
2178
+ try {
2179
+ return prefix && typeof localStorage !== "undefined" ? localStorage : null;
2180
+ } catch {
2181
+ return null;
2182
+ }
2183
+ })();
2184
+ const full = (key) => `${prefix}:${key}`;
2185
+ return {
2186
+ get(key, fallback) {
2187
+ try {
2188
+ const raw = ls ? ls.getItem(full(key)) : memory.get(key) ?? null;
2189
+ return raw === null ? fallback : JSON.parse(raw);
2190
+ } catch {
2191
+ return fallback;
2192
+ }
2193
+ },
2194
+ set(key, value) {
2195
+ const raw = JSON.stringify(value);
2196
+ try {
2197
+ if (ls) ls.setItem(full(key), raw);
2198
+ else memory.set(key, raw);
2199
+ } catch {
2200
+ memory.set(key, raw);
2201
+ }
2202
+ }
2203
+ };
2204
+ }
2205
+
2206
+ // src/styles.ts
2207
+ var CSS = (
2208
+ /* css */
2209
+ `
2210
+ .fnc-root {
2211
+ position: relative;
2212
+ display: flex;
2213
+ flex-direction: column;
2214
+ width: 100%;
2215
+ height: 100%;
2216
+ min-height: 0;
2217
+ background: var(--fnc-bg);
2218
+ color: var(--fnc-text);
2219
+ font-family: var(--fnc-font);
2220
+ font-size: 14px;
2221
+ line-height: 1.4;
2222
+ -webkit-font-smoothing: antialiased;
2223
+ overflow: hidden;
2224
+ }
2225
+ .fnc-root *, .fnc-root *::before, .fnc-root *::after { box-sizing: border-box; }
2226
+ .fnc-root.fnc-pseudo-fullscreen { position: fixed; inset: 0; z-index: 2147483000; }
2227
+ :where(.fnc-root) button {
2228
+ font: inherit;
2229
+ color: inherit;
2230
+ background: none;
2231
+ border: 0;
2232
+ margin: 0;
2233
+ padding: 0;
2234
+ cursor: pointer;
2235
+ -webkit-tap-highlight-color: transparent;
2236
+ }
2237
+ .fnc-root button:focus-visible { outline: 2px solid var(--fnc-accent); outline-offset: 1px; }
2238
+
2239
+ /* \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 */
2240
+ .fnc-toolbar {
2241
+ display: flex;
2242
+ align-items: center;
2243
+ gap: 4px;
2244
+ height: 48px;
2245
+ padding: 0 8px;
2246
+ border-bottom: 1px solid var(--fnc-border);
2247
+ flex: none;
2248
+ }
2249
+ .fnc-symbol {
2250
+ display: flex;
2251
+ align-items: center;
2252
+ gap: 6px;
2253
+ height: 36px;
2254
+ max-width: 220px;
2255
+ padding: 0 10px 0 8px;
2256
+ border-radius: var(--fnc-radius);
2257
+ font-weight: 700;
2258
+ font-size: 15px;
2259
+ /* The symbol gives way first when the toolbar is tight: its name truncates, never overlaps. */
2260
+ flex: 0 1 auto;
2261
+ min-width: 36px;
2262
+ overflow: hidden;
2263
+ }
2264
+ .fnc-symbol .fnc-icon { color: var(--fnc-text-2); display: flex; }
2265
+ .fnc-symbol-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2266
+ .fnc-symbol:not(:disabled):hover, .fnc-btn:hover, .fnc-chip:hover { background: var(--fnc-surface); }
2267
+ .fnc-symbol:disabled { cursor: default; }
2268
+ .fnc-sep { width: 1px; height: 20px; background: var(--fnc-border); margin: 0 4px; flex: none; }
2269
+ .fnc-intervals {
2270
+ display: flex;
2271
+ gap: 2px;
2272
+ overflow-x: auto;
2273
+ scrollbar-width: none;
2274
+ min-width: 0;
2275
+ }
2276
+ .fnc-intervals::-webkit-scrollbar { display: none; }
2277
+ .fnc-chip {
2278
+ height: 32px;
2279
+ padding: 0 10px;
2280
+ border-radius: var(--fnc-radius);
2281
+ font-size: 13px;
2282
+ font-weight: 600;
2283
+ color: var(--fnc-text-2);
2284
+ white-space: nowrap;
2285
+ flex: none;
2286
+ }
2287
+ .fnc-chip[aria-pressed='true'] { color: var(--fnc-text); background: var(--fnc-surface); }
2288
+ .fnc-spacer { flex: 1; min-width: 8px; }
2289
+ .fnc-btn {
2290
+ display: flex;
2291
+ align-items: center;
2292
+ justify-content: center;
2293
+ width: 36px;
2063
2294
  height: 36px;
2064
2295
  border-radius: var(--fnc-radius);
2065
2296
  color: var(--fnc-text-2);
@@ -2114,7 +2345,9 @@ var CSS = (
2114
2345
 
2115
2346
  /* \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
2347
  .fnc-body { position: relative; display: flex; flex: 1; min-height: 0; }
2117
- .fnc-stage { position: relative; flex: 1; min-width: 0; }
2348
+ .fnc-main { position: relative; display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; }
2349
+ .fnc-stage { position: relative; flex: 1; min-width: 0; min-height: 0; }
2350
+ .fnc-slot-bottom { flex: none; min-width: 0; }
2118
2351
  .fnc-chart-host, .fnc-overlay { position: absolute; inset: 0; }
2119
2352
 
2120
2353
  /* \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 */
@@ -2699,6 +2932,103 @@ var CSS = (
2699
2932
  .fnc-toolbar > .fnc-spacer { min-width: 0; }
2700
2933
  }
2701
2934
 
2935
+ /* \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 */
2936
+ .fnc-grid { display: flex; flex: 1; min-width: 0; min-height: 0; }
2937
+ .fnc-split { display: flex; min-width: 0; min-height: 0; }
2938
+ .fnc-split-row { flex-direction: row; }
2939
+ .fnc-split-col { flex-direction: column; }
2940
+ .fnc-pane { position: relative; display: flex; min-width: 0; min-height: 0; }
2941
+ .fnc-pane > .fnc-stage { flex: 1; min-height: 0; }
2942
+ .fnc-split-on .fnc-pane-active::after {
2943
+ content: '';
2944
+ position: absolute;
2945
+ inset: 0;
2946
+ border: 1px solid var(--fnc-accent);
2947
+ pointer-events: none;
2948
+ z-index: 3;
2949
+ }
2950
+ .fnc-splitter { position: relative; flex: none; background: var(--fnc-border); z-index: 4; touch-action: none; }
2951
+ .fnc-splitter::after { content: ''; position: absolute; }
2952
+ .fnc-splitter-row { width: 1px; cursor: col-resize; }
2953
+ .fnc-splitter-row::after { inset: 0 -4px; }
2954
+ .fnc-splitter-col { height: 1px; cursor: row-resize; }
2955
+ .fnc-splitter-col::after { inset: -4px 0; }
2956
+ .fnc-splitter:hover { background: var(--fnc-accent); }
2957
+ .fnc-resizing-x, .fnc-resizing-x * { cursor: col-resize !important; user-select: none; }
2958
+ .fnc-resizing-y, .fnc-resizing-y * { cursor: row-resize !important; user-select: none; }
2959
+ .fnc-parking { display: none; }
2960
+ /* Navigation controls follow the hovered pane, not the whole widget. */
2961
+ .fnc-multi .fnc-body:hover .fnc-nav { opacity: 0; }
2962
+ .fnc-multi .fnc-stage:hover .fnc-nav, .fnc-multi .fnc-nav:focus-within { opacity: 1; }
2963
+ .fnc-pane-max {
2964
+ position: absolute;
2965
+ top: 6px;
2966
+ width: 28px;
2967
+ height: 28px;
2968
+ opacity: 0;
2969
+ background: var(--fnc-bg);
2970
+ pointer-events: auto;
2971
+ z-index: 2;
2972
+ }
2973
+ .fnc-stage:hover .fnc-pane-max, .fnc-pane-max:focus-visible, .fnc-pane-max[aria-pressed='true'] { opacity: 1; }
2974
+ @media (hover: none) { .fnc-pane-max { opacity: 0.7; } }
2975
+ .fnc-pane-tabs {
2976
+ display: flex;
2977
+ gap: 4px;
2978
+ padding: 4px 8px;
2979
+ overflow-x: auto;
2980
+ scrollbar-width: none;
2981
+ border-bottom: 1px solid var(--fnc-border);
2982
+ }
2983
+ .fnc-pane-tab {
2984
+ display: flex;
2985
+ align-items: center;
2986
+ gap: 6px;
2987
+ flex: none;
2988
+ height: 30px;
2989
+ padding: 0 10px 0 6px;
2990
+ border-radius: var(--fnc-radius);
2991
+ font-size: 13px;
2992
+ font-weight: 600;
2993
+ color: var(--fnc-text-2);
2994
+ white-space: nowrap;
2995
+ }
2996
+ .fnc-pane-tab small { font-size: 12px; font-weight: 400; color: var(--fnc-text-2); }
2997
+ .fnc-pane-tab[aria-selected='true'] { background: var(--fnc-surface); color: var(--fnc-text); }
2998
+ .fnc-pane-tab-num {
2999
+ display: grid;
3000
+ place-items: center;
3001
+ width: 18px;
3002
+ height: 18px;
3003
+ border-radius: 5px;
3004
+ background: var(--fnc-surface);
3005
+ font-size: 11px;
3006
+ }
3007
+ .fnc-pane-tab[aria-selected='true'] .fnc-pane-tab-num { background: var(--fnc-accent); color: #fff; }
3008
+ .fnc-layout-menu { min-width: 0; gap: 2px; padding: 8px; }
3009
+ .fnc-layout-row { display: flex; align-items: center; gap: 2px; }
3010
+ .fnc-layout-count { width: 16px; flex: none; font-size: 12px; color: var(--fnc-text-2); text-align: center; }
3011
+ .fnc-layout-opt {
3012
+ display: grid;
3013
+ place-items: center;
3014
+ width: 36px;
3015
+ height: 32px;
3016
+ border-radius: var(--fnc-radius);
3017
+ color: var(--fnc-text-2);
3018
+ }
3019
+ .fnc-layout-opt:hover { background: var(--fnc-surface); color: var(--fnc-text); }
3020
+ .fnc-layout-opt[aria-checked='true'] { background: var(--fnc-surface); color: var(--fnc-accent); }
3021
+ .fnc-sync-menu { min-width: 220px; }
3022
+ .fnc-menu-title { display: flex; flex-direction: column; gap: 2px; padding: 6px 10px 8px; font-size: 13px; font-weight: 600; color: var(--fnc-text); }
3023
+ .fnc-menu-title small { font-size: 12px; font-weight: 400; color: var(--fnc-text-2); }
3024
+ .fnc-sync-item { justify-content: space-between; }
3025
+ .fnc-sync-item[aria-checked='true'] { background: none; color: var(--fnc-text); }
3026
+ .fnc-sync-item:hover { background: var(--fnc-surface); }
3027
+ .fnc-sync-item .fnc-switch { display: inline-block; transform: scale(0.8); transform-origin: right center; }
3028
+ .fnc-switch[data-on='true'] { background: var(--fnc-accent); }
3029
+ .fnc-switch[data-on='true']::after { transform: translateX(16px); }
3030
+ .fnc-btn-sync[aria-pressed='true'] { color: var(--fnc-accent); }
3031
+
2702
3032
  /* \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
3033
  @media (max-width: 640px) {
2704
3034
  .fnc-toolbar { padding: 0 4px; gap: 2px; }
@@ -2711,6 +3041,7 @@ var CSS = (
2711
3041
  .fnc-chip { padding: 0 8px; }
2712
3042
  .fnc-btn-screenshot { display: none; }
2713
3043
  .fnc-btn-label span { display: none; }
3044
+ .fnc-btn-sync { display: none; }
2714
3045
  .fnc-intervals { display: none; }
2715
3046
  .fnc-interval-current { display: inline; }
2716
3047
  .fnc-interval-menu { left: auto; right: -40px; }
@@ -2752,21 +3083,7 @@ function injectStyles(doc = document) {
2752
3083
  doc.head.appendChild(style);
2753
3084
  }
2754
3085
 
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
- ];
3086
+ // src/shell.ts
2770
3087
  var THEME_VARS = {
2771
3088
  background: "--fnc-bg",
2772
3089
  surface: "--fnc-surface",
@@ -2785,37 +3102,19 @@ function applyThemeVars(el, theme) {
2785
3102
  el.style.setProperty("--fnc-radius", `${theme.radius}px`);
2786
3103
  }
2787
3104
  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);
3105
+ const el = document.createElement("div");
3106
+ el.className = className;
3107
+ parent?.appendChild(el);
3108
+ return el;
3109
+ }
3110
+ function createShell(container, options) {
2814
3111
  injectStyles(container.ownerDocument);
3112
+ const storage = createStorage(options.storageKey);
2815
3113
  const win = container.ownerDocument.defaultView;
2816
3114
  const darkQuery = win?.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
2817
- let scheme = colorScheme;
2818
- let themePatch = { ...chartOptions.theme };
3115
+ let scheme = options.colorScheme;
3116
+ let themePatch = { ...options.theme };
3117
+ let panes = () => [];
2819
3118
  const isDark = () => scheme === "system" ? !!darkQuery?.matches : scheme === "dark";
2820
3119
  const resolveTheme = () => ({ ...isDark() ? darkTheme : lightTheme, ...themePatch });
2821
3120
  let theme = resolveTheme();
@@ -2831,33 +3130,138 @@ function createTradingChart(container, options) {
2831
3130
  toolbar: div("fnc-slot fnc-slot-toolbar"),
2832
3131
  toolbarEnd: div("fnc-slot fnc-slot-toolbar-end"),
2833
3132
  belowToolbar: div("fnc-slot fnc-slot-below-toolbar", root),
3133
+ bottom: div("fnc-slot fnc-slot-bottom"),
2834
3134
  footer: div("fnc-slot fnc-slot-footer")
2835
3135
  };
2836
3136
  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);
3137
+ const drawbarHost = options.drawingToolbar ? div("fnc-drawbar-host", body) : null;
3138
+ const main = div("fnc-main", body);
3139
+ main.appendChild(slots.bottom);
2841
3140
  root.appendChild(slots.footer);
2842
- const chart = createChart(chartHost, { ...chartOptions, theme, datafeed, displayTimezone });
2843
3141
  const refreshScheme = () => {
2844
3142
  theme = resolveTheme();
2845
3143
  applyScheme();
2846
- chart.setTheme(theme);
3144
+ for (const p3 of panes()) p3.chart.setTheme(theme);
2847
3145
  };
2848
3146
  const onSchemeChange = () => scheme === "system" && refreshScheme();
2849
3147
  darkQuery?.addEventListener?.("change", onSchemeChange);
2850
- chart.setChartType(chartType);
3148
+ let favoriteIntervals = storage.get("favorite-intervals", [...options.favoriteIntervals]);
3149
+ let drawbarCollapsed = storage.get("drawbar-collapsed", false);
3150
+ const broadcast = (patch) => {
3151
+ for (const p3 of panes()) p3.store.set(patch);
3152
+ };
3153
+ const isNativeFullscreen = () => document.fullscreenElement === root;
3154
+ const isFullscreen = () => isNativeFullscreen() || root.classList.contains("fnc-pseudo-fullscreen");
3155
+ const onFullscreenChange = () => broadcast({ fullscreen: isFullscreen() });
3156
+ document.addEventListener("fullscreenchange", onFullscreenChange);
3157
+ const pseudo = (on) => {
3158
+ root.classList.toggle("fnc-pseudo-fullscreen", on);
3159
+ onFullscreenChange();
3160
+ };
3161
+ return {
3162
+ root,
3163
+ toolbarHost,
3164
+ body,
3165
+ drawbarHost,
3166
+ main,
3167
+ slots,
3168
+ storage,
3169
+ theme: () => theme,
3170
+ sharedState: () => ({ fullscreen: isFullscreen(), favoriteIntervals, drawbarCollapsed }),
3171
+ setPanes: (get) => {
3172
+ panes = get;
3173
+ },
3174
+ setTheme: (patch) => {
3175
+ themePatch = { ...themePatch, ...patch };
3176
+ refreshScheme();
3177
+ },
3178
+ setColorScheme: (next) => {
3179
+ scheme = next;
3180
+ refreshScheme();
3181
+ },
3182
+ isFullscreen,
3183
+ toggleFullscreen: () => {
3184
+ if (isNativeFullscreen()) void document.exitFullscreen();
3185
+ else if (root.classList.contains("fnc-pseudo-fullscreen")) pseudo(false);
3186
+ else if (root.requestFullscreen && document.fullscreenEnabled) root.requestFullscreen().catch(() => pseudo(true));
3187
+ else pseudo(true);
3188
+ },
3189
+ setDrawbarCollapsed: (collapsed) => {
3190
+ drawbarCollapsed = collapsed;
3191
+ storage.set("drawbar-collapsed", collapsed);
3192
+ broadcast({ drawbarCollapsed });
3193
+ },
3194
+ toggleFavoriteInterval: (res) => {
3195
+ favoriteIntervals = favoriteIntervals.includes(res) ? favoriteIntervals.filter((r3) => r3 !== res) : [...favoriteIntervals, res];
3196
+ storage.set("favorite-intervals", favoriteIntervals);
3197
+ broadcast({ favoriteIntervals });
3198
+ options.onFavoriteIntervalsChange?.(favoriteIntervals);
3199
+ },
3200
+ destroy: () => {
3201
+ document.removeEventListener("fullscreenchange", onFullscreenChange);
3202
+ darkQuery?.removeEventListener?.("change", onSchemeChange);
3203
+ if (isNativeFullscreen()) void document.exitFullscreen();
3204
+ root.remove();
3205
+ }
3206
+ };
3207
+ }
3208
+ function bindWidgetKeys(shell, active, searchHotkey, extra) {
3209
+ const { root } = shell;
3210
+ const onKeyDown = (e3) => {
3211
+ const pane = active();
3212
+ if (!pane) return;
3213
+ const s3 = pane.store.get();
3214
+ if (searchHotkey && (e3.ctrlKey || e3.metaKey) && e3.key.toLowerCase() === "k" && pane.canSearch && !s3.dialog) {
3215
+ e3.preventDefault();
3216
+ pane.store.set({ searchOpen: true });
3217
+ return;
3218
+ }
3219
+ if (e3.key === "Escape" && !e3.defaultPrevented && (s3.searchOpen || s3.dialog)) {
3220
+ e3.preventDefault();
3221
+ pane.store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
3222
+ return;
3223
+ }
3224
+ if (e3.key === "Escape" && root.classList.contains("fnc-pseudo-fullscreen") && !s3.searchOpen && !s3.dialog) {
3225
+ shell.toggleFullscreen();
3226
+ return;
3227
+ }
3228
+ if (extra?.(e3)) e3.preventDefault();
3229
+ };
3230
+ const onKeyUp = (e3) => {
3231
+ const pane = active();
3232
+ if (!pane) return;
3233
+ const s3 = pane.store.get();
3234
+ if (e3.key === "Escape" && (s3.searchOpen || s3.dialog)) pane.store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
3235
+ };
3236
+ document.addEventListener("keydown", onKeyDown);
3237
+ document.addEventListener("keyup", onKeyUp);
3238
+ return () => {
3239
+ document.removeEventListener("keydown", onKeyDown);
3240
+ document.removeEventListener("keyup", onKeyUp);
3241
+ };
3242
+ }
3243
+
3244
+ // src/pane.tsx
3245
+ function createPane(shell, config) {
3246
+ const stage = div("fnc-stage");
3247
+ const chartHost = div("fnc-chart-host", stage);
3248
+ const overlayHost = div("fnc-overlay", stage);
3249
+ const chart = createChart(chartHost, {
3250
+ ...config.chartOptions,
3251
+ theme: shell.theme(),
3252
+ datafeed: config.datafeed,
3253
+ displayTimezone: config.displayTimezone
3254
+ });
3255
+ chart.setChartType(config.chartType);
2851
3256
  const store = new Store({
2852
- locale,
3257
+ locale: config.chartOptions.locale ?? "ko",
2853
3258
  symbol: null,
2854
- resolution,
3259
+ resolution: config.resolution,
2855
3260
  loading: false,
2856
3261
  layout: chart.layout,
2857
3262
  atRealtime: true,
2858
- fullscreen: false,
2859
3263
  searchOpen: false,
2860
- chartType,
3264
+ chartType: config.chartType,
2861
3265
  drawingTool: null,
2862
3266
  selection: null,
2863
3267
  drawingCount: 0,
@@ -2865,12 +3269,11 @@ function createTradingChart(container, options) {
2865
3269
  magnet: chart.magnetEnabled,
2866
3270
  indicators: chart.getIndicators(),
2867
3271
  dialog: null,
2868
- favoriteIntervals: storage.get("favorite-intervals", [...favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS]),
2869
- drawbarCollapsed: storage.get("drawbar-collapsed", false)
3272
+ ...shell.sharedState()
2870
3273
  });
2871
3274
  const syncRealtime = () => store.set({ atRealtime: chart.isAtRealtime() });
2872
3275
  const offs = [
2873
- chart.on("symbolChange", (info) => store.set({ symbol: info, resolution: chart.currentResolution ?? resolution })),
3276
+ chart.on("symbolChange", (info) => store.set({ symbol: info, resolution: chart.currentResolution ?? store.get().resolution })),
2874
3277
  chart.on("loadingChange", (loading) => store.set({ loading })),
2875
3278
  chart.on("layoutChange", (layout) => store.set({ layout })),
2876
3279
  chart.on("visibleRangeChange", syncRealtime),
@@ -2880,7 +3283,7 @@ function createTradingChart(container, options) {
2880
3283
  chart.on("drawingsChange", (drawings) => store.set({ drawingCount: drawings.length })),
2881
3284
  chart.on("indicatorsChange", (indicators) => store.set({ indicators })),
2882
3285
  chart.on("magnetChange", (magnet) => store.set({ magnet })),
2883
- chart.on("chartTypeChange", (chartType2) => store.set({ chartType: chartType2 }))
3286
+ chart.on("chartTypeChange", (chartType) => store.set({ chartType }))
2884
3287
  ];
2885
3288
  const load = (sym, res) => {
2886
3289
  store.set({ resolution: res });
@@ -2889,61 +3292,27 @@ function createTradingChart(container, options) {
2889
3292
  store.set({ loading: false });
2890
3293
  });
2891
3294
  };
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
3295
  const ctx = {
2919
- root,
3296
+ root: shell.root,
2920
3297
  chart,
2921
- datafeed,
3298
+ datafeed: config.datafeed,
2922
3299
  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
- },
3300
+ storage: shell.storage,
3301
+ intervalGroups: config.intervalGroups,
3302
+ searchCategories: config.searchCategories,
3303
+ displayTimezone: config.displayTimezone,
3304
+ showLogo: config.symbolLogo,
3305
+ drawingToolbar: config.drawingToolbar,
3306
+ attribution: config.attribution,
3307
+ slots: shell.slots,
3308
+ multi: config.multi,
3309
+ setDrawbarCollapsed: shell.setDrawbarCollapsed,
2935
3310
  selectSymbol: (sym) => void load(sym, store.get().resolution),
2936
3311
  selectResolution: (res) => {
2937
3312
  const current = store.get().symbol;
2938
3313
  if (current) void load(current.symbol, res);
2939
3314
  },
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
- },
3315
+ toggleFavoriteInterval: shell.toggleFavoriteInterval,
2947
3316
  setSearchOpen: (open) => store.set({ searchOpen: open }),
2948
3317
  screenshot: () => {
2949
3318
  const s3 = store.get();
@@ -2956,61 +3325,491 @@ function createTradingChart(container, options) {
2956
3325
  setTimeout(() => URL.revokeObjectURL(a4.href), 1e3);
2957
3326
  });
2958
3327
  },
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
- }
3328
+ toggleFullscreen: shell.toggleFullscreen
2975
3329
  };
2976
- Q(/* @__PURE__ */ a2(Toolbar, { ctx }), toolbarHost);
2977
3330
  Q(/* @__PURE__ */ a2(Overlay, { ctx }), overlayHost);
2978
- if (drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx }), drawbarHost);
2979
- void load(symbol, resolution);
2980
3331
  return {
3332
+ stage,
2981
3333
  chart,
2982
- slots,
2983
- setSymbol: (sym, res) => load(sym, res ?? store.get().resolution),
3334
+ store,
3335
+ ctx,
3336
+ canSearch: typeof config.datafeed.searchSymbols === "function",
3337
+ load,
3338
+ destroy: () => {
3339
+ offs.forEach((off) => off());
3340
+ Q(null, overlayHost);
3341
+ chart.destroy();
3342
+ stage.remove();
3343
+ }
3344
+ };
3345
+ }
3346
+
3347
+ // src/trading-chart.tsx
3348
+ var DEFAULT_INTERVAL_GROUPS = [
3349
+ { id: "minutes", intervals: ["1", "5", "15", "30"] },
3350
+ { id: "hours", intervals: ["60", "240"] },
3351
+ { id: "days", intervals: ["1D", "1W", "1M", "3M", "6M", "12M"] }
3352
+ ];
3353
+ var DEFAULT_FAVORITE_INTERVALS = ["1", "5", "15", "60", "240", "1D"];
3354
+ var stockLike = (i3) => i3.type === "stock" || i3.type === "etf" || i3.type === "index";
3355
+ var DEFAULT_SEARCH_CATEGORIES = [
3356
+ { id: "all", label: { ko: "\uC804\uCCB4", en: "All" }, match: () => true },
3357
+ { id: "kr-stock", label: { ko: "\uAD6D\uB0B4\uC8FC\uC2DD", en: "KR stocks" }, match: (i3) => stockLike(i3) && i3.country === "KR" },
3358
+ { id: "global-stock", label: { ko: "\uD574\uC678\uC8FC\uC2DD", en: "Global stocks" }, match: (i3) => stockLike(i3) && i3.country !== "KR" },
3359
+ { id: "crypto", label: { ko: "\uCF54\uC778", en: "Crypto" }, match: (i3) => i3.type === "crypto" },
3360
+ { id: "futures", label: { ko: "\uC120\uBB3C", en: "Futures" }, match: (i3) => i3.type === "futures" }
3361
+ ];
3362
+ function defaultDisplayTimezone(locale, explicit) {
3363
+ return explicit ?? (locale === "ko" ? "Asia/Seoul" : Intl.DateTimeFormat().resolvedOptions().timeZone);
3364
+ }
3365
+ function createTradingChart(container, options) {
3366
+ const {
3367
+ datafeed,
3368
+ symbol,
3369
+ resolution = "1D",
3370
+ intervalGroups = DEFAULT_INTERVAL_GROUPS,
3371
+ favoriteIntervals,
3372
+ onFavoriteIntervalsChange,
3373
+ searchCategories = DEFAULT_SEARCH_CATEGORIES,
3374
+ storageKey = "funnest-charts",
3375
+ searchHotkey = true,
3376
+ symbolLogo = true,
3377
+ drawingToolbar = true,
3378
+ chartType = "candles",
3379
+ attribution = true,
3380
+ colorScheme = "system",
3381
+ theme,
3382
+ ...chartOptions
3383
+ } = options;
3384
+ const displayTimezone = defaultDisplayTimezone(chartOptions.locale ?? "ko", chartOptions.displayTimezone);
3385
+ const shell = createShell(container, {
3386
+ colorScheme,
3387
+ theme,
3388
+ storageKey,
3389
+ drawingToolbar,
3390
+ favoriteIntervals: favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS,
3391
+ onFavoriteIntervalsChange
3392
+ });
3393
+ const pane = createPane(shell, {
3394
+ datafeed,
3395
+ chartOptions,
3396
+ displayTimezone,
3397
+ resolution,
3398
+ chartType,
3399
+ intervalGroups,
3400
+ searchCategories,
3401
+ symbolLogo,
3402
+ drawingToolbar,
3403
+ attribution
3404
+ });
3405
+ shell.main.insertBefore(pane.stage, shell.slots.bottom);
3406
+ shell.setPanes(() => [pane]);
3407
+ const unbindKeys = bindWidgetKeys(shell, () => pane, searchHotkey);
3408
+ Q(/* @__PURE__ */ a2(Toolbar, { ctx: pane.ctx }), shell.toolbarHost);
3409
+ if (shell.drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx: pane.ctx }), shell.drawbarHost);
3410
+ void pane.load(symbol, resolution);
3411
+ return {
3412
+ chart: pane.chart,
3413
+ slots: shell.slots,
3414
+ setSymbol: (sym, res) => pane.load(sym, res ?? pane.store.get().resolution),
2984
3415
  setResolution: (res) => {
2985
- const current = store.get().symbol;
2986
- return current ? load(current.symbol, res) : Promise.resolve();
3416
+ const current = pane.store.get().symbol;
3417
+ return current ? pane.load(current.symbol, res) : Promise.resolve();
2987
3418
  },
2988
- setLocale: (locale2) => {
2989
- chart.setLocale(locale2);
2990
- store.set({ locale: locale2 });
3419
+ setLocale: (locale) => {
3420
+ pane.chart.setLocale(locale);
3421
+ pane.store.set({ locale });
2991
3422
  },
2992
- save: () => chart.save(),
2993
- load: (state) => chart.load(state),
2994
- setTheme: (patch) => {
2995
- themePatch = { ...themePatch, ...patch };
2996
- refreshScheme();
3423
+ save: () => pane.chart.save(),
3424
+ load: (state) => pane.chart.load(state),
3425
+ setTheme: shell.setTheme,
3426
+ setColorScheme: shell.setColorScheme,
3427
+ destroy: () => {
3428
+ unbindKeys();
3429
+ Q(null, shell.toolbarHost);
3430
+ if (shell.drawbarHost) Q(null, shell.drawbarHost);
3431
+ pane.destroy();
3432
+ shell.destroy();
3433
+ }
3434
+ };
3435
+ }
3436
+
3437
+ // src/multi-chart.tsx
3438
+ import { parseChartState } from "@funnest/charts";
3439
+ var DEFAULT_SYNC = { crosshair: true, time: false, symbol: false, interval: false };
3440
+ function PaneTab({ pane, index, active, onSelect }) {
3441
+ const s3 = useStore(pane.store);
3442
+ const t3 = STRINGS[s3.locale];
3443
+ return /* @__PURE__ */ a2("button", { class: "fnc-pane-tab", role: "tab", "aria-selected": active, onClick: onSelect, children: [
3444
+ /* @__PURE__ */ a2("span", { class: "fnc-pane-tab-num", children: index + 1 }),
3445
+ s3.symbol?.name ?? `${t3.pane} ${index + 1}`,
3446
+ /* @__PURE__ */ a2("small", { children: resolutionLabel(s3.resolution, s3.locale) })
3447
+ ] });
3448
+ }
3449
+ function PaneTabs({ multi, panes }) {
3450
+ const m3 = useStore(multi.store);
3451
+ const visible = panes().slice(0, paneCount(LAYOUTS[m3.layout]));
3452
+ if (!m3.narrow || visible.length < 2) return null;
3453
+ 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) })) });
3454
+ }
3455
+ function createMultiChart(container, options) {
3456
+ const {
3457
+ datafeed,
3458
+ panes: paneSpecs,
3459
+ layout: initialLayout = "1",
3460
+ sync: initialSync,
3461
+ narrowWidth = 640,
3462
+ resolution = "1D",
3463
+ intervalGroups = DEFAULT_INTERVAL_GROUPS,
3464
+ favoriteIntervals,
3465
+ onFavoriteIntervalsChange,
3466
+ searchCategories = DEFAULT_SEARCH_CATEGORIES,
3467
+ storageKey = "funnest-charts",
3468
+ searchHotkey = true,
3469
+ symbolLogo = true,
3470
+ drawingToolbar = true,
3471
+ chartType = "candles",
3472
+ attribution = true,
3473
+ colorScheme = "system",
3474
+ theme,
3475
+ ...chartOptions
3476
+ } = options;
3477
+ if (paneSpecs.length === 0) throw new Error("funnest-charts: createMultiChart needs at least one pane");
3478
+ const displayTimezone = defaultDisplayTimezone(chartOptions.locale ?? "ko", chartOptions.displayTimezone);
3479
+ const shell = createShell(container, {
3480
+ colorScheme,
3481
+ theme,
3482
+ storageKey,
3483
+ drawingToolbar,
3484
+ favoriteIntervals: favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS,
3485
+ onFavoriteIntervalsChange
3486
+ });
3487
+ shell.root.classList.add("fnc-multi");
3488
+ const tabsHost = div("fnc-pane-tabs-host");
3489
+ shell.root.insertBefore(tabsHost, shell.body);
3490
+ const grid = div("fnc-grid");
3491
+ shell.main.insertBefore(grid, shell.slots.bottom);
3492
+ const parking = div("fnc-parking", shell.root);
3493
+ const listeners = /* @__PURE__ */ new Map();
3494
+ const emit = (event, payload) => {
3495
+ for (const l3 of listeners.get(event) ?? []) l3(payload);
3496
+ };
3497
+ let stateQueued = false;
3498
+ const queueState = () => {
3499
+ if (stateQueued) return;
3500
+ stateQueued = true;
3501
+ queueMicrotask(() => {
3502
+ stateQueued = false;
3503
+ emit("stateChange", void 0);
3504
+ });
3505
+ };
3506
+ const store = new Store({
3507
+ layout: isLayoutId(initialLayout) ? initialLayout : "1",
3508
+ sync: { ...DEFAULT_SYNC, ...initialSync },
3509
+ active: 0,
3510
+ maximized: null,
3511
+ narrow: false
3512
+ });
3513
+ let sizes = {};
3514
+ const panes = [];
3515
+ const cells = [];
3516
+ let driver = -1;
3517
+ const visibleCount = () => Math.min(paneCount(LAYOUTS[store.get().layout]), MAX_PANES);
3518
+ const visible = () => panes.slice(0, visibleCount());
3519
+ const controls = {
3520
+ store,
3521
+ setLayout: (layout) => setLayout(layout),
3522
+ setSync: (patch) => setSync(patch),
3523
+ activate: (index) => activate(index),
3524
+ toggleMaximize: (index) => toggleMaximize(index)
3525
+ };
3526
+ function makePane(index, spec) {
3527
+ const pane = createPane(shell, {
3528
+ datafeed,
3529
+ chartOptions,
3530
+ displayTimezone,
3531
+ resolution: spec.resolution ?? resolution,
3532
+ chartType,
3533
+ intervalGroups,
3534
+ searchCategories,
3535
+ symbolLogo,
3536
+ drawingToolbar,
3537
+ // One badge for the whole widget, not one per pane.
3538
+ attribution: attribution && index === 0,
3539
+ multi: controls
3540
+ });
3541
+ const cell = div("fnc-pane");
3542
+ cell.appendChild(pane.stage);
3543
+ const touch = () => {
3544
+ driver = panes.indexOf(pane);
3545
+ if (driver >= 0 && driver !== store.get().active) activate(driver);
3546
+ };
3547
+ cell.addEventListener("pointerdown", touch, true);
3548
+ cell.addEventListener("wheel", () => driver = panes.indexOf(pane), { capture: true, passive: true });
3549
+ const c3 = pane.chart;
3550
+ c3.on("crosshairMove", (info) => {
3551
+ if (!store.get().sync.crosshair || info?.source === "sync") return;
3552
+ for (const other of visible()) if (other !== pane) other.chart.setSyncedCrosshair(info ? info.time : null);
3553
+ });
3554
+ c3.on("visibleRangeChange", () => {
3555
+ if (!store.get().sync.time || panes[driver] !== pane) return;
3556
+ broadcastTime(pane);
3557
+ });
3558
+ c3.on("symbolChange", (info) => {
3559
+ queueState();
3560
+ if (panes[store.get().active] !== pane) return;
3561
+ const res = c3.currentResolution;
3562
+ const { sync } = store.get();
3563
+ if (!sync.symbol && !sync.interval) return;
3564
+ for (const other of visible()) {
3565
+ if (other === pane) continue;
3566
+ const sym = sync.symbol ? info.symbol : other.chart.symbolInfo?.symbol;
3567
+ const r3 = sync.interval && res ? res : other.chart.currentResolution ?? other.store.get().resolution;
3568
+ if (sym && (sym !== other.chart.symbolInfo?.symbol || r3 !== other.chart.currentResolution)) void other.load(sym, r3);
3569
+ }
3570
+ });
3571
+ c3.on("stateChange", queueState);
3572
+ panes[index] = pane;
3573
+ cells[index] = cell;
3574
+ void pane.load(spec.symbol, spec.resolution ?? resolution);
3575
+ return pane;
3576
+ }
3577
+ function broadcastTime(from) {
3578
+ const range = from.chart.getVisibleTimeRange();
3579
+ if (!range) return;
3580
+ const res = from.chart.currentResolution;
3581
+ for (const other of visible()) {
3582
+ if (other !== from && other.chart.currentResolution === res) other.chart.setVisibleTimeRange(range);
3583
+ }
3584
+ }
3585
+ function ensurePanes(count) {
3586
+ for (let i3 = panes.length; i3 < count; i3++) {
3587
+ const src = panes[store.get().active] ?? panes[0];
3588
+ const spec = paneSpecs[i3] ?? {
3589
+ symbol: src?.chart.symbolInfo?.symbol ?? paneSpecs[0].symbol,
3590
+ resolution: src?.chart.currentResolution ?? src?.store.get().resolution ?? resolution
3591
+ };
3592
+ makePane(i3, spec);
3593
+ }
3594
+ }
3595
+ function buildNode(node, path, layout) {
3596
+ if (typeof node === "number") return cells[node];
3597
+ const box = div(`fnc-split fnc-split-${node.dir}`);
3598
+ const key = `${layout}:${path}`;
3599
+ const weights = sizesFor(sizes, key, node.children.length);
3600
+ const children = node.children.map((child, i3) => {
3601
+ const el = buildNode(child, path ? `${path}.${i3}` : String(i3), layout);
3602
+ el.style.flex = `${weights[i3]} 1 0px`;
3603
+ return el;
3604
+ });
3605
+ children.forEach((el, i3) => {
3606
+ box.appendChild(el);
3607
+ if (i3 < children.length - 1) box.appendChild(makeSplitter(box, node.dir, key, i3, children));
3608
+ });
3609
+ return box;
3610
+ }
3611
+ function makeSplitter(box, dir, key, i3, children) {
3612
+ const bar = div(`fnc-splitter fnc-splitter-${dir}`);
3613
+ bar.setAttribute("role", "separator");
3614
+ bar.setAttribute("aria-orientation", dir === "row" ? "vertical" : "horizontal");
3615
+ let last = null;
3616
+ const apply = (next) => {
3617
+ sizes = { ...sizes, [key]: next };
3618
+ next.forEach((w3, j2) => children[j2].style.flex = `${w3} 1 0px`);
3619
+ queueState();
3620
+ };
3621
+ bar.addEventListener("pointerdown", (e3) => {
3622
+ bar.setPointerCapture(e3.pointerId);
3623
+ last = dir === "row" ? e3.clientX : e3.clientY;
3624
+ shell.root.classList.add(dir === "row" ? "fnc-resizing-x" : "fnc-resizing-y");
3625
+ });
3626
+ bar.addEventListener("pointermove", (e3) => {
3627
+ if (last === null) return;
3628
+ const pos = dir === "row" ? e3.clientX : e3.clientY;
3629
+ const length = dir === "row" ? box.clientWidth : box.clientHeight;
3630
+ if (length <= 0) return;
3631
+ apply(resizePair(sizesFor(sizes, key, children.length), i3, (pos - last) / length));
3632
+ last = pos;
3633
+ });
3634
+ const end = () => {
3635
+ last = null;
3636
+ shell.root.classList.remove("fnc-resizing-x", "fnc-resizing-y");
3637
+ };
3638
+ bar.addEventListener("pointerup", end);
3639
+ bar.addEventListener("pointercancel", end);
3640
+ bar.addEventListener("dblclick", () => apply(Array(children.length).fill(1)));
3641
+ return bar;
3642
+ }
3643
+ function renderGrid() {
3644
+ const s3 = store.get();
3645
+ const count = visibleCount();
3646
+ for (let i3 = 0; i3 < panes.length; i3++) {
3647
+ if (i3 >= count) parking.appendChild(cells[i3]);
3648
+ cells[i3].classList.toggle("fnc-pane-active", i3 === s3.active && count > 1);
3649
+ }
3650
+ const single = s3.maximized ?? (s3.narrow ? s3.active : null);
3651
+ const top = single !== null && single < count ? cells[single] : buildNode(LAYOUTS[s3.layout], "", s3.layout);
3652
+ top.style.flex = "1 1 0px";
3653
+ grid.replaceChildren(top);
3654
+ shell.root.classList.toggle("fnc-split-on", count > 1 && single === null);
3655
+ }
3656
+ function bindToolbar() {
3657
+ const pane = panes[store.get().active];
3658
+ Q(/* @__PURE__ */ a2(Toolbar, { ctx: pane.ctx }, store.get().active), shell.toolbarHost);
3659
+ if (shell.drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx: pane.ctx }, store.get().active), shell.drawbarHost);
3660
+ }
3661
+ function activate(index) {
3662
+ const s3 = store.get();
3663
+ if (index < 0 || index >= visibleCount() || index === s3.active) return;
3664
+ panes[s3.active]?.chart.setDrawingTool(null);
3665
+ store.set({ active: index, maximized: s3.maximized === null ? null : index });
3666
+ bindToolbar();
3667
+ renderGrid();
3668
+ emit("activeChange", { index, chart: panes[index].chart });
3669
+ queueState();
3670
+ }
3671
+ function setLayout(layout) {
3672
+ if (!isLayoutId(layout)) return;
3673
+ const count = paneCount(LAYOUTS[layout]);
3674
+ ensurePanes(count);
3675
+ const s3 = store.get();
3676
+ for (const p3 of panes.slice(count)) p3.chart.setSyncedCrosshair(null);
3677
+ store.set({ layout, maximized: null, active: Math.min(s3.active, count - 1) });
3678
+ if (s3.active !== store.get().active) bindToolbar();
3679
+ renderGrid();
3680
+ emit("layoutChange", { layout, panes: count });
3681
+ queueState();
3682
+ }
3683
+ function setSync(patch) {
3684
+ const before = store.get().sync;
3685
+ const sync = { ...before, ...patch };
3686
+ store.set({ sync });
3687
+ const active = panes[store.get().active];
3688
+ if (!sync.crosshair) for (const p3 of panes) p3.chart.setSyncedCrosshair(null);
3689
+ if (sync.time && !before.time) broadcastTime(active);
3690
+ if (sync.symbol && !before.symbol || sync.interval && !before.interval) {
3691
+ const info = active.chart.symbolInfo;
3692
+ const res = active.chart.currentResolution;
3693
+ for (const other of visible()) {
3694
+ if (other === active || !info) continue;
3695
+ const sym = sync.symbol ? info.symbol : other.chart.symbolInfo?.symbol ?? info.symbol;
3696
+ const r3 = sync.interval && res ? res : other.chart.currentResolution ?? other.store.get().resolution;
3697
+ if (sym !== other.chart.symbolInfo?.symbol || r3 !== other.chart.currentResolution) void other.load(sym, r3);
3698
+ }
3699
+ }
3700
+ queueState();
3701
+ }
3702
+ function toggleMaximize(index) {
3703
+ const s3 = store.get();
3704
+ const target = index ?? s3.active;
3705
+ if (s3.maximized !== null) store.set({ maximized: null });
3706
+ else if (visibleCount() > 1) {
3707
+ if (target !== s3.active) activate(target);
3708
+ store.set({ maximized: target });
3709
+ }
3710
+ renderGrid();
3711
+ }
3712
+ makePane(0, paneSpecs[0]);
3713
+ ensurePanes(Math.max(visibleCount(), Math.min(paneSpecs.length, MAX_PANES)));
3714
+ shell.setPanes(() => panes);
3715
+ bindToolbar();
3716
+ Q(/* @__PURE__ */ a2(PaneTabs, { multi: controls, panes: () => panes }), tabsHost);
3717
+ renderGrid();
3718
+ const resize = new ResizeObserver(() => {
3719
+ const narrow = shell.root.clientWidth > 0 && shell.root.clientWidth < narrowWidth;
3720
+ if (narrow === store.get().narrow) return;
3721
+ store.set({ narrow });
3722
+ renderGrid();
3723
+ });
3724
+ resize.observe(shell.root);
3725
+ const unbindKeys = bindWidgetKeys(
3726
+ shell,
3727
+ () => panes[store.get().active] ?? null,
3728
+ searchHotkey,
3729
+ (e3) => {
3730
+ if (!e3.altKey || e3.ctrlKey || e3.metaKey) return false;
3731
+ if (e3.key === "Enter") {
3732
+ toggleMaximize();
3733
+ return true;
3734
+ }
3735
+ const digit = /^Digit([1-8])$/.exec(e3.code)?.[1];
3736
+ if (!digit) return false;
3737
+ activate(Number(digit) - 1);
3738
+ return true;
3739
+ }
3740
+ );
3741
+ const save = () => {
3742
+ const s3 = store.get();
3743
+ return { version: 1, layout: s3.layout, sync: { ...s3.sync }, active: s3.active, sizes: { ...sizes }, panes: panes.map((p3) => p3.chart.save()) };
3744
+ };
3745
+ return {
3746
+ slots: shell.slots,
3747
+ get charts() {
3748
+ return visible().map((p3) => p3.chart);
2997
3749
  },
2998
- setColorScheme: (next) => {
2999
- scheme = next;
3000
- refreshScheme();
3750
+ get activeChart() {
3751
+ return panes[store.get().active].chart;
3752
+ },
3753
+ get activeIndex() {
3754
+ return store.get().active;
3755
+ },
3756
+ get layout() {
3757
+ return store.get().layout;
3758
+ },
3759
+ get sync() {
3760
+ return { ...store.get().sync };
3761
+ },
3762
+ setLayout,
3763
+ setSync,
3764
+ activate,
3765
+ toggleMaximize,
3766
+ setSymbol: (sym, res) => {
3767
+ const pane = panes[store.get().active];
3768
+ return pane.load(sym, res ?? pane.store.get().resolution);
3769
+ },
3770
+ setResolution: (res) => {
3771
+ const pane = panes[store.get().active];
3772
+ const current = pane.store.get().symbol;
3773
+ return current ? pane.load(current.symbol, res) : Promise.resolve();
3774
+ },
3775
+ setLocale: (locale) => {
3776
+ for (const p3 of panes) {
3777
+ p3.chart.setLocale(locale);
3778
+ p3.store.set({ locale });
3779
+ }
3780
+ },
3781
+ setTheme: shell.setTheme,
3782
+ setColorScheme: shell.setColorScheme,
3783
+ save,
3784
+ load: async (input) => {
3785
+ const raw = input;
3786
+ if (!raw || typeof raw !== "object" || raw.version !== 1 || !Array.isArray(raw.panes) || raw.panes.length === 0) {
3787
+ throw new Error("funnest-charts: unsupported multi-chart state (expected version 1)");
3788
+ }
3789
+ const states = raw.panes.slice(0, MAX_PANES).map((p3) => parseChartState(p3));
3790
+ sizes = raw.sizes && typeof raw.sizes === "object" ? { ...raw.sizes } : {};
3791
+ const layout = isLayoutId(raw.layout) ? raw.layout : "1";
3792
+ ensurePanes(Math.max(states.length, paneCount(LAYOUTS[layout])));
3793
+ store.set({ sync: { ...DEFAULT_SYNC, ...raw.sync } });
3794
+ setLayout(layout);
3795
+ activate(Math.min(Math.max(0, Number(raw.active) || 0), visibleCount() - 1));
3796
+ await Promise.all(states.map((state, i3) => panes[i3].chart.load(state)));
3797
+ },
3798
+ on: (event, listener) => {
3799
+ let set = listeners.get(event);
3800
+ if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
3801
+ set.add(listener);
3802
+ return () => set.delete(listener);
3001
3803
  },
3002
3804
  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();
3805
+ unbindKeys();
3806
+ resize.disconnect();
3807
+ Q(null, shell.toolbarHost);
3808
+ if (shell.drawbarHost) Q(null, shell.drawbarHost);
3809
+ Q(null, tabsHost);
3810
+ for (const p3 of panes) p3.destroy();
3811
+ listeners.clear();
3812
+ shell.destroy();
3014
3813
  }
3015
3814
  };
3016
3815
  }
@@ -3018,6 +3817,11 @@ export {
3018
3817
  DEFAULT_FAVORITE_INTERVALS,
3019
3818
  DEFAULT_INTERVAL_GROUPS,
3020
3819
  DEFAULT_SEARCH_CATEGORIES,
3820
+ LAYOUTS,
3821
+ LAYOUT_GROUPS,
3822
+ MAX_PANES,
3823
+ createMultiChart,
3021
3824
  createTradingChart,
3825
+ paneCount,
3022
3826
  resolutionLabel
3023
3827
  };