@funnest/charts-ui 0.3.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/components/layout-menu.d.ts +11 -0
- package/dist/components/slot.d.ts +2 -1
- package/dist/context.d.ts +31 -0
- package/dist/i18n.d.ts +22 -0
- package/dist/icons.d.ts +3 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1370 -548
- package/dist/layouts.d.ts +34 -0
- package/dist/multi-chart.d.ts +78 -0
- package/dist/pane.d.ts +33 -0
- package/dist/shell.d.ts +77 -0
- package/dist/trading-chart.d.ts +4 -16
- package/package.json +2 -2
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:
|
|
1343
|
-
"aria-label":
|
|
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:
|
|
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:
|
|
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((
|
|
1484
|
-
/* @__PURE__ */ a2("span", { class: "fnc-row-label fnc-row-label-wide", children:
|
|
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,19 +1383,216 @@ function IntervalPicker({ ctx }) {
|
|
|
1603
1383
|
] });
|
|
1604
1384
|
}
|
|
1605
1385
|
|
|
1606
|
-
// src/
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
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;
|
|
1614
1450
|
}
|
|
1615
1451
|
|
|
1616
|
-
// src/components/
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/components/symbol-search.tsx
|
|
1594
|
+
import { formatPrice } from "@funnest/charts";
|
|
1595
|
+
var DEBOUNCE_MS = 150;
|
|
1619
1596
|
var RECENT_KEY = "recent-symbols";
|
|
1620
1597
|
var FAVORITE_KEY = "favorite-symbols";
|
|
1621
1598
|
var MAX_RECENT = 8;
|
|
@@ -1712,7 +1689,7 @@ function SymbolSearch({ ctx }) {
|
|
|
1712
1689
|
else return;
|
|
1713
1690
|
e3.preventDefault();
|
|
1714
1691
|
};
|
|
1715
|
-
const
|
|
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:
|
|
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) =>
|
|
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) =>
|
|
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",
|
|
@@ -1912,7 +1890,7 @@ function Toolbar({ ctx }) {
|
|
|
1912
1890
|
children: /* @__PURE__ */ a2(PencilIcon, {})
|
|
1913
1891
|
}
|
|
1914
1892
|
),
|
|
1915
|
-
/* @__PURE__ */ a2(Slot, { el: ctx.slots.toolbarEnd }),
|
|
1893
|
+
/* @__PURE__ */ a2(Slot, { el: ctx.slots.toolbarEnd, fixed: true }),
|
|
1916
1894
|
/* @__PURE__ */ a2(
|
|
1917
1895
|
"button",
|
|
1918
1896
|
{
|
|
@@ -1940,126 +1918,382 @@ function Toolbar({ ctx }) {
|
|
|
1940
1918
|
] });
|
|
1941
1919
|
}
|
|
1942
1920
|
|
|
1943
|
-
// src/
|
|
1944
|
-
|
|
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
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
.fnc-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
.
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
+
);
|
|
2052
1985
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
.
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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;
|
|
2294
|
+
height: 36px;
|
|
2295
|
+
border-radius: var(--fnc-radius);
|
|
2296
|
+
color: var(--fnc-text-2);
|
|
2063
2297
|
flex: none;
|
|
2064
2298
|
}
|
|
2065
2299
|
.fnc-btn:hover { color: var(--fnc-text); }
|
|
@@ -2111,7 +2345,9 @@ var CSS = (
|
|
|
2111
2345
|
|
|
2112
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 */
|
|
2113
2347
|
.fnc-body { position: relative; display: flex; flex: 1; min-height: 0; }
|
|
2114
|
-
.fnc-
|
|
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; }
|
|
2115
2351
|
.fnc-chart-host, .fnc-overlay { position: absolute; inset: 0; }
|
|
2116
2352
|
|
|
2117
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 */
|
|
@@ -2278,6 +2514,8 @@ var CSS = (
|
|
|
2278
2514
|
/* \u2500\u2500 Slots: host-app UI inside the widget (collapse when empty) \u2500\u2500 */
|
|
2279
2515
|
.fnc-slot:empty { display: none; }
|
|
2280
2516
|
.fnc-slot-host { display: flex; align-self: stretch; min-width: 0; flex: 0 1 auto; }
|
|
2517
|
+
/* Buttons next to the gear keep their size (they'd otherwise slide under it on phones). */
|
|
2518
|
+
.fnc-slot-host-fixed { flex: none; }
|
|
2281
2519
|
.fnc-slot-toolbar { position: relative; display: flex; align-items: center; min-width: 0; padding-left: 9px; margin-left: 4px; }
|
|
2282
2520
|
.fnc-slot-toolbar::before {
|
|
2283
2521
|
content: '';
|
|
@@ -2686,13 +2924,124 @@ var CSS = (
|
|
|
2686
2924
|
.fnc-action-primary { background: var(--fnc-accent); color: #fff; }
|
|
2687
2925
|
.fnc-action-danger { flex: 0 0 auto; padding: 0 16px; color: var(--fnc-danger, #F04452); background: none; }
|
|
2688
2926
|
|
|
2927
|
+
/* \u2500\u2500 Narrow phones (\u2264 400px): drop the menu chevrons and separators so every button fits \u2500\u2500 */
|
|
2928
|
+
@media (max-width: 400px) {
|
|
2929
|
+
.fnc-toolbar .fnc-btn-wide > svg:last-child { display: none; }
|
|
2930
|
+
.fnc-toolbar .fnc-btn-wide { padding: 0 3px; }
|
|
2931
|
+
.fnc-toolbar > .fnc-sep { display: none; }
|
|
2932
|
+
.fnc-toolbar > .fnc-spacer { min-width: 0; }
|
|
2933
|
+
}
|
|
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
|
+
|
|
2689
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 */
|
|
2690
3033
|
@media (max-width: 640px) {
|
|
2691
|
-
.fnc-toolbar { padding: 0 4px; }
|
|
3034
|
+
.fnc-toolbar { padding: 0 4px; gap: 2px; }
|
|
3035
|
+
.fnc-toolbar .fnc-btn { width: 32px; height: 32px; }
|
|
3036
|
+
.fnc-toolbar .fnc-btn-wide, .fnc-toolbar .fnc-btn-label { width: auto; padding: 0 6px; }
|
|
3037
|
+
.fnc-toolbar .fnc-sep { margin: 0 2px; }
|
|
2692
3038
|
.fnc-symbol { max-width: 140px; padding: 0 8px 0 6px; }
|
|
3039
|
+
/* Logo only on phones; the name is in the chart legend right below. */
|
|
3040
|
+
.fnc-toolbar .fnc-symbol:has(.fnc-avatar) :is(.fnc-symbol-name, .fnc-icon) { display: none; }
|
|
2693
3041
|
.fnc-chip { padding: 0 8px; }
|
|
2694
3042
|
.fnc-btn-screenshot { display: none; }
|
|
2695
3043
|
.fnc-btn-label span { display: none; }
|
|
3044
|
+
.fnc-btn-sync { display: none; }
|
|
2696
3045
|
.fnc-intervals { display: none; }
|
|
2697
3046
|
.fnc-interval-current { display: inline; }
|
|
2698
3047
|
.fnc-interval-menu { left: auto; right: -40px; }
|
|
@@ -2715,7 +3064,7 @@ var CSS = (
|
|
|
2715
3064
|
.fnc-dialog.fnc-search-dialog { width: 100%; height: 100%; }
|
|
2716
3065
|
.fnc-sr-footer, .fnc-sr-side, .fnc-hide-mobile, .fnc-sr-spark { display: none; }
|
|
2717
3066
|
/* The interval button must stay whole; the symbol name truncates instead. */
|
|
2718
|
-
.fnc-symbol { flex: 0 1 auto; min-width:
|
|
3067
|
+
.fnc-symbol { flex: 0 1 auto; min-width: 36px; }
|
|
2719
3068
|
/* MA rows wrap to two lines: [label \u2713 color width \xD7] / [source period] */
|
|
2720
3069
|
.fnc-rows .fnc-row { flex-wrap: wrap; row-gap: 8px; padding-bottom: 10px; border-bottom: 1px solid var(--fnc-border); }
|
|
2721
3070
|
/* Basis = row \u2212 indent(52) \u2212 gap(8) \u2212 period(72): too wide for line 1, so it always wraps. */
|
|
@@ -2734,21 +3083,7 @@ function injectStyles(doc = document) {
|
|
|
2734
3083
|
doc.head.appendChild(style);
|
|
2735
3084
|
}
|
|
2736
3085
|
|
|
2737
|
-
// src/
|
|
2738
|
-
var DEFAULT_INTERVAL_GROUPS = [
|
|
2739
|
-
{ id: "minutes", intervals: ["1", "5", "15", "30"] },
|
|
2740
|
-
{ id: "hours", intervals: ["60", "240"] },
|
|
2741
|
-
{ id: "days", intervals: ["1D", "1W", "1M", "3M", "6M", "12M"] }
|
|
2742
|
-
];
|
|
2743
|
-
var DEFAULT_FAVORITE_INTERVALS = ["1", "5", "15", "60", "240", "1D"];
|
|
2744
|
-
var stockLike = (i3) => i3.type === "stock" || i3.type === "etf" || i3.type === "index";
|
|
2745
|
-
var DEFAULT_SEARCH_CATEGORIES = [
|
|
2746
|
-
{ id: "all", label: { ko: "\uC804\uCCB4", en: "All" }, match: () => true },
|
|
2747
|
-
{ id: "kr-stock", label: { ko: "\uAD6D\uB0B4\uC8FC\uC2DD", en: "KR stocks" }, match: (i3) => stockLike(i3) && i3.country === "KR" },
|
|
2748
|
-
{ id: "global-stock", label: { ko: "\uD574\uC678\uC8FC\uC2DD", en: "Global stocks" }, match: (i3) => stockLike(i3) && i3.country !== "KR" },
|
|
2749
|
-
{ id: "crypto", label: { ko: "\uCF54\uC778", en: "Crypto" }, match: (i3) => i3.type === "crypto" },
|
|
2750
|
-
{ id: "futures", label: { ko: "\uC120\uBB3C", en: "Futures" }, match: (i3) => i3.type === "futures" }
|
|
2751
|
-
];
|
|
3086
|
+
// src/shell.ts
|
|
2752
3087
|
var THEME_VARS = {
|
|
2753
3088
|
background: "--fnc-bg",
|
|
2754
3089
|
surface: "--fnc-surface",
|
|
@@ -2767,37 +3102,19 @@ function applyThemeVars(el, theme) {
|
|
|
2767
3102
|
el.style.setProperty("--fnc-radius", `${theme.radius}px`);
|
|
2768
3103
|
}
|
|
2769
3104
|
function div(className, parent) {
|
|
2770
|
-
const el = document.createElement("div");
|
|
2771
|
-
el.className = className;
|
|
2772
|
-
parent?.appendChild(el);
|
|
2773
|
-
return el;
|
|
2774
|
-
}
|
|
2775
|
-
function
|
|
2776
|
-
const {
|
|
2777
|
-
datafeed,
|
|
2778
|
-
symbol,
|
|
2779
|
-
resolution = "1D",
|
|
2780
|
-
intervalGroups = DEFAULT_INTERVAL_GROUPS,
|
|
2781
|
-
favoriteIntervals,
|
|
2782
|
-
onFavoriteIntervalsChange,
|
|
2783
|
-
searchCategories = DEFAULT_SEARCH_CATEGORIES,
|
|
2784
|
-
storageKey = "funnest-charts",
|
|
2785
|
-
searchHotkey = true,
|
|
2786
|
-
symbolLogo = true,
|
|
2787
|
-
drawingToolbar = true,
|
|
2788
|
-
chartType = "candles",
|
|
2789
|
-
attribution = true,
|
|
2790
|
-
colorScheme = "system",
|
|
2791
|
-
...chartOptions
|
|
2792
|
-
} = options;
|
|
2793
|
-
const locale = chartOptions.locale ?? "ko";
|
|
2794
|
-
const displayTimezone = chartOptions.displayTimezone ?? (locale === "ko" ? "Asia/Seoul" : Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
2795
|
-
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) {
|
|
2796
3111
|
injectStyles(container.ownerDocument);
|
|
3112
|
+
const storage = createStorage(options.storageKey);
|
|
2797
3113
|
const win = container.ownerDocument.defaultView;
|
|
2798
3114
|
const darkQuery = win?.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
|
|
2799
|
-
let scheme = colorScheme;
|
|
2800
|
-
let themePatch = { ...
|
|
3115
|
+
let scheme = options.colorScheme;
|
|
3116
|
+
let themePatch = { ...options.theme };
|
|
3117
|
+
let panes = () => [];
|
|
2801
3118
|
const isDark = () => scheme === "system" ? !!darkQuery?.matches : scheme === "dark";
|
|
2802
3119
|
const resolveTheme = () => ({ ...isDark() ? darkTheme : lightTheme, ...themePatch });
|
|
2803
3120
|
let theme = resolveTheme();
|
|
@@ -2813,33 +3130,138 @@ function createTradingChart(container, options) {
|
|
|
2813
3130
|
toolbar: div("fnc-slot fnc-slot-toolbar"),
|
|
2814
3131
|
toolbarEnd: div("fnc-slot fnc-slot-toolbar-end"),
|
|
2815
3132
|
belowToolbar: div("fnc-slot fnc-slot-below-toolbar", root),
|
|
3133
|
+
bottom: div("fnc-slot fnc-slot-bottom"),
|
|
2816
3134
|
footer: div("fnc-slot fnc-slot-footer")
|
|
2817
3135
|
};
|
|
2818
3136
|
const body = div("fnc-body", root);
|
|
2819
|
-
const drawbarHost = drawingToolbar ? div("fnc-drawbar-host", body) : null;
|
|
2820
|
-
const
|
|
2821
|
-
|
|
2822
|
-
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);
|
|
2823
3140
|
root.appendChild(slots.footer);
|
|
2824
|
-
const chart = createChart(chartHost, { ...chartOptions, theme, datafeed, displayTimezone });
|
|
2825
3141
|
const refreshScheme = () => {
|
|
2826
3142
|
theme = resolveTheme();
|
|
2827
3143
|
applyScheme();
|
|
2828
|
-
chart.setTheme(theme);
|
|
3144
|
+
for (const p3 of panes()) p3.chart.setTheme(theme);
|
|
2829
3145
|
};
|
|
2830
3146
|
const onSchemeChange = () => scheme === "system" && refreshScheme();
|
|
2831
3147
|
darkQuery?.addEventListener?.("change", onSchemeChange);
|
|
2832
|
-
|
|
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);
|
|
2833
3256
|
const store = new Store({
|
|
2834
|
-
locale,
|
|
3257
|
+
locale: config.chartOptions.locale ?? "ko",
|
|
2835
3258
|
symbol: null,
|
|
2836
|
-
resolution,
|
|
3259
|
+
resolution: config.resolution,
|
|
2837
3260
|
loading: false,
|
|
2838
3261
|
layout: chart.layout,
|
|
2839
3262
|
atRealtime: true,
|
|
2840
|
-
fullscreen: false,
|
|
2841
3263
|
searchOpen: false,
|
|
2842
|
-
chartType,
|
|
3264
|
+
chartType: config.chartType,
|
|
2843
3265
|
drawingTool: null,
|
|
2844
3266
|
selection: null,
|
|
2845
3267
|
drawingCount: 0,
|
|
@@ -2847,12 +3269,11 @@ function createTradingChart(container, options) {
|
|
|
2847
3269
|
magnet: chart.magnetEnabled,
|
|
2848
3270
|
indicators: chart.getIndicators(),
|
|
2849
3271
|
dialog: null,
|
|
2850
|
-
|
|
2851
|
-
drawbarCollapsed: storage.get("drawbar-collapsed", false)
|
|
3272
|
+
...shell.sharedState()
|
|
2852
3273
|
});
|
|
2853
3274
|
const syncRealtime = () => store.set({ atRealtime: chart.isAtRealtime() });
|
|
2854
3275
|
const offs = [
|
|
2855
|
-
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 })),
|
|
2856
3277
|
chart.on("loadingChange", (loading) => store.set({ loading })),
|
|
2857
3278
|
chart.on("layoutChange", (layout) => store.set({ layout })),
|
|
2858
3279
|
chart.on("visibleRangeChange", syncRealtime),
|
|
@@ -2862,7 +3283,7 @@ function createTradingChart(container, options) {
|
|
|
2862
3283
|
chart.on("drawingsChange", (drawings) => store.set({ drawingCount: drawings.length })),
|
|
2863
3284
|
chart.on("indicatorsChange", (indicators) => store.set({ indicators })),
|
|
2864
3285
|
chart.on("magnetChange", (magnet) => store.set({ magnet })),
|
|
2865
|
-
chart.on("chartTypeChange", (
|
|
3286
|
+
chart.on("chartTypeChange", (chartType) => store.set({ chartType }))
|
|
2866
3287
|
];
|
|
2867
3288
|
const load = (sym, res) => {
|
|
2868
3289
|
store.set({ resolution: res });
|
|
@@ -2871,61 +3292,27 @@ function createTradingChart(container, options) {
|
|
|
2871
3292
|
store.set({ loading: false });
|
|
2872
3293
|
});
|
|
2873
3294
|
};
|
|
2874
|
-
const isNativeFullscreen = () => document.fullscreenElement === root;
|
|
2875
|
-
const onFullscreenChange = () => store.set({ fullscreen: isNativeFullscreen() || root.classList.contains("fnc-pseudo-fullscreen") });
|
|
2876
|
-
const onKeyDown = (e3) => {
|
|
2877
|
-
const s3 = store.get();
|
|
2878
|
-
if (searchHotkey && (e3.ctrlKey || e3.metaKey) && e3.key.toLowerCase() === "k" && datafeed.searchSymbols && !s3.dialog) {
|
|
2879
|
-
e3.preventDefault();
|
|
2880
|
-
store.set({ searchOpen: true });
|
|
2881
|
-
return;
|
|
2882
|
-
}
|
|
2883
|
-
if (e3.key === "Escape" && !e3.defaultPrevented && (s3.searchOpen || s3.dialog)) {
|
|
2884
|
-
e3.preventDefault();
|
|
2885
|
-
store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
|
|
2886
|
-
return;
|
|
2887
|
-
}
|
|
2888
|
-
if (e3.key === "Escape" && root.classList.contains("fnc-pseudo-fullscreen") && !s3.searchOpen && !s3.dialog) {
|
|
2889
|
-
root.classList.remove("fnc-pseudo-fullscreen");
|
|
2890
|
-
onFullscreenChange();
|
|
2891
|
-
}
|
|
2892
|
-
};
|
|
2893
|
-
const onKeyUp = (e3) => {
|
|
2894
|
-
const s3 = store.get();
|
|
2895
|
-
if (e3.key === "Escape" && (s3.searchOpen || s3.dialog)) store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
|
|
2896
|
-
};
|
|
2897
|
-
document.addEventListener("fullscreenchange", onFullscreenChange);
|
|
2898
|
-
document.addEventListener("keydown", onKeyDown);
|
|
2899
|
-
document.addEventListener("keyup", onKeyUp);
|
|
2900
3295
|
const ctx = {
|
|
2901
|
-
root,
|
|
3296
|
+
root: shell.root,
|
|
2902
3297
|
chart,
|
|
2903
|
-
datafeed,
|
|
3298
|
+
datafeed: config.datafeed,
|
|
2904
3299
|
store,
|
|
2905
|
-
storage,
|
|
2906
|
-
intervalGroups,
|
|
2907
|
-
searchCategories,
|
|
2908
|
-
displayTimezone,
|
|
2909
|
-
showLogo: symbolLogo,
|
|
2910
|
-
drawingToolbar,
|
|
2911
|
-
attribution,
|
|
2912
|
-
slots,
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
storage.set("drawbar-collapsed", collapsed);
|
|
2916
|
-
},
|
|
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,
|
|
2917
3310
|
selectSymbol: (sym) => void load(sym, store.get().resolution),
|
|
2918
3311
|
selectResolution: (res) => {
|
|
2919
3312
|
const current = store.get().symbol;
|
|
2920
3313
|
if (current) void load(current.symbol, res);
|
|
2921
3314
|
},
|
|
2922
|
-
toggleFavoriteInterval:
|
|
2923
|
-
const current = store.get().favoriteIntervals;
|
|
2924
|
-
const next = current.includes(res) ? current.filter((r3) => r3 !== res) : [...current, res];
|
|
2925
|
-
store.set({ favoriteIntervals: next });
|
|
2926
|
-
storage.set("favorite-intervals", next);
|
|
2927
|
-
onFavoriteIntervalsChange?.(next);
|
|
2928
|
-
},
|
|
3315
|
+
toggleFavoriteInterval: shell.toggleFavoriteInterval,
|
|
2929
3316
|
setSearchOpen: (open) => store.set({ searchOpen: open }),
|
|
2930
3317
|
screenshot: () => {
|
|
2931
3318
|
const s3 = store.get();
|
|
@@ -2938,61 +3325,491 @@ function createTradingChart(container, options) {
|
|
|
2938
3325
|
setTimeout(() => URL.revokeObjectURL(a4.href), 1e3);
|
|
2939
3326
|
});
|
|
2940
3327
|
},
|
|
2941
|
-
toggleFullscreen:
|
|
2942
|
-
if (isNativeFullscreen()) {
|
|
2943
|
-
void document.exitFullscreen();
|
|
2944
|
-
} else if (root.classList.contains("fnc-pseudo-fullscreen")) {
|
|
2945
|
-
root.classList.remove("fnc-pseudo-fullscreen");
|
|
2946
|
-
onFullscreenChange();
|
|
2947
|
-
} else if (root.requestFullscreen && document.fullscreenEnabled) {
|
|
2948
|
-
root.requestFullscreen().catch(() => {
|
|
2949
|
-
root.classList.add("fnc-pseudo-fullscreen");
|
|
2950
|
-
onFullscreenChange();
|
|
2951
|
-
});
|
|
2952
|
-
} else {
|
|
2953
|
-
root.classList.add("fnc-pseudo-fullscreen");
|
|
2954
|
-
onFullscreenChange();
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
3328
|
+
toggleFullscreen: shell.toggleFullscreen
|
|
2957
3329
|
};
|
|
2958
|
-
Q(/* @__PURE__ */ a2(Toolbar, { ctx }), toolbarHost);
|
|
2959
3330
|
Q(/* @__PURE__ */ a2(Overlay, { ctx }), overlayHost);
|
|
2960
|
-
if (drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx }), drawbarHost);
|
|
2961
|
-
void load(symbol, resolution);
|
|
2962
3331
|
return {
|
|
3332
|
+
stage,
|
|
2963
3333
|
chart,
|
|
2964
|
-
|
|
2965
|
-
|
|
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),
|
|
2966
3415
|
setResolution: (res) => {
|
|
2967
|
-
const current = store.get().symbol;
|
|
2968
|
-
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();
|
|
2969
3418
|
},
|
|
2970
|
-
setLocale: (
|
|
2971
|
-
chart.setLocale(
|
|
2972
|
-
store.set({ locale
|
|
3419
|
+
setLocale: (locale) => {
|
|
3420
|
+
pane.chart.setLocale(locale);
|
|
3421
|
+
pane.store.set({ locale });
|
|
2973
3422
|
},
|
|
2974
|
-
save: () => chart.save(),
|
|
2975
|
-
load: (state) => chart.load(state),
|
|
2976
|
-
setTheme:
|
|
2977
|
-
|
|
2978
|
-
|
|
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);
|
|
2979
3749
|
},
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
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);
|
|
2983
3803
|
},
|
|
2984
3804
|
destroy: () => {
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
if (drawbarHost) Q(null, drawbarHost);
|
|
2994
|
-
chart.destroy();
|
|
2995
|
-
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();
|
|
2996
3813
|
}
|
|
2997
3814
|
};
|
|
2998
3815
|
}
|
|
@@ -3000,6 +3817,11 @@ export {
|
|
|
3000
3817
|
DEFAULT_FAVORITE_INTERVALS,
|
|
3001
3818
|
DEFAULT_INTERVAL_GROUPS,
|
|
3002
3819
|
DEFAULT_SEARCH_CATEGORIES,
|
|
3820
|
+
LAYOUTS,
|
|
3821
|
+
LAYOUT_GROUPS,
|
|
3822
|
+
MAX_PANES,
|
|
3823
|
+
createMultiChart,
|
|
3003
3824
|
createTradingChart,
|
|
3825
|
+
paneCount,
|
|
3004
3826
|
resolutionLabel
|
|
3005
3827
|
};
|