@funnest/charts-ui 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/layout-menu.d.ts +11 -0
- 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 +1347 -543
- 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,18 +1383,215 @@ 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;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/components/layout-menu.tsx
|
|
1453
|
+
function LayoutGlyph({ id }) {
|
|
1454
|
+
return /* @__PURE__ */ a2("svg", { width: "20", height: "16", viewBox: "0 0 20 16", "aria-hidden": "true", children: layoutRects(LAYOUTS[id]).map((r3) => /* @__PURE__ */ a2("rect", { x: r3.x + 0.5, y: r3.y + 0.5, width: Math.max(0, r3.w - 1), height: Math.max(0, r3.h - 1), rx: "1.5", fill: "none", stroke: "currentColor", "stroke-width": "1.2" })) });
|
|
1455
|
+
}
|
|
1456
|
+
function usePopover() {
|
|
1457
|
+
const [open, setOpen] = d2(false);
|
|
1458
|
+
const ref = T2(null);
|
|
1459
|
+
A2(() => {
|
|
1460
|
+
if (!open) return;
|
|
1461
|
+
const onDown = (e3) => !ref.current?.contains(e3.target) && setOpen(false);
|
|
1462
|
+
const onKey = (e3) => e3.key === "Escape" && setOpen(false);
|
|
1463
|
+
document.addEventListener("pointerdown", onDown);
|
|
1464
|
+
document.addEventListener("keydown", onKey);
|
|
1465
|
+
return () => {
|
|
1466
|
+
document.removeEventListener("pointerdown", onDown);
|
|
1467
|
+
document.removeEventListener("keydown", onKey);
|
|
1468
|
+
};
|
|
1469
|
+
}, [open]);
|
|
1470
|
+
return { open, setOpen, ref };
|
|
1471
|
+
}
|
|
1472
|
+
var SYNC_KEYS = [
|
|
1473
|
+
{ key: "crosshair", label: "syncCrosshair" },
|
|
1474
|
+
{ key: "time", label: "syncTime" },
|
|
1475
|
+
{ key: "symbol", label: "syncSymbol" },
|
|
1476
|
+
{ key: "interval", label: "syncInterval" }
|
|
1477
|
+
];
|
|
1478
|
+
function LayoutMenu({ ctx, multi }) {
|
|
1479
|
+
const s3 = useStore(ctx.store);
|
|
1480
|
+
const m3 = useStore(multi.store);
|
|
1481
|
+
const t3 = STRINGS[s3.locale];
|
|
1482
|
+
const layout = usePopover();
|
|
1483
|
+
const sync = usePopover();
|
|
1484
|
+
const anySync = Object.values(m3.sync).some(Boolean);
|
|
1485
|
+
return /* @__PURE__ */ a2($, { children: [
|
|
1486
|
+
/* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: layout.ref, children: [
|
|
1487
|
+
/* @__PURE__ */ a2(
|
|
1488
|
+
"button",
|
|
1489
|
+
{
|
|
1490
|
+
class: "fnc-btn fnc-btn-wide fnc-btn-layout",
|
|
1491
|
+
title: t3.layout,
|
|
1492
|
+
"aria-label": t3.layout,
|
|
1493
|
+
"aria-haspopup": "menu",
|
|
1494
|
+
"aria-expanded": layout.open,
|
|
1495
|
+
onClick: () => layout.setOpen(!layout.open),
|
|
1496
|
+
children: [
|
|
1497
|
+
/* @__PURE__ */ a2(LayoutGlyph, { id: m3.layout }),
|
|
1498
|
+
/* @__PURE__ */ a2(ChevronDownIcon, {})
|
|
1499
|
+
]
|
|
1500
|
+
}
|
|
1501
|
+
),
|
|
1502
|
+
layout.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-layout-menu", role: "menu", "aria-label": t3.layout, children: LAYOUT_GROUPS.map((group) => /* @__PURE__ */ a2("div", { class: "fnc-layout-row", children: [
|
|
1503
|
+
/* @__PURE__ */ a2("span", { class: "fnc-layout-count", children: group[0].split("-")[0] }),
|
|
1504
|
+
group.map((id) => /* @__PURE__ */ a2(
|
|
1505
|
+
"button",
|
|
1506
|
+
{
|
|
1507
|
+
class: "fnc-layout-opt",
|
|
1508
|
+
role: "menuitemradio",
|
|
1509
|
+
"aria-checked": id === m3.layout,
|
|
1510
|
+
"aria-label": `${t3.layout} ${id}`,
|
|
1511
|
+
title: id,
|
|
1512
|
+
onClick: () => {
|
|
1513
|
+
multi.setLayout(id);
|
|
1514
|
+
layout.setOpen(false);
|
|
1515
|
+
},
|
|
1516
|
+
children: /* @__PURE__ */ a2(LayoutGlyph, { id })
|
|
1517
|
+
}
|
|
1518
|
+
))
|
|
1519
|
+
] })) })
|
|
1520
|
+
] }),
|
|
1521
|
+
/* @__PURE__ */ a2("div", { class: "fnc-menu-wrap", ref: sync.ref, children: [
|
|
1522
|
+
/* @__PURE__ */ a2(
|
|
1523
|
+
"button",
|
|
1524
|
+
{
|
|
1525
|
+
class: "fnc-btn fnc-btn-sync",
|
|
1526
|
+
title: t3.sync,
|
|
1527
|
+
"aria-label": t3.sync,
|
|
1528
|
+
"aria-haspopup": "menu",
|
|
1529
|
+
"aria-expanded": sync.open,
|
|
1530
|
+
"aria-pressed": anySync,
|
|
1531
|
+
onClick: () => sync.setOpen(!sync.open),
|
|
1532
|
+
children: /* @__PURE__ */ a2(LinkIcon, {})
|
|
1533
|
+
}
|
|
1534
|
+
),
|
|
1535
|
+
sync.open && /* @__PURE__ */ a2("div", { class: "fnc-menu fnc-sync-menu", role: "menu", "aria-label": t3.sync, children: [
|
|
1536
|
+
/* @__PURE__ */ a2("div", { class: "fnc-menu-title", children: [
|
|
1537
|
+
t3.sync,
|
|
1538
|
+
/* @__PURE__ */ a2("small", { children: t3.syncHint })
|
|
1539
|
+
] }),
|
|
1540
|
+
SYNC_KEYS.map(({ key, label }) => /* @__PURE__ */ a2(
|
|
1541
|
+
"button",
|
|
1542
|
+
{
|
|
1543
|
+
class: "fnc-menu-item fnc-sync-item",
|
|
1544
|
+
role: "menuitemcheckbox",
|
|
1545
|
+
"aria-checked": m3.sync[key],
|
|
1546
|
+
onClick: () => multi.setSync({ [key]: !m3.sync[key] }),
|
|
1547
|
+
children: [
|
|
1548
|
+
/* @__PURE__ */ a2("span", { children: t3[label] }),
|
|
1549
|
+
/* @__PURE__ */ a2("span", { class: "fnc-switch", "data-on": m3.sync[key] ? "true" : "false", "aria-hidden": "true" })
|
|
1550
|
+
]
|
|
1551
|
+
}
|
|
1552
|
+
))
|
|
1553
|
+
] })
|
|
1554
|
+
] })
|
|
1555
|
+
] });
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/components/slot.tsx
|
|
1559
|
+
function Slot({ el, fixed = false }) {
|
|
1560
|
+
const ref = T2(null);
|
|
1561
|
+
F2(() => {
|
|
1562
|
+
ref.current?.appendChild(el);
|
|
1563
|
+
return () => el.remove();
|
|
1564
|
+
}, [el]);
|
|
1565
|
+
return /* @__PURE__ */ a2("div", { class: fixed ? "fnc-slot-host fnc-slot-host-fixed" : "fnc-slot-host", ref });
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/components/symbol-avatar.tsx
|
|
1569
|
+
function hue(text) {
|
|
1570
|
+
let h3 = 0;
|
|
1571
|
+
for (const ch of text) h3 = (h3 * 31 + ch.charCodeAt(0)) % 360;
|
|
1572
|
+
return h3;
|
|
1573
|
+
}
|
|
1574
|
+
function SymbolAvatar({ info, size = 24 }) {
|
|
1575
|
+
const [failed, setFailed] = d2(false);
|
|
1576
|
+
A2(() => setFailed(false), [info.logoUrl]);
|
|
1577
|
+
const style = { width: `${size}px`, height: `${size}px`, fontSize: `${Math.round(size * 0.46)}px` };
|
|
1578
|
+
if (info.logoUrl && !failed) {
|
|
1579
|
+
return /* @__PURE__ */ a2("img", { class: "fnc-avatar", src: info.logoUrl, alt: "", style, onError: () => setFailed(true) });
|
|
1580
|
+
}
|
|
1581
|
+
const h3 = hue(info.symbol);
|
|
1582
|
+
return /* @__PURE__ */ a2(
|
|
1583
|
+
"span",
|
|
1584
|
+
{
|
|
1585
|
+
class: "fnc-avatar fnc-avatar-initial",
|
|
1586
|
+
"aria-hidden": "true",
|
|
1587
|
+
style: { ...style, background: `hsl(${h3} 70% 50% / 0.16)`, color: `hsl(${h3} 60% 42%)` },
|
|
1588
|
+
children: (info.name || info.symbol).trim().charAt(0).toUpperCase()
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1614
1591
|
}
|
|
1615
1592
|
|
|
1616
1593
|
// src/components/symbol-search.tsx
|
|
1617
|
-
import { formatPrice
|
|
1594
|
+
import { formatPrice } from "@funnest/charts";
|
|
1618
1595
|
var DEBOUNCE_MS = 150;
|
|
1619
1596
|
var RECENT_KEY = "recent-symbols";
|
|
1620
1597
|
var FAVORITE_KEY = "favorite-symbols";
|
|
@@ -1712,7 +1689,7 @@ function SymbolSearch({ ctx }) {
|
|
|
1712
1689
|
else return;
|
|
1713
1690
|
e3.preventDefault();
|
|
1714
1691
|
};
|
|
1715
|
-
const
|
|
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",
|
|
@@ -1940,126 +1918,379 @@ 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
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
1924
|
+
// src/components/overlay.tsx
|
|
1925
|
+
import { formatCompact, formatPrice as formatPrice2 } from "@funnest/charts";
|
|
1926
|
+
var ZOOM_STEP = 1.25;
|
|
1927
|
+
var CONTROL_OFFSET = 52;
|
|
1928
|
+
var dirClass = (v3) => v3 > 0 ? "fnc-up" : v3 < 0 ? "fnc-down" : "fnc-flat";
|
|
1929
|
+
function IndicatorItem({ ctx, item }) {
|
|
1930
|
+
const t3 = STRINGS[ctx.store.get().locale];
|
|
1931
|
+
return /* @__PURE__ */ a2(
|
|
1932
|
+
"div",
|
|
1933
|
+
{
|
|
1934
|
+
class: item.visible ? "fnc-ind" : "fnc-ind fnc-ind-hidden",
|
|
1935
|
+
role: "button",
|
|
1936
|
+
tabIndex: 0,
|
|
1937
|
+
"aria-label": `${item.title} ${t3.indicatorSettings}`,
|
|
1938
|
+
onClick: (e3) => {
|
|
1939
|
+
if (e3.target.closest(".fnc-ind-actions")) return;
|
|
1940
|
+
ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } });
|
|
1941
|
+
},
|
|
1942
|
+
onKeyDown: (e3) => e3.key === "Enter" && ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
|
|
1943
|
+
children: [
|
|
1944
|
+
/* @__PURE__ */ a2(
|
|
1945
|
+
"button",
|
|
1946
|
+
{
|
|
1947
|
+
class: "fnc-ind-btn fnc-ind-eye",
|
|
1948
|
+
title: item.visible ? t3.hide : t3.show,
|
|
1949
|
+
"aria-label": `${item.title} ${item.visible ? t3.hide : t3.show}`,
|
|
1950
|
+
"aria-pressed": !item.visible,
|
|
1951
|
+
onClick: (e3) => {
|
|
1952
|
+
e3.stopPropagation();
|
|
1953
|
+
ctx.chart.setIndicatorVisible(item.id, !item.visible);
|
|
1954
|
+
},
|
|
1955
|
+
children: item.visible ? /* @__PURE__ */ a2(EyeIcon, {}) : /* @__PURE__ */ a2(EyeOffIcon, {})
|
|
1956
|
+
}
|
|
1957
|
+
),
|
|
1958
|
+
/* @__PURE__ */ a2("span", { class: "fnc-ind-title", children: item.title }),
|
|
1959
|
+
item.visible && item.values.map((v3) => /* @__PURE__ */ a2("b", { style: { color: v3.color }, title: v3.label, children: v3.value })),
|
|
1960
|
+
/* @__PURE__ */ a2("span", { class: "fnc-ind-actions", children: [
|
|
1961
|
+
/* @__PURE__ */ a2(
|
|
1962
|
+
"button",
|
|
1963
|
+
{
|
|
1964
|
+
class: "fnc-ind-btn",
|
|
1965
|
+
title: t3.indicatorSettings,
|
|
1966
|
+
"aria-label": `${item.title} ${t3.indicatorSettings}`,
|
|
1967
|
+
onClick: () => ctx.store.set({ dialog: { kind: "indicatorSettings", id: item.id } }),
|
|
1968
|
+
children: /* @__PURE__ */ a2(GearIcon, {})
|
|
1969
|
+
}
|
|
1970
|
+
),
|
|
1971
|
+
/* @__PURE__ */ a2(
|
|
1972
|
+
"button",
|
|
1973
|
+
{
|
|
1974
|
+
class: "fnc-ind-btn",
|
|
1975
|
+
title: t3.removeIndicator,
|
|
1976
|
+
"aria-label": `${item.title} ${t3.removeIndicator}`,
|
|
1977
|
+
onClick: () => ctx.chart.removeIndicator(item.id),
|
|
1978
|
+
children: /* @__PURE__ */ a2(SmallCloseIcon, {})
|
|
1979
|
+
}
|
|
1980
|
+
)
|
|
1981
|
+
] })
|
|
1982
|
+
]
|
|
1983
|
+
}
|
|
1984
|
+
);
|
|
2055
1985
|
}
|
|
2056
|
-
|
|
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;
|
|
2063
2294
|
height: 36px;
|
|
2064
2295
|
border-radius: var(--fnc-radius);
|
|
2065
2296
|
color: var(--fnc-text-2);
|
|
@@ -2114,7 +2345,9 @@ var CSS = (
|
|
|
2114
2345
|
|
|
2115
2346
|
/* \u2500\u2500 Chart body & overlays \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2116
2347
|
.fnc-body { position: relative; display: flex; flex: 1; min-height: 0; }
|
|
2117
|
-
.fnc-
|
|
2348
|
+
.fnc-main { position: relative; display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; }
|
|
2349
|
+
.fnc-stage { position: relative; flex: 1; min-width: 0; min-height: 0; }
|
|
2350
|
+
.fnc-slot-bottom { flex: none; min-width: 0; }
|
|
2118
2351
|
.fnc-chart-host, .fnc-overlay { position: absolute; inset: 0; }
|
|
2119
2352
|
|
|
2120
2353
|
/* \u2500\u2500 Drawing tool bar (left) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
@@ -2699,6 +2932,103 @@ var CSS = (
|
|
|
2699
2932
|
.fnc-toolbar > .fnc-spacer { min-width: 0; }
|
|
2700
2933
|
}
|
|
2701
2934
|
|
|
2935
|
+
/* \u2500\u2500 Multi-chart (split panes) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2936
|
+
.fnc-grid { display: flex; flex: 1; min-width: 0; min-height: 0; }
|
|
2937
|
+
.fnc-split { display: flex; min-width: 0; min-height: 0; }
|
|
2938
|
+
.fnc-split-row { flex-direction: row; }
|
|
2939
|
+
.fnc-split-col { flex-direction: column; }
|
|
2940
|
+
.fnc-pane { position: relative; display: flex; min-width: 0; min-height: 0; }
|
|
2941
|
+
.fnc-pane > .fnc-stage { flex: 1; min-height: 0; }
|
|
2942
|
+
.fnc-split-on .fnc-pane-active::after {
|
|
2943
|
+
content: '';
|
|
2944
|
+
position: absolute;
|
|
2945
|
+
inset: 0;
|
|
2946
|
+
border: 1px solid var(--fnc-accent);
|
|
2947
|
+
pointer-events: none;
|
|
2948
|
+
z-index: 3;
|
|
2949
|
+
}
|
|
2950
|
+
.fnc-splitter { position: relative; flex: none; background: var(--fnc-border); z-index: 4; touch-action: none; }
|
|
2951
|
+
.fnc-splitter::after { content: ''; position: absolute; }
|
|
2952
|
+
.fnc-splitter-row { width: 1px; cursor: col-resize; }
|
|
2953
|
+
.fnc-splitter-row::after { inset: 0 -4px; }
|
|
2954
|
+
.fnc-splitter-col { height: 1px; cursor: row-resize; }
|
|
2955
|
+
.fnc-splitter-col::after { inset: -4px 0; }
|
|
2956
|
+
.fnc-splitter:hover { background: var(--fnc-accent); }
|
|
2957
|
+
.fnc-resizing-x, .fnc-resizing-x * { cursor: col-resize !important; user-select: none; }
|
|
2958
|
+
.fnc-resizing-y, .fnc-resizing-y * { cursor: row-resize !important; user-select: none; }
|
|
2959
|
+
.fnc-parking { display: none; }
|
|
2960
|
+
/* Navigation controls follow the hovered pane, not the whole widget. */
|
|
2961
|
+
.fnc-multi .fnc-body:hover .fnc-nav { opacity: 0; }
|
|
2962
|
+
.fnc-multi .fnc-stage:hover .fnc-nav, .fnc-multi .fnc-nav:focus-within { opacity: 1; }
|
|
2963
|
+
.fnc-pane-max {
|
|
2964
|
+
position: absolute;
|
|
2965
|
+
top: 6px;
|
|
2966
|
+
width: 28px;
|
|
2967
|
+
height: 28px;
|
|
2968
|
+
opacity: 0;
|
|
2969
|
+
background: var(--fnc-bg);
|
|
2970
|
+
pointer-events: auto;
|
|
2971
|
+
z-index: 2;
|
|
2972
|
+
}
|
|
2973
|
+
.fnc-stage:hover .fnc-pane-max, .fnc-pane-max:focus-visible, .fnc-pane-max[aria-pressed='true'] { opacity: 1; }
|
|
2974
|
+
@media (hover: none) { .fnc-pane-max { opacity: 0.7; } }
|
|
2975
|
+
.fnc-pane-tabs {
|
|
2976
|
+
display: flex;
|
|
2977
|
+
gap: 4px;
|
|
2978
|
+
padding: 4px 8px;
|
|
2979
|
+
overflow-x: auto;
|
|
2980
|
+
scrollbar-width: none;
|
|
2981
|
+
border-bottom: 1px solid var(--fnc-border);
|
|
2982
|
+
}
|
|
2983
|
+
.fnc-pane-tab {
|
|
2984
|
+
display: flex;
|
|
2985
|
+
align-items: center;
|
|
2986
|
+
gap: 6px;
|
|
2987
|
+
flex: none;
|
|
2988
|
+
height: 30px;
|
|
2989
|
+
padding: 0 10px 0 6px;
|
|
2990
|
+
border-radius: var(--fnc-radius);
|
|
2991
|
+
font-size: 13px;
|
|
2992
|
+
font-weight: 600;
|
|
2993
|
+
color: var(--fnc-text-2);
|
|
2994
|
+
white-space: nowrap;
|
|
2995
|
+
}
|
|
2996
|
+
.fnc-pane-tab small { font-size: 12px; font-weight: 400; color: var(--fnc-text-2); }
|
|
2997
|
+
.fnc-pane-tab[aria-selected='true'] { background: var(--fnc-surface); color: var(--fnc-text); }
|
|
2998
|
+
.fnc-pane-tab-num {
|
|
2999
|
+
display: grid;
|
|
3000
|
+
place-items: center;
|
|
3001
|
+
width: 18px;
|
|
3002
|
+
height: 18px;
|
|
3003
|
+
border-radius: 5px;
|
|
3004
|
+
background: var(--fnc-surface);
|
|
3005
|
+
font-size: 11px;
|
|
3006
|
+
}
|
|
3007
|
+
.fnc-pane-tab[aria-selected='true'] .fnc-pane-tab-num { background: var(--fnc-accent); color: #fff; }
|
|
3008
|
+
.fnc-layout-menu { min-width: 0; gap: 2px; padding: 8px; }
|
|
3009
|
+
.fnc-layout-row { display: flex; align-items: center; gap: 2px; }
|
|
3010
|
+
.fnc-layout-count { width: 16px; flex: none; font-size: 12px; color: var(--fnc-text-2); text-align: center; }
|
|
3011
|
+
.fnc-layout-opt {
|
|
3012
|
+
display: grid;
|
|
3013
|
+
place-items: center;
|
|
3014
|
+
width: 36px;
|
|
3015
|
+
height: 32px;
|
|
3016
|
+
border-radius: var(--fnc-radius);
|
|
3017
|
+
color: var(--fnc-text-2);
|
|
3018
|
+
}
|
|
3019
|
+
.fnc-layout-opt:hover { background: var(--fnc-surface); color: var(--fnc-text); }
|
|
3020
|
+
.fnc-layout-opt[aria-checked='true'] { background: var(--fnc-surface); color: var(--fnc-accent); }
|
|
3021
|
+
.fnc-sync-menu { min-width: 220px; }
|
|
3022
|
+
.fnc-menu-title { display: flex; flex-direction: column; gap: 2px; padding: 6px 10px 8px; font-size: 13px; font-weight: 600; color: var(--fnc-text); }
|
|
3023
|
+
.fnc-menu-title small { font-size: 12px; font-weight: 400; color: var(--fnc-text-2); }
|
|
3024
|
+
.fnc-sync-item { justify-content: space-between; }
|
|
3025
|
+
.fnc-sync-item[aria-checked='true'] { background: none; color: var(--fnc-text); }
|
|
3026
|
+
.fnc-sync-item:hover { background: var(--fnc-surface); }
|
|
3027
|
+
.fnc-sync-item .fnc-switch { display: inline-block; transform: scale(0.8); transform-origin: right center; }
|
|
3028
|
+
.fnc-switch[data-on='true'] { background: var(--fnc-accent); }
|
|
3029
|
+
.fnc-switch[data-on='true']::after { transform: translateX(16px); }
|
|
3030
|
+
.fnc-btn-sync[aria-pressed='true'] { color: var(--fnc-accent); }
|
|
3031
|
+
|
|
2702
3032
|
/* \u2500\u2500 Mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2703
3033
|
@media (max-width: 640px) {
|
|
2704
3034
|
.fnc-toolbar { padding: 0 4px; gap: 2px; }
|
|
@@ -2711,6 +3041,7 @@ var CSS = (
|
|
|
2711
3041
|
.fnc-chip { padding: 0 8px; }
|
|
2712
3042
|
.fnc-btn-screenshot { display: none; }
|
|
2713
3043
|
.fnc-btn-label span { display: none; }
|
|
3044
|
+
.fnc-btn-sync { display: none; }
|
|
2714
3045
|
.fnc-intervals { display: none; }
|
|
2715
3046
|
.fnc-interval-current { display: inline; }
|
|
2716
3047
|
.fnc-interval-menu { left: auto; right: -40px; }
|
|
@@ -2752,21 +3083,7 @@ function injectStyles(doc = document) {
|
|
|
2752
3083
|
doc.head.appendChild(style);
|
|
2753
3084
|
}
|
|
2754
3085
|
|
|
2755
|
-
// src/
|
|
2756
|
-
var DEFAULT_INTERVAL_GROUPS = [
|
|
2757
|
-
{ id: "minutes", intervals: ["1", "5", "15", "30"] },
|
|
2758
|
-
{ id: "hours", intervals: ["60", "240"] },
|
|
2759
|
-
{ id: "days", intervals: ["1D", "1W", "1M", "3M", "6M", "12M"] }
|
|
2760
|
-
];
|
|
2761
|
-
var DEFAULT_FAVORITE_INTERVALS = ["1", "5", "15", "60", "240", "1D"];
|
|
2762
|
-
var stockLike = (i3) => i3.type === "stock" || i3.type === "etf" || i3.type === "index";
|
|
2763
|
-
var DEFAULT_SEARCH_CATEGORIES = [
|
|
2764
|
-
{ id: "all", label: { ko: "\uC804\uCCB4", en: "All" }, match: () => true },
|
|
2765
|
-
{ id: "kr-stock", label: { ko: "\uAD6D\uB0B4\uC8FC\uC2DD", en: "KR stocks" }, match: (i3) => stockLike(i3) && i3.country === "KR" },
|
|
2766
|
-
{ id: "global-stock", label: { ko: "\uD574\uC678\uC8FC\uC2DD", en: "Global stocks" }, match: (i3) => stockLike(i3) && i3.country !== "KR" },
|
|
2767
|
-
{ id: "crypto", label: { ko: "\uCF54\uC778", en: "Crypto" }, match: (i3) => i3.type === "crypto" },
|
|
2768
|
-
{ id: "futures", label: { ko: "\uC120\uBB3C", en: "Futures" }, match: (i3) => i3.type === "futures" }
|
|
2769
|
-
];
|
|
3086
|
+
// src/shell.ts
|
|
2770
3087
|
var THEME_VARS = {
|
|
2771
3088
|
background: "--fnc-bg",
|
|
2772
3089
|
surface: "--fnc-surface",
|
|
@@ -2785,37 +3102,19 @@ function applyThemeVars(el, theme) {
|
|
|
2785
3102
|
el.style.setProperty("--fnc-radius", `${theme.radius}px`);
|
|
2786
3103
|
}
|
|
2787
3104
|
function div(className, parent) {
|
|
2788
|
-
const el = document.createElement("div");
|
|
2789
|
-
el.className = className;
|
|
2790
|
-
parent?.appendChild(el);
|
|
2791
|
-
return el;
|
|
2792
|
-
}
|
|
2793
|
-
function
|
|
2794
|
-
const {
|
|
2795
|
-
datafeed,
|
|
2796
|
-
symbol,
|
|
2797
|
-
resolution = "1D",
|
|
2798
|
-
intervalGroups = DEFAULT_INTERVAL_GROUPS,
|
|
2799
|
-
favoriteIntervals,
|
|
2800
|
-
onFavoriteIntervalsChange,
|
|
2801
|
-
searchCategories = DEFAULT_SEARCH_CATEGORIES,
|
|
2802
|
-
storageKey = "funnest-charts",
|
|
2803
|
-
searchHotkey = true,
|
|
2804
|
-
symbolLogo = true,
|
|
2805
|
-
drawingToolbar = true,
|
|
2806
|
-
chartType = "candles",
|
|
2807
|
-
attribution = true,
|
|
2808
|
-
colorScheme = "system",
|
|
2809
|
-
...chartOptions
|
|
2810
|
-
} = options;
|
|
2811
|
-
const locale = chartOptions.locale ?? "ko";
|
|
2812
|
-
const displayTimezone = chartOptions.displayTimezone ?? (locale === "ko" ? "Asia/Seoul" : Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
2813
|
-
const storage = createStorage(storageKey);
|
|
3105
|
+
const el = document.createElement("div");
|
|
3106
|
+
el.className = className;
|
|
3107
|
+
parent?.appendChild(el);
|
|
3108
|
+
return el;
|
|
3109
|
+
}
|
|
3110
|
+
function createShell(container, options) {
|
|
2814
3111
|
injectStyles(container.ownerDocument);
|
|
3112
|
+
const storage = createStorage(options.storageKey);
|
|
2815
3113
|
const win = container.ownerDocument.defaultView;
|
|
2816
3114
|
const darkQuery = win?.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
|
|
2817
|
-
let scheme = colorScheme;
|
|
2818
|
-
let themePatch = { ...
|
|
3115
|
+
let scheme = options.colorScheme;
|
|
3116
|
+
let themePatch = { ...options.theme };
|
|
3117
|
+
let panes = () => [];
|
|
2819
3118
|
const isDark = () => scheme === "system" ? !!darkQuery?.matches : scheme === "dark";
|
|
2820
3119
|
const resolveTheme = () => ({ ...isDark() ? darkTheme : lightTheme, ...themePatch });
|
|
2821
3120
|
let theme = resolveTheme();
|
|
@@ -2831,33 +3130,138 @@ function createTradingChart(container, options) {
|
|
|
2831
3130
|
toolbar: div("fnc-slot fnc-slot-toolbar"),
|
|
2832
3131
|
toolbarEnd: div("fnc-slot fnc-slot-toolbar-end"),
|
|
2833
3132
|
belowToolbar: div("fnc-slot fnc-slot-below-toolbar", root),
|
|
3133
|
+
bottom: div("fnc-slot fnc-slot-bottom"),
|
|
2834
3134
|
footer: div("fnc-slot fnc-slot-footer")
|
|
2835
3135
|
};
|
|
2836
3136
|
const body = div("fnc-body", root);
|
|
2837
|
-
const drawbarHost = drawingToolbar ? div("fnc-drawbar-host", body) : null;
|
|
2838
|
-
const
|
|
2839
|
-
|
|
2840
|
-
const overlayHost = div("fnc-overlay", stage);
|
|
3137
|
+
const drawbarHost = options.drawingToolbar ? div("fnc-drawbar-host", body) : null;
|
|
3138
|
+
const main = div("fnc-main", body);
|
|
3139
|
+
main.appendChild(slots.bottom);
|
|
2841
3140
|
root.appendChild(slots.footer);
|
|
2842
|
-
const chart = createChart(chartHost, { ...chartOptions, theme, datafeed, displayTimezone });
|
|
2843
3141
|
const refreshScheme = () => {
|
|
2844
3142
|
theme = resolveTheme();
|
|
2845
3143
|
applyScheme();
|
|
2846
|
-
chart.setTheme(theme);
|
|
3144
|
+
for (const p3 of panes()) p3.chart.setTheme(theme);
|
|
2847
3145
|
};
|
|
2848
3146
|
const onSchemeChange = () => scheme === "system" && refreshScheme();
|
|
2849
3147
|
darkQuery?.addEventListener?.("change", onSchemeChange);
|
|
2850
|
-
|
|
3148
|
+
let favoriteIntervals = storage.get("favorite-intervals", [...options.favoriteIntervals]);
|
|
3149
|
+
let drawbarCollapsed = storage.get("drawbar-collapsed", false);
|
|
3150
|
+
const broadcast = (patch) => {
|
|
3151
|
+
for (const p3 of panes()) p3.store.set(patch);
|
|
3152
|
+
};
|
|
3153
|
+
const isNativeFullscreen = () => document.fullscreenElement === root;
|
|
3154
|
+
const isFullscreen = () => isNativeFullscreen() || root.classList.contains("fnc-pseudo-fullscreen");
|
|
3155
|
+
const onFullscreenChange = () => broadcast({ fullscreen: isFullscreen() });
|
|
3156
|
+
document.addEventListener("fullscreenchange", onFullscreenChange);
|
|
3157
|
+
const pseudo = (on) => {
|
|
3158
|
+
root.classList.toggle("fnc-pseudo-fullscreen", on);
|
|
3159
|
+
onFullscreenChange();
|
|
3160
|
+
};
|
|
3161
|
+
return {
|
|
3162
|
+
root,
|
|
3163
|
+
toolbarHost,
|
|
3164
|
+
body,
|
|
3165
|
+
drawbarHost,
|
|
3166
|
+
main,
|
|
3167
|
+
slots,
|
|
3168
|
+
storage,
|
|
3169
|
+
theme: () => theme,
|
|
3170
|
+
sharedState: () => ({ fullscreen: isFullscreen(), favoriteIntervals, drawbarCollapsed }),
|
|
3171
|
+
setPanes: (get) => {
|
|
3172
|
+
panes = get;
|
|
3173
|
+
},
|
|
3174
|
+
setTheme: (patch) => {
|
|
3175
|
+
themePatch = { ...themePatch, ...patch };
|
|
3176
|
+
refreshScheme();
|
|
3177
|
+
},
|
|
3178
|
+
setColorScheme: (next) => {
|
|
3179
|
+
scheme = next;
|
|
3180
|
+
refreshScheme();
|
|
3181
|
+
},
|
|
3182
|
+
isFullscreen,
|
|
3183
|
+
toggleFullscreen: () => {
|
|
3184
|
+
if (isNativeFullscreen()) void document.exitFullscreen();
|
|
3185
|
+
else if (root.classList.contains("fnc-pseudo-fullscreen")) pseudo(false);
|
|
3186
|
+
else if (root.requestFullscreen && document.fullscreenEnabled) root.requestFullscreen().catch(() => pseudo(true));
|
|
3187
|
+
else pseudo(true);
|
|
3188
|
+
},
|
|
3189
|
+
setDrawbarCollapsed: (collapsed) => {
|
|
3190
|
+
drawbarCollapsed = collapsed;
|
|
3191
|
+
storage.set("drawbar-collapsed", collapsed);
|
|
3192
|
+
broadcast({ drawbarCollapsed });
|
|
3193
|
+
},
|
|
3194
|
+
toggleFavoriteInterval: (res) => {
|
|
3195
|
+
favoriteIntervals = favoriteIntervals.includes(res) ? favoriteIntervals.filter((r3) => r3 !== res) : [...favoriteIntervals, res];
|
|
3196
|
+
storage.set("favorite-intervals", favoriteIntervals);
|
|
3197
|
+
broadcast({ favoriteIntervals });
|
|
3198
|
+
options.onFavoriteIntervalsChange?.(favoriteIntervals);
|
|
3199
|
+
},
|
|
3200
|
+
destroy: () => {
|
|
3201
|
+
document.removeEventListener("fullscreenchange", onFullscreenChange);
|
|
3202
|
+
darkQuery?.removeEventListener?.("change", onSchemeChange);
|
|
3203
|
+
if (isNativeFullscreen()) void document.exitFullscreen();
|
|
3204
|
+
root.remove();
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
function bindWidgetKeys(shell, active, searchHotkey, extra) {
|
|
3209
|
+
const { root } = shell;
|
|
3210
|
+
const onKeyDown = (e3) => {
|
|
3211
|
+
const pane = active();
|
|
3212
|
+
if (!pane) return;
|
|
3213
|
+
const s3 = pane.store.get();
|
|
3214
|
+
if (searchHotkey && (e3.ctrlKey || e3.metaKey) && e3.key.toLowerCase() === "k" && pane.canSearch && !s3.dialog) {
|
|
3215
|
+
e3.preventDefault();
|
|
3216
|
+
pane.store.set({ searchOpen: true });
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
if (e3.key === "Escape" && !e3.defaultPrevented && (s3.searchOpen || s3.dialog)) {
|
|
3220
|
+
e3.preventDefault();
|
|
3221
|
+
pane.store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
if (e3.key === "Escape" && root.classList.contains("fnc-pseudo-fullscreen") && !s3.searchOpen && !s3.dialog) {
|
|
3225
|
+
shell.toggleFullscreen();
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
if (extra?.(e3)) e3.preventDefault();
|
|
3229
|
+
};
|
|
3230
|
+
const onKeyUp = (e3) => {
|
|
3231
|
+
const pane = active();
|
|
3232
|
+
if (!pane) return;
|
|
3233
|
+
const s3 = pane.store.get();
|
|
3234
|
+
if (e3.key === "Escape" && (s3.searchOpen || s3.dialog)) pane.store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
|
|
3235
|
+
};
|
|
3236
|
+
document.addEventListener("keydown", onKeyDown);
|
|
3237
|
+
document.addEventListener("keyup", onKeyUp);
|
|
3238
|
+
return () => {
|
|
3239
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
3240
|
+
document.removeEventListener("keyup", onKeyUp);
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// src/pane.tsx
|
|
3245
|
+
function createPane(shell, config) {
|
|
3246
|
+
const stage = div("fnc-stage");
|
|
3247
|
+
const chartHost = div("fnc-chart-host", stage);
|
|
3248
|
+
const overlayHost = div("fnc-overlay", stage);
|
|
3249
|
+
const chart = createChart(chartHost, {
|
|
3250
|
+
...config.chartOptions,
|
|
3251
|
+
theme: shell.theme(),
|
|
3252
|
+
datafeed: config.datafeed,
|
|
3253
|
+
displayTimezone: config.displayTimezone
|
|
3254
|
+
});
|
|
3255
|
+
chart.setChartType(config.chartType);
|
|
2851
3256
|
const store = new Store({
|
|
2852
|
-
locale,
|
|
3257
|
+
locale: config.chartOptions.locale ?? "ko",
|
|
2853
3258
|
symbol: null,
|
|
2854
|
-
resolution,
|
|
3259
|
+
resolution: config.resolution,
|
|
2855
3260
|
loading: false,
|
|
2856
3261
|
layout: chart.layout,
|
|
2857
3262
|
atRealtime: true,
|
|
2858
|
-
fullscreen: false,
|
|
2859
3263
|
searchOpen: false,
|
|
2860
|
-
chartType,
|
|
3264
|
+
chartType: config.chartType,
|
|
2861
3265
|
drawingTool: null,
|
|
2862
3266
|
selection: null,
|
|
2863
3267
|
drawingCount: 0,
|
|
@@ -2865,12 +3269,11 @@ function createTradingChart(container, options) {
|
|
|
2865
3269
|
magnet: chart.magnetEnabled,
|
|
2866
3270
|
indicators: chart.getIndicators(),
|
|
2867
3271
|
dialog: null,
|
|
2868
|
-
|
|
2869
|
-
drawbarCollapsed: storage.get("drawbar-collapsed", false)
|
|
3272
|
+
...shell.sharedState()
|
|
2870
3273
|
});
|
|
2871
3274
|
const syncRealtime = () => store.set({ atRealtime: chart.isAtRealtime() });
|
|
2872
3275
|
const offs = [
|
|
2873
|
-
chart.on("symbolChange", (info) => store.set({ symbol: info, resolution: chart.currentResolution ?? resolution })),
|
|
3276
|
+
chart.on("symbolChange", (info) => store.set({ symbol: info, resolution: chart.currentResolution ?? store.get().resolution })),
|
|
2874
3277
|
chart.on("loadingChange", (loading) => store.set({ loading })),
|
|
2875
3278
|
chart.on("layoutChange", (layout) => store.set({ layout })),
|
|
2876
3279
|
chart.on("visibleRangeChange", syncRealtime),
|
|
@@ -2880,7 +3283,7 @@ function createTradingChart(container, options) {
|
|
|
2880
3283
|
chart.on("drawingsChange", (drawings) => store.set({ drawingCount: drawings.length })),
|
|
2881
3284
|
chart.on("indicatorsChange", (indicators) => store.set({ indicators })),
|
|
2882
3285
|
chart.on("magnetChange", (magnet) => store.set({ magnet })),
|
|
2883
|
-
chart.on("chartTypeChange", (
|
|
3286
|
+
chart.on("chartTypeChange", (chartType) => store.set({ chartType }))
|
|
2884
3287
|
];
|
|
2885
3288
|
const load = (sym, res) => {
|
|
2886
3289
|
store.set({ resolution: res });
|
|
@@ -2889,61 +3292,27 @@ function createTradingChart(container, options) {
|
|
|
2889
3292
|
store.set({ loading: false });
|
|
2890
3293
|
});
|
|
2891
3294
|
};
|
|
2892
|
-
const isNativeFullscreen = () => document.fullscreenElement === root;
|
|
2893
|
-
const onFullscreenChange = () => store.set({ fullscreen: isNativeFullscreen() || root.classList.contains("fnc-pseudo-fullscreen") });
|
|
2894
|
-
const onKeyDown = (e3) => {
|
|
2895
|
-
const s3 = store.get();
|
|
2896
|
-
if (searchHotkey && (e3.ctrlKey || e3.metaKey) && e3.key.toLowerCase() === "k" && datafeed.searchSymbols && !s3.dialog) {
|
|
2897
|
-
e3.preventDefault();
|
|
2898
|
-
store.set({ searchOpen: true });
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
if (e3.key === "Escape" && !e3.defaultPrevented && (s3.searchOpen || s3.dialog)) {
|
|
2902
|
-
e3.preventDefault();
|
|
2903
|
-
store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
|
|
2904
|
-
return;
|
|
2905
|
-
}
|
|
2906
|
-
if (e3.key === "Escape" && root.classList.contains("fnc-pseudo-fullscreen") && !s3.searchOpen && !s3.dialog) {
|
|
2907
|
-
root.classList.remove("fnc-pseudo-fullscreen");
|
|
2908
|
-
onFullscreenChange();
|
|
2909
|
-
}
|
|
2910
|
-
};
|
|
2911
|
-
const onKeyUp = (e3) => {
|
|
2912
|
-
const s3 = store.get();
|
|
2913
|
-
if (e3.key === "Escape" && (s3.searchOpen || s3.dialog)) store.set(s3.searchOpen ? { searchOpen: false } : { dialog: null });
|
|
2914
|
-
};
|
|
2915
|
-
document.addEventListener("fullscreenchange", onFullscreenChange);
|
|
2916
|
-
document.addEventListener("keydown", onKeyDown);
|
|
2917
|
-
document.addEventListener("keyup", onKeyUp);
|
|
2918
3295
|
const ctx = {
|
|
2919
|
-
root,
|
|
3296
|
+
root: shell.root,
|
|
2920
3297
|
chart,
|
|
2921
|
-
datafeed,
|
|
3298
|
+
datafeed: config.datafeed,
|
|
2922
3299
|
store,
|
|
2923
|
-
storage,
|
|
2924
|
-
intervalGroups,
|
|
2925
|
-
searchCategories,
|
|
2926
|
-
displayTimezone,
|
|
2927
|
-
showLogo: symbolLogo,
|
|
2928
|
-
drawingToolbar,
|
|
2929
|
-
attribution,
|
|
2930
|
-
slots,
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
storage.set("drawbar-collapsed", collapsed);
|
|
2934
|
-
},
|
|
3300
|
+
storage: shell.storage,
|
|
3301
|
+
intervalGroups: config.intervalGroups,
|
|
3302
|
+
searchCategories: config.searchCategories,
|
|
3303
|
+
displayTimezone: config.displayTimezone,
|
|
3304
|
+
showLogo: config.symbolLogo,
|
|
3305
|
+
drawingToolbar: config.drawingToolbar,
|
|
3306
|
+
attribution: config.attribution,
|
|
3307
|
+
slots: shell.slots,
|
|
3308
|
+
multi: config.multi,
|
|
3309
|
+
setDrawbarCollapsed: shell.setDrawbarCollapsed,
|
|
2935
3310
|
selectSymbol: (sym) => void load(sym, store.get().resolution),
|
|
2936
3311
|
selectResolution: (res) => {
|
|
2937
3312
|
const current = store.get().symbol;
|
|
2938
3313
|
if (current) void load(current.symbol, res);
|
|
2939
3314
|
},
|
|
2940
|
-
toggleFavoriteInterval:
|
|
2941
|
-
const current = store.get().favoriteIntervals;
|
|
2942
|
-
const next = current.includes(res) ? current.filter((r3) => r3 !== res) : [...current, res];
|
|
2943
|
-
store.set({ favoriteIntervals: next });
|
|
2944
|
-
storage.set("favorite-intervals", next);
|
|
2945
|
-
onFavoriteIntervalsChange?.(next);
|
|
2946
|
-
},
|
|
3315
|
+
toggleFavoriteInterval: shell.toggleFavoriteInterval,
|
|
2947
3316
|
setSearchOpen: (open) => store.set({ searchOpen: open }),
|
|
2948
3317
|
screenshot: () => {
|
|
2949
3318
|
const s3 = store.get();
|
|
@@ -2956,61 +3325,491 @@ function createTradingChart(container, options) {
|
|
|
2956
3325
|
setTimeout(() => URL.revokeObjectURL(a4.href), 1e3);
|
|
2957
3326
|
});
|
|
2958
3327
|
},
|
|
2959
|
-
toggleFullscreen:
|
|
2960
|
-
if (isNativeFullscreen()) {
|
|
2961
|
-
void document.exitFullscreen();
|
|
2962
|
-
} else if (root.classList.contains("fnc-pseudo-fullscreen")) {
|
|
2963
|
-
root.classList.remove("fnc-pseudo-fullscreen");
|
|
2964
|
-
onFullscreenChange();
|
|
2965
|
-
} else if (root.requestFullscreen && document.fullscreenEnabled) {
|
|
2966
|
-
root.requestFullscreen().catch(() => {
|
|
2967
|
-
root.classList.add("fnc-pseudo-fullscreen");
|
|
2968
|
-
onFullscreenChange();
|
|
2969
|
-
});
|
|
2970
|
-
} else {
|
|
2971
|
-
root.classList.add("fnc-pseudo-fullscreen");
|
|
2972
|
-
onFullscreenChange();
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
3328
|
+
toggleFullscreen: shell.toggleFullscreen
|
|
2975
3329
|
};
|
|
2976
|
-
Q(/* @__PURE__ */ a2(Toolbar, { ctx }), toolbarHost);
|
|
2977
3330
|
Q(/* @__PURE__ */ a2(Overlay, { ctx }), overlayHost);
|
|
2978
|
-
if (drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx }), drawbarHost);
|
|
2979
|
-
void load(symbol, resolution);
|
|
2980
3331
|
return {
|
|
3332
|
+
stage,
|
|
2981
3333
|
chart,
|
|
2982
|
-
|
|
2983
|
-
|
|
3334
|
+
store,
|
|
3335
|
+
ctx,
|
|
3336
|
+
canSearch: typeof config.datafeed.searchSymbols === "function",
|
|
3337
|
+
load,
|
|
3338
|
+
destroy: () => {
|
|
3339
|
+
offs.forEach((off) => off());
|
|
3340
|
+
Q(null, overlayHost);
|
|
3341
|
+
chart.destroy();
|
|
3342
|
+
stage.remove();
|
|
3343
|
+
}
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// src/trading-chart.tsx
|
|
3348
|
+
var DEFAULT_INTERVAL_GROUPS = [
|
|
3349
|
+
{ id: "minutes", intervals: ["1", "5", "15", "30"] },
|
|
3350
|
+
{ id: "hours", intervals: ["60", "240"] },
|
|
3351
|
+
{ id: "days", intervals: ["1D", "1W", "1M", "3M", "6M", "12M"] }
|
|
3352
|
+
];
|
|
3353
|
+
var DEFAULT_FAVORITE_INTERVALS = ["1", "5", "15", "60", "240", "1D"];
|
|
3354
|
+
var stockLike = (i3) => i3.type === "stock" || i3.type === "etf" || i3.type === "index";
|
|
3355
|
+
var DEFAULT_SEARCH_CATEGORIES = [
|
|
3356
|
+
{ id: "all", label: { ko: "\uC804\uCCB4", en: "All" }, match: () => true },
|
|
3357
|
+
{ id: "kr-stock", label: { ko: "\uAD6D\uB0B4\uC8FC\uC2DD", en: "KR stocks" }, match: (i3) => stockLike(i3) && i3.country === "KR" },
|
|
3358
|
+
{ id: "global-stock", label: { ko: "\uD574\uC678\uC8FC\uC2DD", en: "Global stocks" }, match: (i3) => stockLike(i3) && i3.country !== "KR" },
|
|
3359
|
+
{ id: "crypto", label: { ko: "\uCF54\uC778", en: "Crypto" }, match: (i3) => i3.type === "crypto" },
|
|
3360
|
+
{ id: "futures", label: { ko: "\uC120\uBB3C", en: "Futures" }, match: (i3) => i3.type === "futures" }
|
|
3361
|
+
];
|
|
3362
|
+
function defaultDisplayTimezone(locale, explicit) {
|
|
3363
|
+
return explicit ?? (locale === "ko" ? "Asia/Seoul" : Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
3364
|
+
}
|
|
3365
|
+
function createTradingChart(container, options) {
|
|
3366
|
+
const {
|
|
3367
|
+
datafeed,
|
|
3368
|
+
symbol,
|
|
3369
|
+
resolution = "1D",
|
|
3370
|
+
intervalGroups = DEFAULT_INTERVAL_GROUPS,
|
|
3371
|
+
favoriteIntervals,
|
|
3372
|
+
onFavoriteIntervalsChange,
|
|
3373
|
+
searchCategories = DEFAULT_SEARCH_CATEGORIES,
|
|
3374
|
+
storageKey = "funnest-charts",
|
|
3375
|
+
searchHotkey = true,
|
|
3376
|
+
symbolLogo = true,
|
|
3377
|
+
drawingToolbar = true,
|
|
3378
|
+
chartType = "candles",
|
|
3379
|
+
attribution = true,
|
|
3380
|
+
colorScheme = "system",
|
|
3381
|
+
theme,
|
|
3382
|
+
...chartOptions
|
|
3383
|
+
} = options;
|
|
3384
|
+
const displayTimezone = defaultDisplayTimezone(chartOptions.locale ?? "ko", chartOptions.displayTimezone);
|
|
3385
|
+
const shell = createShell(container, {
|
|
3386
|
+
colorScheme,
|
|
3387
|
+
theme,
|
|
3388
|
+
storageKey,
|
|
3389
|
+
drawingToolbar,
|
|
3390
|
+
favoriteIntervals: favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS,
|
|
3391
|
+
onFavoriteIntervalsChange
|
|
3392
|
+
});
|
|
3393
|
+
const pane = createPane(shell, {
|
|
3394
|
+
datafeed,
|
|
3395
|
+
chartOptions,
|
|
3396
|
+
displayTimezone,
|
|
3397
|
+
resolution,
|
|
3398
|
+
chartType,
|
|
3399
|
+
intervalGroups,
|
|
3400
|
+
searchCategories,
|
|
3401
|
+
symbolLogo,
|
|
3402
|
+
drawingToolbar,
|
|
3403
|
+
attribution
|
|
3404
|
+
});
|
|
3405
|
+
shell.main.insertBefore(pane.stage, shell.slots.bottom);
|
|
3406
|
+
shell.setPanes(() => [pane]);
|
|
3407
|
+
const unbindKeys = bindWidgetKeys(shell, () => pane, searchHotkey);
|
|
3408
|
+
Q(/* @__PURE__ */ a2(Toolbar, { ctx: pane.ctx }), shell.toolbarHost);
|
|
3409
|
+
if (shell.drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx: pane.ctx }), shell.drawbarHost);
|
|
3410
|
+
void pane.load(symbol, resolution);
|
|
3411
|
+
return {
|
|
3412
|
+
chart: pane.chart,
|
|
3413
|
+
slots: shell.slots,
|
|
3414
|
+
setSymbol: (sym, res) => pane.load(sym, res ?? pane.store.get().resolution),
|
|
2984
3415
|
setResolution: (res) => {
|
|
2985
|
-
const current = store.get().symbol;
|
|
2986
|
-
return current ? load(current.symbol, res) : Promise.resolve();
|
|
3416
|
+
const current = pane.store.get().symbol;
|
|
3417
|
+
return current ? pane.load(current.symbol, res) : Promise.resolve();
|
|
2987
3418
|
},
|
|
2988
|
-
setLocale: (
|
|
2989
|
-
chart.setLocale(
|
|
2990
|
-
store.set({ locale
|
|
3419
|
+
setLocale: (locale) => {
|
|
3420
|
+
pane.chart.setLocale(locale);
|
|
3421
|
+
pane.store.set({ locale });
|
|
2991
3422
|
},
|
|
2992
|
-
save: () => chart.save(),
|
|
2993
|
-
load: (state) => chart.load(state),
|
|
2994
|
-
setTheme:
|
|
2995
|
-
|
|
2996
|
-
|
|
3423
|
+
save: () => pane.chart.save(),
|
|
3424
|
+
load: (state) => pane.chart.load(state),
|
|
3425
|
+
setTheme: shell.setTheme,
|
|
3426
|
+
setColorScheme: shell.setColorScheme,
|
|
3427
|
+
destroy: () => {
|
|
3428
|
+
unbindKeys();
|
|
3429
|
+
Q(null, shell.toolbarHost);
|
|
3430
|
+
if (shell.drawbarHost) Q(null, shell.drawbarHost);
|
|
3431
|
+
pane.destroy();
|
|
3432
|
+
shell.destroy();
|
|
3433
|
+
}
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// src/multi-chart.tsx
|
|
3438
|
+
import { parseChartState } from "@funnest/charts";
|
|
3439
|
+
var DEFAULT_SYNC = { crosshair: true, time: false, symbol: false, interval: false };
|
|
3440
|
+
function PaneTab({ pane, index, active, onSelect }) {
|
|
3441
|
+
const s3 = useStore(pane.store);
|
|
3442
|
+
const t3 = STRINGS[s3.locale];
|
|
3443
|
+
return /* @__PURE__ */ a2("button", { class: "fnc-pane-tab", role: "tab", "aria-selected": active, onClick: onSelect, children: [
|
|
3444
|
+
/* @__PURE__ */ a2("span", { class: "fnc-pane-tab-num", children: index + 1 }),
|
|
3445
|
+
s3.symbol?.name ?? `${t3.pane} ${index + 1}`,
|
|
3446
|
+
/* @__PURE__ */ a2("small", { children: resolutionLabel(s3.resolution, s3.locale) })
|
|
3447
|
+
] });
|
|
3448
|
+
}
|
|
3449
|
+
function PaneTabs({ multi, panes }) {
|
|
3450
|
+
const m3 = useStore(multi.store);
|
|
3451
|
+
const visible = panes().slice(0, paneCount(LAYOUTS[m3.layout]));
|
|
3452
|
+
if (!m3.narrow || visible.length < 2) return null;
|
|
3453
|
+
return /* @__PURE__ */ a2("div", { class: "fnc-pane-tabs", role: "tablist", children: visible.map((p3, i3) => /* @__PURE__ */ a2(PaneTab, { pane: p3, index: i3, active: i3 === m3.active, onSelect: () => multi.activate(i3) })) });
|
|
3454
|
+
}
|
|
3455
|
+
function createMultiChart(container, options) {
|
|
3456
|
+
const {
|
|
3457
|
+
datafeed,
|
|
3458
|
+
panes: paneSpecs,
|
|
3459
|
+
layout: initialLayout = "1",
|
|
3460
|
+
sync: initialSync,
|
|
3461
|
+
narrowWidth = 640,
|
|
3462
|
+
resolution = "1D",
|
|
3463
|
+
intervalGroups = DEFAULT_INTERVAL_GROUPS,
|
|
3464
|
+
favoriteIntervals,
|
|
3465
|
+
onFavoriteIntervalsChange,
|
|
3466
|
+
searchCategories = DEFAULT_SEARCH_CATEGORIES,
|
|
3467
|
+
storageKey = "funnest-charts",
|
|
3468
|
+
searchHotkey = true,
|
|
3469
|
+
symbolLogo = true,
|
|
3470
|
+
drawingToolbar = true,
|
|
3471
|
+
chartType = "candles",
|
|
3472
|
+
attribution = true,
|
|
3473
|
+
colorScheme = "system",
|
|
3474
|
+
theme,
|
|
3475
|
+
...chartOptions
|
|
3476
|
+
} = options;
|
|
3477
|
+
if (paneSpecs.length === 0) throw new Error("funnest-charts: createMultiChart needs at least one pane");
|
|
3478
|
+
const displayTimezone = defaultDisplayTimezone(chartOptions.locale ?? "ko", chartOptions.displayTimezone);
|
|
3479
|
+
const shell = createShell(container, {
|
|
3480
|
+
colorScheme,
|
|
3481
|
+
theme,
|
|
3482
|
+
storageKey,
|
|
3483
|
+
drawingToolbar,
|
|
3484
|
+
favoriteIntervals: favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS,
|
|
3485
|
+
onFavoriteIntervalsChange
|
|
3486
|
+
});
|
|
3487
|
+
shell.root.classList.add("fnc-multi");
|
|
3488
|
+
const tabsHost = div("fnc-pane-tabs-host");
|
|
3489
|
+
shell.root.insertBefore(tabsHost, shell.body);
|
|
3490
|
+
const grid = div("fnc-grid");
|
|
3491
|
+
shell.main.insertBefore(grid, shell.slots.bottom);
|
|
3492
|
+
const parking = div("fnc-parking", shell.root);
|
|
3493
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
3494
|
+
const emit = (event, payload) => {
|
|
3495
|
+
for (const l3 of listeners.get(event) ?? []) l3(payload);
|
|
3496
|
+
};
|
|
3497
|
+
let stateQueued = false;
|
|
3498
|
+
const queueState = () => {
|
|
3499
|
+
if (stateQueued) return;
|
|
3500
|
+
stateQueued = true;
|
|
3501
|
+
queueMicrotask(() => {
|
|
3502
|
+
stateQueued = false;
|
|
3503
|
+
emit("stateChange", void 0);
|
|
3504
|
+
});
|
|
3505
|
+
};
|
|
3506
|
+
const store = new Store({
|
|
3507
|
+
layout: isLayoutId(initialLayout) ? initialLayout : "1",
|
|
3508
|
+
sync: { ...DEFAULT_SYNC, ...initialSync },
|
|
3509
|
+
active: 0,
|
|
3510
|
+
maximized: null,
|
|
3511
|
+
narrow: false
|
|
3512
|
+
});
|
|
3513
|
+
let sizes = {};
|
|
3514
|
+
const panes = [];
|
|
3515
|
+
const cells = [];
|
|
3516
|
+
let driver = -1;
|
|
3517
|
+
const visibleCount = () => Math.min(paneCount(LAYOUTS[store.get().layout]), MAX_PANES);
|
|
3518
|
+
const visible = () => panes.slice(0, visibleCount());
|
|
3519
|
+
const controls = {
|
|
3520
|
+
store,
|
|
3521
|
+
setLayout: (layout) => setLayout(layout),
|
|
3522
|
+
setSync: (patch) => setSync(patch),
|
|
3523
|
+
activate: (index) => activate(index),
|
|
3524
|
+
toggleMaximize: (index) => toggleMaximize(index)
|
|
3525
|
+
};
|
|
3526
|
+
function makePane(index, spec) {
|
|
3527
|
+
const pane = createPane(shell, {
|
|
3528
|
+
datafeed,
|
|
3529
|
+
chartOptions,
|
|
3530
|
+
displayTimezone,
|
|
3531
|
+
resolution: spec.resolution ?? resolution,
|
|
3532
|
+
chartType,
|
|
3533
|
+
intervalGroups,
|
|
3534
|
+
searchCategories,
|
|
3535
|
+
symbolLogo,
|
|
3536
|
+
drawingToolbar,
|
|
3537
|
+
// One badge for the whole widget, not one per pane.
|
|
3538
|
+
attribution: attribution && index === 0,
|
|
3539
|
+
multi: controls
|
|
3540
|
+
});
|
|
3541
|
+
const cell = div("fnc-pane");
|
|
3542
|
+
cell.appendChild(pane.stage);
|
|
3543
|
+
const touch = () => {
|
|
3544
|
+
driver = panes.indexOf(pane);
|
|
3545
|
+
if (driver >= 0 && driver !== store.get().active) activate(driver);
|
|
3546
|
+
};
|
|
3547
|
+
cell.addEventListener("pointerdown", touch, true);
|
|
3548
|
+
cell.addEventListener("wheel", () => driver = panes.indexOf(pane), { capture: true, passive: true });
|
|
3549
|
+
const c3 = pane.chart;
|
|
3550
|
+
c3.on("crosshairMove", (info) => {
|
|
3551
|
+
if (!store.get().sync.crosshair || info?.source === "sync") return;
|
|
3552
|
+
for (const other of visible()) if (other !== pane) other.chart.setSyncedCrosshair(info ? info.time : null);
|
|
3553
|
+
});
|
|
3554
|
+
c3.on("visibleRangeChange", () => {
|
|
3555
|
+
if (!store.get().sync.time || panes[driver] !== pane) return;
|
|
3556
|
+
broadcastTime(pane);
|
|
3557
|
+
});
|
|
3558
|
+
c3.on("symbolChange", (info) => {
|
|
3559
|
+
queueState();
|
|
3560
|
+
if (panes[store.get().active] !== pane) return;
|
|
3561
|
+
const res = c3.currentResolution;
|
|
3562
|
+
const { sync } = store.get();
|
|
3563
|
+
if (!sync.symbol && !sync.interval) return;
|
|
3564
|
+
for (const other of visible()) {
|
|
3565
|
+
if (other === pane) continue;
|
|
3566
|
+
const sym = sync.symbol ? info.symbol : other.chart.symbolInfo?.symbol;
|
|
3567
|
+
const r3 = sync.interval && res ? res : other.chart.currentResolution ?? other.store.get().resolution;
|
|
3568
|
+
if (sym && (sym !== other.chart.symbolInfo?.symbol || r3 !== other.chart.currentResolution)) void other.load(sym, r3);
|
|
3569
|
+
}
|
|
3570
|
+
});
|
|
3571
|
+
c3.on("stateChange", queueState);
|
|
3572
|
+
panes[index] = pane;
|
|
3573
|
+
cells[index] = cell;
|
|
3574
|
+
void pane.load(spec.symbol, spec.resolution ?? resolution);
|
|
3575
|
+
return pane;
|
|
3576
|
+
}
|
|
3577
|
+
function broadcastTime(from) {
|
|
3578
|
+
const range = from.chart.getVisibleTimeRange();
|
|
3579
|
+
if (!range) return;
|
|
3580
|
+
const res = from.chart.currentResolution;
|
|
3581
|
+
for (const other of visible()) {
|
|
3582
|
+
if (other !== from && other.chart.currentResolution === res) other.chart.setVisibleTimeRange(range);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
function ensurePanes(count) {
|
|
3586
|
+
for (let i3 = panes.length; i3 < count; i3++) {
|
|
3587
|
+
const src = panes[store.get().active] ?? panes[0];
|
|
3588
|
+
const spec = paneSpecs[i3] ?? {
|
|
3589
|
+
symbol: src?.chart.symbolInfo?.symbol ?? paneSpecs[0].symbol,
|
|
3590
|
+
resolution: src?.chart.currentResolution ?? src?.store.get().resolution ?? resolution
|
|
3591
|
+
};
|
|
3592
|
+
makePane(i3, spec);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
function buildNode(node, path, layout) {
|
|
3596
|
+
if (typeof node === "number") return cells[node];
|
|
3597
|
+
const box = div(`fnc-split fnc-split-${node.dir}`);
|
|
3598
|
+
const key = `${layout}:${path}`;
|
|
3599
|
+
const weights = sizesFor(sizes, key, node.children.length);
|
|
3600
|
+
const children = node.children.map((child, i3) => {
|
|
3601
|
+
const el = buildNode(child, path ? `${path}.${i3}` : String(i3), layout);
|
|
3602
|
+
el.style.flex = `${weights[i3]} 1 0px`;
|
|
3603
|
+
return el;
|
|
3604
|
+
});
|
|
3605
|
+
children.forEach((el, i3) => {
|
|
3606
|
+
box.appendChild(el);
|
|
3607
|
+
if (i3 < children.length - 1) box.appendChild(makeSplitter(box, node.dir, key, i3, children));
|
|
3608
|
+
});
|
|
3609
|
+
return box;
|
|
3610
|
+
}
|
|
3611
|
+
function makeSplitter(box, dir, key, i3, children) {
|
|
3612
|
+
const bar = div(`fnc-splitter fnc-splitter-${dir}`);
|
|
3613
|
+
bar.setAttribute("role", "separator");
|
|
3614
|
+
bar.setAttribute("aria-orientation", dir === "row" ? "vertical" : "horizontal");
|
|
3615
|
+
let last = null;
|
|
3616
|
+
const apply = (next) => {
|
|
3617
|
+
sizes = { ...sizes, [key]: next };
|
|
3618
|
+
next.forEach((w3, j2) => children[j2].style.flex = `${w3} 1 0px`);
|
|
3619
|
+
queueState();
|
|
3620
|
+
};
|
|
3621
|
+
bar.addEventListener("pointerdown", (e3) => {
|
|
3622
|
+
bar.setPointerCapture(e3.pointerId);
|
|
3623
|
+
last = dir === "row" ? e3.clientX : e3.clientY;
|
|
3624
|
+
shell.root.classList.add(dir === "row" ? "fnc-resizing-x" : "fnc-resizing-y");
|
|
3625
|
+
});
|
|
3626
|
+
bar.addEventListener("pointermove", (e3) => {
|
|
3627
|
+
if (last === null) return;
|
|
3628
|
+
const pos = dir === "row" ? e3.clientX : e3.clientY;
|
|
3629
|
+
const length = dir === "row" ? box.clientWidth : box.clientHeight;
|
|
3630
|
+
if (length <= 0) return;
|
|
3631
|
+
apply(resizePair(sizesFor(sizes, key, children.length), i3, (pos - last) / length));
|
|
3632
|
+
last = pos;
|
|
3633
|
+
});
|
|
3634
|
+
const end = () => {
|
|
3635
|
+
last = null;
|
|
3636
|
+
shell.root.classList.remove("fnc-resizing-x", "fnc-resizing-y");
|
|
3637
|
+
};
|
|
3638
|
+
bar.addEventListener("pointerup", end);
|
|
3639
|
+
bar.addEventListener("pointercancel", end);
|
|
3640
|
+
bar.addEventListener("dblclick", () => apply(Array(children.length).fill(1)));
|
|
3641
|
+
return bar;
|
|
3642
|
+
}
|
|
3643
|
+
function renderGrid() {
|
|
3644
|
+
const s3 = store.get();
|
|
3645
|
+
const count = visibleCount();
|
|
3646
|
+
for (let i3 = 0; i3 < panes.length; i3++) {
|
|
3647
|
+
if (i3 >= count) parking.appendChild(cells[i3]);
|
|
3648
|
+
cells[i3].classList.toggle("fnc-pane-active", i3 === s3.active && count > 1);
|
|
3649
|
+
}
|
|
3650
|
+
const single = s3.maximized ?? (s3.narrow ? s3.active : null);
|
|
3651
|
+
const top = single !== null && single < count ? cells[single] : buildNode(LAYOUTS[s3.layout], "", s3.layout);
|
|
3652
|
+
top.style.flex = "1 1 0px";
|
|
3653
|
+
grid.replaceChildren(top);
|
|
3654
|
+
shell.root.classList.toggle("fnc-split-on", count > 1 && single === null);
|
|
3655
|
+
}
|
|
3656
|
+
function bindToolbar() {
|
|
3657
|
+
const pane = panes[store.get().active];
|
|
3658
|
+
Q(/* @__PURE__ */ a2(Toolbar, { ctx: pane.ctx }, store.get().active), shell.toolbarHost);
|
|
3659
|
+
if (shell.drawbarHost) Q(/* @__PURE__ */ a2(DrawingToolbar, { ctx: pane.ctx }, store.get().active), shell.drawbarHost);
|
|
3660
|
+
}
|
|
3661
|
+
function activate(index) {
|
|
3662
|
+
const s3 = store.get();
|
|
3663
|
+
if (index < 0 || index >= visibleCount() || index === s3.active) return;
|
|
3664
|
+
panes[s3.active]?.chart.setDrawingTool(null);
|
|
3665
|
+
store.set({ active: index, maximized: s3.maximized === null ? null : index });
|
|
3666
|
+
bindToolbar();
|
|
3667
|
+
renderGrid();
|
|
3668
|
+
emit("activeChange", { index, chart: panes[index].chart });
|
|
3669
|
+
queueState();
|
|
3670
|
+
}
|
|
3671
|
+
function setLayout(layout) {
|
|
3672
|
+
if (!isLayoutId(layout)) return;
|
|
3673
|
+
const count = paneCount(LAYOUTS[layout]);
|
|
3674
|
+
ensurePanes(count);
|
|
3675
|
+
const s3 = store.get();
|
|
3676
|
+
for (const p3 of panes.slice(count)) p3.chart.setSyncedCrosshair(null);
|
|
3677
|
+
store.set({ layout, maximized: null, active: Math.min(s3.active, count - 1) });
|
|
3678
|
+
if (s3.active !== store.get().active) bindToolbar();
|
|
3679
|
+
renderGrid();
|
|
3680
|
+
emit("layoutChange", { layout, panes: count });
|
|
3681
|
+
queueState();
|
|
3682
|
+
}
|
|
3683
|
+
function setSync(patch) {
|
|
3684
|
+
const before = store.get().sync;
|
|
3685
|
+
const sync = { ...before, ...patch };
|
|
3686
|
+
store.set({ sync });
|
|
3687
|
+
const active = panes[store.get().active];
|
|
3688
|
+
if (!sync.crosshair) for (const p3 of panes) p3.chart.setSyncedCrosshair(null);
|
|
3689
|
+
if (sync.time && !before.time) broadcastTime(active);
|
|
3690
|
+
if (sync.symbol && !before.symbol || sync.interval && !before.interval) {
|
|
3691
|
+
const info = active.chart.symbolInfo;
|
|
3692
|
+
const res = active.chart.currentResolution;
|
|
3693
|
+
for (const other of visible()) {
|
|
3694
|
+
if (other === active || !info) continue;
|
|
3695
|
+
const sym = sync.symbol ? info.symbol : other.chart.symbolInfo?.symbol ?? info.symbol;
|
|
3696
|
+
const r3 = sync.interval && res ? res : other.chart.currentResolution ?? other.store.get().resolution;
|
|
3697
|
+
if (sym !== other.chart.symbolInfo?.symbol || r3 !== other.chart.currentResolution) void other.load(sym, r3);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
queueState();
|
|
3701
|
+
}
|
|
3702
|
+
function toggleMaximize(index) {
|
|
3703
|
+
const s3 = store.get();
|
|
3704
|
+
const target = index ?? s3.active;
|
|
3705
|
+
if (s3.maximized !== null) store.set({ maximized: null });
|
|
3706
|
+
else if (visibleCount() > 1) {
|
|
3707
|
+
if (target !== s3.active) activate(target);
|
|
3708
|
+
store.set({ maximized: target });
|
|
3709
|
+
}
|
|
3710
|
+
renderGrid();
|
|
3711
|
+
}
|
|
3712
|
+
makePane(0, paneSpecs[0]);
|
|
3713
|
+
ensurePanes(Math.max(visibleCount(), Math.min(paneSpecs.length, MAX_PANES)));
|
|
3714
|
+
shell.setPanes(() => panes);
|
|
3715
|
+
bindToolbar();
|
|
3716
|
+
Q(/* @__PURE__ */ a2(PaneTabs, { multi: controls, panes: () => panes }), tabsHost);
|
|
3717
|
+
renderGrid();
|
|
3718
|
+
const resize = new ResizeObserver(() => {
|
|
3719
|
+
const narrow = shell.root.clientWidth > 0 && shell.root.clientWidth < narrowWidth;
|
|
3720
|
+
if (narrow === store.get().narrow) return;
|
|
3721
|
+
store.set({ narrow });
|
|
3722
|
+
renderGrid();
|
|
3723
|
+
});
|
|
3724
|
+
resize.observe(shell.root);
|
|
3725
|
+
const unbindKeys = bindWidgetKeys(
|
|
3726
|
+
shell,
|
|
3727
|
+
() => panes[store.get().active] ?? null,
|
|
3728
|
+
searchHotkey,
|
|
3729
|
+
(e3) => {
|
|
3730
|
+
if (!e3.altKey || e3.ctrlKey || e3.metaKey) return false;
|
|
3731
|
+
if (e3.key === "Enter") {
|
|
3732
|
+
toggleMaximize();
|
|
3733
|
+
return true;
|
|
3734
|
+
}
|
|
3735
|
+
const digit = /^Digit([1-8])$/.exec(e3.code)?.[1];
|
|
3736
|
+
if (!digit) return false;
|
|
3737
|
+
activate(Number(digit) - 1);
|
|
3738
|
+
return true;
|
|
3739
|
+
}
|
|
3740
|
+
);
|
|
3741
|
+
const save = () => {
|
|
3742
|
+
const s3 = store.get();
|
|
3743
|
+
return { version: 1, layout: s3.layout, sync: { ...s3.sync }, active: s3.active, sizes: { ...sizes }, panes: panes.map((p3) => p3.chart.save()) };
|
|
3744
|
+
};
|
|
3745
|
+
return {
|
|
3746
|
+
slots: shell.slots,
|
|
3747
|
+
get charts() {
|
|
3748
|
+
return visible().map((p3) => p3.chart);
|
|
2997
3749
|
},
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3750
|
+
get activeChart() {
|
|
3751
|
+
return panes[store.get().active].chart;
|
|
3752
|
+
},
|
|
3753
|
+
get activeIndex() {
|
|
3754
|
+
return store.get().active;
|
|
3755
|
+
},
|
|
3756
|
+
get layout() {
|
|
3757
|
+
return store.get().layout;
|
|
3758
|
+
},
|
|
3759
|
+
get sync() {
|
|
3760
|
+
return { ...store.get().sync };
|
|
3761
|
+
},
|
|
3762
|
+
setLayout,
|
|
3763
|
+
setSync,
|
|
3764
|
+
activate,
|
|
3765
|
+
toggleMaximize,
|
|
3766
|
+
setSymbol: (sym, res) => {
|
|
3767
|
+
const pane = panes[store.get().active];
|
|
3768
|
+
return pane.load(sym, res ?? pane.store.get().resolution);
|
|
3769
|
+
},
|
|
3770
|
+
setResolution: (res) => {
|
|
3771
|
+
const pane = panes[store.get().active];
|
|
3772
|
+
const current = pane.store.get().symbol;
|
|
3773
|
+
return current ? pane.load(current.symbol, res) : Promise.resolve();
|
|
3774
|
+
},
|
|
3775
|
+
setLocale: (locale) => {
|
|
3776
|
+
for (const p3 of panes) {
|
|
3777
|
+
p3.chart.setLocale(locale);
|
|
3778
|
+
p3.store.set({ locale });
|
|
3779
|
+
}
|
|
3780
|
+
},
|
|
3781
|
+
setTheme: shell.setTheme,
|
|
3782
|
+
setColorScheme: shell.setColorScheme,
|
|
3783
|
+
save,
|
|
3784
|
+
load: async (input) => {
|
|
3785
|
+
const raw = input;
|
|
3786
|
+
if (!raw || typeof raw !== "object" || raw.version !== 1 || !Array.isArray(raw.panes) || raw.panes.length === 0) {
|
|
3787
|
+
throw new Error("funnest-charts: unsupported multi-chart state (expected version 1)");
|
|
3788
|
+
}
|
|
3789
|
+
const states = raw.panes.slice(0, MAX_PANES).map((p3) => parseChartState(p3));
|
|
3790
|
+
sizes = raw.sizes && typeof raw.sizes === "object" ? { ...raw.sizes } : {};
|
|
3791
|
+
const layout = isLayoutId(raw.layout) ? raw.layout : "1";
|
|
3792
|
+
ensurePanes(Math.max(states.length, paneCount(LAYOUTS[layout])));
|
|
3793
|
+
store.set({ sync: { ...DEFAULT_SYNC, ...raw.sync } });
|
|
3794
|
+
setLayout(layout);
|
|
3795
|
+
activate(Math.min(Math.max(0, Number(raw.active) || 0), visibleCount() - 1));
|
|
3796
|
+
await Promise.all(states.map((state, i3) => panes[i3].chart.load(state)));
|
|
3797
|
+
},
|
|
3798
|
+
on: (event, listener) => {
|
|
3799
|
+
let set = listeners.get(event);
|
|
3800
|
+
if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
|
|
3801
|
+
set.add(listener);
|
|
3802
|
+
return () => set.delete(listener);
|
|
3001
3803
|
},
|
|
3002
3804
|
destroy: () => {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
if (drawbarHost) Q(null, drawbarHost);
|
|
3012
|
-
chart.destroy();
|
|
3013
|
-
root.remove();
|
|
3805
|
+
unbindKeys();
|
|
3806
|
+
resize.disconnect();
|
|
3807
|
+
Q(null, shell.toolbarHost);
|
|
3808
|
+
if (shell.drawbarHost) Q(null, shell.drawbarHost);
|
|
3809
|
+
Q(null, tabsHost);
|
|
3810
|
+
for (const p3 of panes) p3.destroy();
|
|
3811
|
+
listeners.clear();
|
|
3812
|
+
shell.destroy();
|
|
3014
3813
|
}
|
|
3015
3814
|
};
|
|
3016
3815
|
}
|
|
@@ -3018,6 +3817,11 @@ export {
|
|
|
3018
3817
|
DEFAULT_FAVORITE_INTERVALS,
|
|
3019
3818
|
DEFAULT_INTERVAL_GROUPS,
|
|
3020
3819
|
DEFAULT_SEARCH_CATEGORIES,
|
|
3820
|
+
LAYOUTS,
|
|
3821
|
+
LAYOUT_GROUPS,
|
|
3822
|
+
MAX_PANES,
|
|
3823
|
+
createMultiChart,
|
|
3021
3824
|
createTradingChart,
|
|
3825
|
+
paneCount,
|
|
3022
3826
|
resolutionLabel
|
|
3023
3827
|
};
|