@funnest/charts-ui 0.6.0 → 0.6.2

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.
@@ -0,0 +1,8 @@
1
+ import type { WidgetContext } from '../context';
2
+ /**
3
+ * Right-click menu on the chart (mouse only): undo/redo, the host's own items
4
+ * (`contextMenuItems`), a horizontal line at the clicked price, reset and settings.
5
+ */
6
+ export declare function ContextMenu({ ctx }: {
7
+ ctx: WidgetContext;
8
+ }): import("preact").JSX.Element | null;
@@ -1,11 +1,6 @@
1
- import type { MultiControls, WidgetContext } from '../context';
1
+ /** @jsxImportSource preact */
2
2
  import { type LayoutId } from '../layouts';
3
3
  /** Mini drawing of a split layout. */
4
4
  export declare function LayoutGlyph({ id }: {
5
5
  id: LayoutId;
6
6
  }): import("preact").JSX.Element;
7
- /** Toolbar buttons of the multi-chart widget: split layout picker and pane sync toggles. */
8
- export declare function LayoutMenu({ ctx, multi }: {
9
- ctx: WidgetContext;
10
- multi: MultiControls;
11
- }): import("preact").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import type { JSX } from 'preact';
2
+ import type { WidgetContext } from '../context';
3
+ /**
4
+ * One "도구" button for the chart's less frequent tools, so the toolbar keeps symbol, interval,
5
+ * market info and 지표 in view: chart type, split layout (+ sync), compare, replay, price axis,
6
+ * screenshot and chart settings.
7
+ */
8
+ export declare function ToolsMenu({ ctx }: {
9
+ ctx: WidgetContext;
10
+ }): JSX.Element;
package/dist/context.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Chart, ChartLayout, ChartType, CompareInfo, Datafeed, Drawing, DrawingType, IndicatorConfig, Locale, ReplayStatus, Resolution, SymbolInfo } from '@funnest/charts';
1
+ import type { Chart, ChartContextMenu, ChartLayout, ChartType, CompareInfo, Datafeed, Drawing, DrawingType, IndicatorConfig, Locale, ReplayStatus, Resolution, SymbolInfo } from '@funnest/charts';
2
2
  import type { LayoutId } from './layouts';
3
3
  import type { WidgetStorage } from './storage';
4
4
  import type { Store } from './store';
@@ -39,6 +39,17 @@ export interface UiState {
39
39
  compares: CompareInfo[];
40
40
  replay: ReplayStatus;
41
41
  }
42
+ /** A row in the chart's right-click menu, or a divider. */
43
+ export type ContextMenuItem = {
44
+ label: string;
45
+ shortcut?: string;
46
+ disabled?: boolean;
47
+ onSelect(): void;
48
+ } | 'separator';
49
+ /** Extra right-click items for a click on `chart` (e.g. "create an alert at this price"). */
50
+ export type ContextMenuItems = (target: ChartContextMenu & {
51
+ chart: Chart;
52
+ }) => ContextMenuItem[];
42
53
  export interface IntervalGroup {
43
54
  id: 'minutes' | 'hours' | 'days';
44
55
  intervals: Resolution[];
@@ -65,6 +76,7 @@ export interface WidgetContext {
65
76
  drawingToolbar: boolean;
66
77
  /** Show the Funnest logo badge on the chart. */
67
78
  attribution: boolean;
79
+ contextMenuItems: ContextMenuItems | undefined;
68
80
  slots: {
69
81
  toolbar: HTMLElement;
70
82
  toolbarEnd: HTMLElement;
package/dist/i18n.d.ts CHANGED
@@ -47,6 +47,10 @@ export declare const STRINGS: {
47
47
  scaleLog: string;
48
48
  scalePercent: string;
49
49
  undo: string;
50
+ tools: string;
51
+ horizontalAt: string;
52
+ resetChart: string;
53
+ chartSettingsMore: string;
50
54
  redo: string;
51
55
  compare: string;
52
56
  compareSymbol: string;
@@ -173,6 +177,10 @@ export declare const STRINGS: {
173
177
  scaleLog: string;
174
178
  scalePercent: string;
175
179
  undo: string;
180
+ tools: string;
181
+ horizontalAt: string;
182
+ resetChart: string;
183
+ chartSettingsMore: string;
176
184
  redo: string;
177
185
  compare: string;
178
186
  compareSymbol: string;
package/dist/icons.d.ts CHANGED
@@ -40,8 +40,6 @@ export declare const FunnestMark: () => import("preact").JSX.Element;
40
40
  export declare const LinkIcon: () => import("preact").JSX.Element;
41
41
  export declare const MaximizeIcon: () => import("preact").JSX.Element;
42
42
  export declare const RestoreIcon: () => import("preact").JSX.Element;
43
- export declare const UndoIcon: () => import("preact").JSX.Element;
44
- export declare const RedoIcon: () => import("preact").JSX.Element;
45
43
  /** Two price lines diverging (compare symbols). */
46
44
  export declare const CompareIcon: () => import("preact").JSX.Element;
47
45
  export declare const ReplayIcon: () => import("preact").JSX.Element;
@@ -54,3 +52,5 @@ export declare const SvgIcon: ({ markup }: {
54
52
  }) => import("preact").JSX.Element;
55
53
  /** Tiny corner triangle on drawing-bar buttons that hold more tools. */
56
54
  export declare const CornerIcon: () => import("preact").JSX.Element;
55
+ /** Sliders: the 도구 menu. */
56
+ export declare const ToolsIcon: () => import("preact").JSX.Element;
package/dist/index.d.ts CHANGED
@@ -8,6 +8,6 @@ export { createMultiChart } from './multi-chart';
8
8
  export type { MultiChart, MultiChartEvents, MultiChartOptions, MultiChartState, PaneSpec } from './multi-chart';
9
9
  export { LAYOUT_GROUPS, LAYOUTS, MAX_PANES, paneCount } from './layouts';
10
10
  export type { LayoutId, SplitNode, SplitSizes } from './layouts';
11
- export type { IntervalGroup, LayoutSync, SearchCategory } from './context';
11
+ export type { ContextMenuItem, ContextMenuItems, IntervalGroup, LayoutSync, SearchCategory } from './context';
12
12
  export type { ColorScheme, TradingChart, TradingChartOptions, TradingChartSlots } from './trading-chart';
13
13
  export { resolutionLabel } from './i18n';
package/dist/index.js CHANGED
@@ -470,6 +470,10 @@ var STRINGS = {
470
470
  scaleLog: "\uB85C\uADF8",
471
471
  scalePercent: "%",
472
472
  undo: "\uB418\uB3CC\uB9AC\uAE30",
473
+ tools: "\uB3C4\uAD6C",
474
+ horizontalAt: "{price}\uC5D0 \uC218\uD3C9\uC120 \uAE0B\uAE30",
475
+ resetChart: "\uCC28\uD2B8 \uCD08\uAE30\uD654",
476
+ chartSettingsMore: "\uCC28\uD2B8 \uC124\uC815\u2026",
473
477
  redo: "\uB2E4\uC2DC \uC2E4\uD589",
474
478
  compare: "\uBE44\uAD50",
475
479
  compareSymbol: "\uBE44\uAD50\uD560 \uC885\uBAA9 \uCD94\uAC00",
@@ -598,6 +602,10 @@ var STRINGS = {
598
602
  scaleLog: "Log",
599
603
  scalePercent: "%",
600
604
  undo: "Undo",
605
+ tools: "Tools",
606
+ horizontalAt: "Horizontal line at {price}",
607
+ resetChart: "Reset chart",
608
+ chartSettingsMore: "Chart settings\u2026",
601
609
  redo: "Redo",
602
610
  compare: "Compare",
603
611
  compareSymbol: "Add symbol to compare",
@@ -809,20 +817,8 @@ var FunnestMark = () => /* @__PURE__ */ a3("svg", { width: "20", height: "20", v
809
817
  /* @__PURE__ */ a3("path", { d: "M7 13.5C9.5 21.5 17 23.5 23.4 11.2", stroke: "#fff", "stroke-width": "3.2", "stroke-linecap": "round" }),
810
818
  /* @__PURE__ */ a3("circle", { cx: "24.6", cy: "8.6", r: "2.9", fill: "#FFD43B" })
811
819
  ] });
812
- var LinkIcon = () => /* @__PURE__ */ a3(Icon, { children: [
813
- /* @__PURE__ */ a3("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" }),
814
- /* @__PURE__ */ a3("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" })
815
- ] });
816
820
  var MaximizeIcon = () => /* @__PURE__ */ a3(Icon, { size: 16, children: /* @__PURE__ */ a3("path", { d: "M11 4h5v5M9 16H4v-5M16 4l-5 5M4 16l5-5" }) });
817
821
  var RestoreIcon = () => /* @__PURE__ */ a3(Icon, { size: 16, children: /* @__PURE__ */ a3("path", { d: "M15 9h-4V5M5 11h4v4M11 9l5-5M9 11l-5 5" }) });
818
- var UndoIcon = () => /* @__PURE__ */ a3(Icon, { size: 18, children: [
819
- /* @__PURE__ */ a3("path", { d: "M7 5L3.5 8.5 7 12" }),
820
- /* @__PURE__ */ a3("path", { d: "M3.5 8.5h8.5a4.5 4.5 0 0 1 0 9H9" })
821
- ] });
822
- var RedoIcon = () => /* @__PURE__ */ a3(Icon, { size: 18, children: [
823
- /* @__PURE__ */ a3("path", { d: "M13 5l3.5 3.5L13 12" }),
824
- /* @__PURE__ */ a3("path", { d: "M16.5 8.5H8a4.5 4.5 0 0 0 0 9h3" })
825
- ] });
826
822
  var CompareIcon = () => /* @__PURE__ */ a3(Icon, { children: [
827
823
  /* @__PURE__ */ a3("path", { d: "M3 14l4-4 3 2 7-7" }),
828
824
  /* @__PURE__ */ a3("path", { d: "M3 9l4 2 3-3 7 5", opacity: "0.55" })
@@ -853,6 +849,11 @@ var SvgIcon = ({ markup }) => markup.trim().startsWith("<svg") ? /* @__PURE__ */
853
849
  }
854
850
  );
855
851
  var CornerIcon = () => /* @__PURE__ */ a3("svg", { width: "6", height: "6", viewBox: "0 0 6 6", "aria-hidden": "true", children: /* @__PURE__ */ a3("path", { d: "M6 0v6H0z", fill: "currentColor" }) });
852
+ var ToolsIcon = () => /* @__PURE__ */ a3(Icon, { size: 18, children: [
853
+ /* @__PURE__ */ a3("path", { d: "M3.5 6h7M14.5 6h2M3.5 14h2M9.5 14h7" }),
854
+ /* @__PURE__ */ a3("circle", { cx: "12.5", cy: "6", r: "2" }),
855
+ /* @__PURE__ */ a3("circle", { cx: "7.5", cy: "14", r: "2" })
856
+ ] });
856
857
 
857
858
  // src/store.ts
858
859
  var Store = class {
@@ -1039,93 +1040,6 @@ function SelectionBar({ ctx }) {
1039
1040
  ] });
1040
1041
  }
1041
1042
 
1042
- // src/components/keep-in-view.ts
1043
- var MARGIN = 8;
1044
- function useKeepInView(open) {
1045
- const ref = T2(null);
1046
- F2(() => {
1047
- const el = ref.current;
1048
- if (!open || !el) return;
1049
- el.style.translate = "";
1050
- const fit = () => {
1051
- el.style.translate = "";
1052
- const r3 = el.getBoundingClientRect();
1053
- const root = el.closest(".fnc-root")?.getBoundingClientRect();
1054
- const minX = Math.max(0, root?.left ?? 0) + MARGIN;
1055
- const maxX = Math.min(document.documentElement.clientWidth, root?.right ?? Infinity) - MARGIN;
1056
- let dx = 0;
1057
- if (r3.right > maxX) dx = maxX - r3.right;
1058
- if (r3.left + dx < minX) dx = minX - r3.left;
1059
- if (dx) el.style.translate = `${Math.round(dx)}px 0`;
1060
- };
1061
- fit();
1062
- window.addEventListener("resize", fit);
1063
- return () => window.removeEventListener("resize", fit);
1064
- }, [open]);
1065
- return ref;
1066
- }
1067
-
1068
- // src/components/chart-type-menu.tsx
1069
- var TYPES = [
1070
- { type: "candles", label: "candles", icon: CandlesIcon },
1071
- { type: "line", label: "line", icon: LineChartIcon },
1072
- { type: "area", label: "area", icon: AreaChartIcon }
1073
- ];
1074
- function ChartTypeMenu({ ctx }) {
1075
- const s3 = useStore(ctx.store);
1076
- const t3 = STRINGS[s3.locale];
1077
- const [open, setOpen] = d2(false);
1078
- const ref = T2(null);
1079
- const menuRef = useKeepInView(open);
1080
- const current = TYPES.find((x2) => x2.type === s3.chartType) ?? TYPES[0];
1081
- A2(() => {
1082
- if (!open) return;
1083
- const onDown = (e3) => !ref.current?.contains(e3.target) && setOpen(false);
1084
- const onKey = (e3) => e3.key === "Escape" && setOpen(false);
1085
- document.addEventListener("pointerdown", onDown);
1086
- document.addEventListener("keydown", onKey);
1087
- return () => {
1088
- document.removeEventListener("pointerdown", onDown);
1089
- document.removeEventListener("keydown", onKey);
1090
- };
1091
- }, [open]);
1092
- const choose = (type) => {
1093
- ctx.chart.setChartType(type);
1094
- ctx.store.set({ chartType: type });
1095
- setOpen(false);
1096
- };
1097
- return /* @__PURE__ */ a3("div", { class: "fnc-menu-wrap fnc-chart-type", ref, children: [
1098
- /* @__PURE__ */ a3(
1099
- "button",
1100
- {
1101
- class: "fnc-btn fnc-btn-wide",
1102
- "data-tip": t3.chartType,
1103
- "aria-label": t3.chartType,
1104
- "aria-haspopup": "menu",
1105
- "aria-expanded": open,
1106
- onClick: () => setOpen(!open),
1107
- children: [
1108
- /* @__PURE__ */ a3(current.icon, {}),
1109
- /* @__PURE__ */ a3(ChevronDownIcon, {})
1110
- ]
1111
- }
1112
- ),
1113
- open && /* @__PURE__ */ a3("div", { class: "fnc-menu", role: "menu", ref: menuRef, children: TYPES.map(({ type, label, icon: Icon2 }) => /* @__PURE__ */ a3(
1114
- "button",
1115
- {
1116
- class: "fnc-menu-item",
1117
- role: "menuitemradio",
1118
- "aria-checked": type === s3.chartType,
1119
- onClick: () => choose(type),
1120
- children: [
1121
- /* @__PURE__ */ a3(Icon2, {}),
1122
- /* @__PURE__ */ a3("span", { children: t3[label] })
1123
- ]
1124
- }
1125
- )) })
1126
- ] });
1127
- }
1128
-
1129
1043
  // src/components/indicator-dialogs.tsx
1130
1044
  import {
1131
1045
  maLines,
@@ -1486,6 +1400,32 @@ function Dialogs({ ctx }) {
1486
1400
  return null;
1487
1401
  }
1488
1402
 
1403
+ // src/components/keep-in-view.ts
1404
+ var MARGIN = 8;
1405
+ function useKeepInView(open) {
1406
+ const ref = T2(null);
1407
+ F2(() => {
1408
+ const el = ref.current;
1409
+ if (!open || !el) return;
1410
+ el.style.translate = "";
1411
+ const fit = () => {
1412
+ el.style.translate = "";
1413
+ const r3 = el.getBoundingClientRect();
1414
+ const root = el.closest(".fnc-root")?.getBoundingClientRect();
1415
+ const minX = Math.max(0, root?.left ?? 0) + MARGIN;
1416
+ const maxX = Math.min(document.documentElement.clientWidth, root?.right ?? Infinity) - MARGIN;
1417
+ let dx = 0;
1418
+ if (r3.right > maxX) dx = maxX - r3.right;
1419
+ if (r3.left + dx < minX) dx = minX - r3.left;
1420
+ if (dx) el.style.translate = `${Math.round(dx)}px 0`;
1421
+ };
1422
+ fit();
1423
+ window.addEventListener("resize", fit);
1424
+ return () => window.removeEventListener("resize", fit);
1425
+ }, [open]);
1426
+ return ref;
1427
+ }
1428
+
1489
1429
  // src/components/interval-picker.tsx
1490
1430
  var GROUP_LABEL = {
1491
1431
  minutes: "intervalMinutes",
@@ -1579,181 +1519,6 @@ function IntervalPicker({ ctx }) {
1579
1519
  ] });
1580
1520
  }
1581
1521
 
1582
- // src/layouts.ts
1583
- var row = (...children) => ({ dir: "row", children });
1584
- var col = (...children) => ({ dir: "col", children });
1585
- var LAYOUTS = {
1586
- "1": 0,
1587
- "2-cols": row(0, 1),
1588
- "2-rows": col(0, 1),
1589
- "3-cols": row(0, 1, 2),
1590
- "3-rows": col(0, 1, 2),
1591
- "3-left": row(0, col(1, 2)),
1592
- "3-top": col(0, row(1, 2)),
1593
- "4-grid": col(row(0, 1), row(2, 3)),
1594
- "4-cols": row(0, 1, 2, 3),
1595
- "4-left": row(0, col(1, 2, 3)),
1596
- "6-grid": col(row(0, 1, 2), row(3, 4, 5)),
1597
- "8-grid": col(row(0, 1, 2, 3), row(4, 5, 6, 7))
1598
- };
1599
- var LAYOUT_GROUPS = [
1600
- ["1"],
1601
- ["2-cols", "2-rows"],
1602
- ["3-cols", "3-rows", "3-left", "3-top"],
1603
- ["4-grid", "4-cols", "4-left"],
1604
- ["6-grid", "8-grid"]
1605
- ];
1606
- var MAX_PANES = 8;
1607
- function isLayoutId(value) {
1608
- return typeof value === "string" && value in LAYOUTS;
1609
- }
1610
- function paneCount(node) {
1611
- if (typeof node === "number") return 1;
1612
- let n2 = 0;
1613
- for (const child of node.children) n2 += paneCount(child);
1614
- return n2;
1615
- }
1616
- function sizesFor(sizes, key, count) {
1617
- const s3 = sizes[key];
1618
- return s3 && s3.length === count && s3.every((v3) => Number.isFinite(v3) && v3 > 0) ? s3 : Array(count).fill(1);
1619
- }
1620
- function resizePair(weights, i3, deltaFraction, min = 0.12) {
1621
- const total = weights.reduce((a5, b2) => a5 + b2, 0);
1622
- const a4 = weights[i3];
1623
- const b = weights[i3 + 1];
1624
- const pair = a4 + b;
1625
- const delta = deltaFraction * total;
1626
- const nextA = Math.min(pair * (1 - min), Math.max(pair * min, a4 + delta));
1627
- const next = [...weights];
1628
- next[i3] = nextA;
1629
- next[i3 + 1] = pair - nextA;
1630
- return next;
1631
- }
1632
- function layoutRects(node, x2 = 0, y3 = 0, w3 = 20, h3 = 16, gap = 1.6) {
1633
- if (typeof node === "number") return [{ x: x2, y: y3, w: w3, h: h3 }];
1634
- const n2 = node.children.length;
1635
- const out = [];
1636
- node.children.forEach((child, i3) => {
1637
- if (node.dir === "row") {
1638
- const cw = (w3 - gap * (n2 - 1)) / n2;
1639
- out.push(...layoutRects(child, x2 + i3 * (cw + gap), y3, cw, h3, gap));
1640
- } else {
1641
- const ch = (h3 - gap * (n2 - 1)) / n2;
1642
- out.push(...layoutRects(child, x2, y3 + i3 * (ch + gap), w3, ch, gap));
1643
- }
1644
- });
1645
- return out;
1646
- }
1647
-
1648
- // src/components/layout-menu.tsx
1649
- var LAYOUT_PANES = (id) => paneCount(LAYOUTS[id]);
1650
- function LayoutGlyph({ id }) {
1651
- return /* @__PURE__ */ a3("svg", { width: "20", height: "16", viewBox: "0 0 20 16", "aria-hidden": "true", children: layoutRects(LAYOUTS[id]).map((r3) => /* @__PURE__ */ a3("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" })) });
1652
- }
1653
- function usePopover() {
1654
- const [open, setOpen] = d2(false);
1655
- const ref = T2(null);
1656
- A2(() => {
1657
- if (!open) return;
1658
- const onDown = (e3) => !ref.current?.contains(e3.target) && setOpen(false);
1659
- const onKey = (e3) => e3.key === "Escape" && setOpen(false);
1660
- document.addEventListener("pointerdown", onDown);
1661
- document.addEventListener("keydown", onKey);
1662
- return () => {
1663
- document.removeEventListener("pointerdown", onDown);
1664
- document.removeEventListener("keydown", onKey);
1665
- };
1666
- }, [open]);
1667
- return { open, setOpen, ref };
1668
- }
1669
- var SYNC_KEYS = [
1670
- { key: "crosshair", label: "syncCrosshair" },
1671
- { key: "time", label: "syncTime" },
1672
- { key: "symbol", label: "syncSymbol" },
1673
- { key: "interval", label: "syncInterval" }
1674
- ];
1675
- function LayoutMenu({ ctx, multi }) {
1676
- const s3 = useStore(ctx.store);
1677
- const m3 = useStore(multi.store);
1678
- const t3 = STRINGS[s3.locale];
1679
- const layout = usePopover();
1680
- const sync = usePopover();
1681
- const layoutMenuRef = useKeepInView(layout.open);
1682
- const syncMenuRef = useKeepInView(sync.open);
1683
- const anySync = Object.values(m3.sync).some(Boolean);
1684
- return /* @__PURE__ */ a3($, { children: [
1685
- /* @__PURE__ */ a3("div", { class: "fnc-menu-wrap", ref: layout.ref, children: [
1686
- /* @__PURE__ */ a3(
1687
- "button",
1688
- {
1689
- class: "fnc-btn fnc-btn-wide fnc-btn-layout",
1690
- "data-tip": t3.layout,
1691
- "aria-label": t3.layout,
1692
- "aria-haspopup": "menu",
1693
- "aria-expanded": layout.open,
1694
- onClick: () => layout.setOpen(!layout.open),
1695
- children: [
1696
- /* @__PURE__ */ a3(LayoutGlyph, { id: m3.layout }),
1697
- /* @__PURE__ */ a3(ChevronDownIcon, {})
1698
- ]
1699
- }
1700
- ),
1701
- layout.open && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-layout-menu", role: "menu", "aria-label": t3.layout, ref: layoutMenuRef, children: LAYOUT_GROUPS.map((group) => /* @__PURE__ */ a3("div", { class: "fnc-layout-row", children: [
1702
- /* @__PURE__ */ a3("span", { class: "fnc-layout-count", children: group[0].split("-")[0] }),
1703
- group.map((id) => /* @__PURE__ */ a3(
1704
- "button",
1705
- {
1706
- class: "fnc-layout-opt",
1707
- role: "menuitemradio",
1708
- "aria-checked": id === m3.layout,
1709
- "aria-label": `${t3.layout} ${id}`,
1710
- "data-layout": id,
1711
- onClick: () => {
1712
- multi.setLayout(id);
1713
- layout.setOpen(false);
1714
- },
1715
- children: /* @__PURE__ */ a3(LayoutGlyph, { id })
1716
- }
1717
- ))
1718
- ] })) })
1719
- ] }),
1720
- LAYOUT_PANES(m3.layout) > 1 && /* @__PURE__ */ a3("div", { class: "fnc-menu-wrap", ref: sync.ref, children: [
1721
- /* @__PURE__ */ a3(
1722
- "button",
1723
- {
1724
- class: "fnc-btn fnc-btn-sync",
1725
- "data-tip": t3.sync,
1726
- "aria-label": t3.sync,
1727
- "aria-haspopup": "menu",
1728
- "aria-expanded": sync.open,
1729
- "aria-pressed": anySync,
1730
- onClick: () => sync.setOpen(!sync.open),
1731
- children: /* @__PURE__ */ a3(LinkIcon, {})
1732
- }
1733
- ),
1734
- sync.open && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-sync-menu", role: "menu", "aria-label": t3.sync, ref: syncMenuRef, children: [
1735
- /* @__PURE__ */ a3("div", { class: "fnc-menu-title", children: [
1736
- t3.sync,
1737
- /* @__PURE__ */ a3("small", { children: t3.syncHint })
1738
- ] }),
1739
- SYNC_KEYS.map(({ key, label }) => /* @__PURE__ */ a3(
1740
- "button",
1741
- {
1742
- class: "fnc-menu-item fnc-sync-item",
1743
- role: "menuitemcheckbox",
1744
- "aria-checked": m3.sync[key],
1745
- onClick: () => multi.setSync({ [key]: !m3.sync[key] }),
1746
- children: [
1747
- /* @__PURE__ */ a3("span", { children: t3[label] }),
1748
- /* @__PURE__ */ a3("span", { class: "fnc-switch", "data-on": m3.sync[key] ? "true" : "false", "aria-hidden": "true" })
1749
- ]
1750
- }
1751
- ))
1752
- ] })
1753
- ] })
1754
- ] });
1755
- }
1756
-
1757
1522
  // src/components/slot.tsx
1758
1523
  function Slot({ el, fixed = false }) {
1759
1524
  const ref = T2(null);
@@ -2038,12 +1803,235 @@ function SymbolSearch({ ctx }) {
2038
1803
  ] }) });
2039
1804
  }
2040
1805
 
1806
+ // src/layouts.ts
1807
+ var row = (...children) => ({ dir: "row", children });
1808
+ var col = (...children) => ({ dir: "col", children });
1809
+ var LAYOUTS = {
1810
+ "1": 0,
1811
+ "2-cols": row(0, 1),
1812
+ "2-rows": col(0, 1),
1813
+ "3-cols": row(0, 1, 2),
1814
+ "3-rows": col(0, 1, 2),
1815
+ "3-left": row(0, col(1, 2)),
1816
+ "3-top": col(0, row(1, 2)),
1817
+ "4-grid": col(row(0, 1), row(2, 3)),
1818
+ "4-cols": row(0, 1, 2, 3),
1819
+ "4-left": row(0, col(1, 2, 3)),
1820
+ "6-grid": col(row(0, 1, 2), row(3, 4, 5)),
1821
+ "8-grid": col(row(0, 1, 2, 3), row(4, 5, 6, 7))
1822
+ };
1823
+ var LAYOUT_GROUPS = [
1824
+ ["1"],
1825
+ ["2-cols", "2-rows"],
1826
+ ["3-cols", "3-rows", "3-left", "3-top"],
1827
+ ["4-grid", "4-cols", "4-left"],
1828
+ ["6-grid", "8-grid"]
1829
+ ];
1830
+ var MAX_PANES = 8;
1831
+ function isLayoutId(value) {
1832
+ return typeof value === "string" && value in LAYOUTS;
1833
+ }
1834
+ function paneCount(node) {
1835
+ if (typeof node === "number") return 1;
1836
+ let n2 = 0;
1837
+ for (const child of node.children) n2 += paneCount(child);
1838
+ return n2;
1839
+ }
1840
+ function sizesFor(sizes, key, count) {
1841
+ const s3 = sizes[key];
1842
+ return s3 && s3.length === count && s3.every((v3) => Number.isFinite(v3) && v3 > 0) ? s3 : Array(count).fill(1);
1843
+ }
1844
+ function resizePair(weights, i3, deltaFraction, min = 0.12) {
1845
+ const total = weights.reduce((a5, b2) => a5 + b2, 0);
1846
+ const a4 = weights[i3];
1847
+ const b = weights[i3 + 1];
1848
+ const pair = a4 + b;
1849
+ const delta = deltaFraction * total;
1850
+ const nextA = Math.min(pair * (1 - min), Math.max(pair * min, a4 + delta));
1851
+ const next = [...weights];
1852
+ next[i3] = nextA;
1853
+ next[i3 + 1] = pair - nextA;
1854
+ return next;
1855
+ }
1856
+ function layoutRects(node, x2 = 0, y3 = 0, w3 = 20, h3 = 16, gap = 1.6) {
1857
+ if (typeof node === "number") return [{ x: x2, y: y3, w: w3, h: h3 }];
1858
+ const n2 = node.children.length;
1859
+ const out = [];
1860
+ node.children.forEach((child, i3) => {
1861
+ if (node.dir === "row") {
1862
+ const cw = (w3 - gap * (n2 - 1)) / n2;
1863
+ out.push(...layoutRects(child, x2 + i3 * (cw + gap), y3, cw, h3, gap));
1864
+ } else {
1865
+ const ch = (h3 - gap * (n2 - 1)) / n2;
1866
+ out.push(...layoutRects(child, x2, y3 + i3 * (ch + gap), w3, ch, gap));
1867
+ }
1868
+ });
1869
+ return out;
1870
+ }
1871
+
1872
+ // src/components/layout-menu.tsx
1873
+ function LayoutGlyph({ id }) {
1874
+ return /* @__PURE__ */ a3("svg", { width: "20", height: "16", viewBox: "0 0 20 16", "aria-hidden": "true", children: layoutRects(LAYOUTS[id]).map((r3) => /* @__PURE__ */ a3("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" })) });
1875
+ }
1876
+
1877
+ // src/components/tools-menu.tsx
1878
+ var TYPES = [
1879
+ { type: "candles", label: "candles", icon: CandlesIcon },
1880
+ { type: "line", label: "line", icon: LineChartIcon },
1881
+ { type: "area", label: "area", icon: AreaChartIcon }
1882
+ ];
1883
+ var SCALES2 = [
1884
+ { mode: "normal", label: "scaleNormal" },
1885
+ { mode: "log", label: "scaleLog" },
1886
+ { mode: "percent", label: "scalePercent" }
1887
+ ];
1888
+ var SYNC_KEYS = [
1889
+ { key: "crosshair", label: "syncCrosshair" },
1890
+ { key: "time", label: "syncTime" },
1891
+ { key: "symbol", label: "syncSymbol" },
1892
+ { key: "interval", label: "syncInterval" }
1893
+ ];
1894
+ function LayoutSection({ ctx, multi, close }) {
1895
+ const s3 = useStore(ctx.store);
1896
+ const m3 = useStore(multi.store);
1897
+ const t3 = STRINGS[s3.locale];
1898
+ return /* @__PURE__ */ a3($, { children: [
1899
+ /* @__PURE__ */ a3("div", { class: "fnc-tools-section", children: t3.layout }),
1900
+ /* @__PURE__ */ a3("div", { class: "fnc-tools-layouts", children: LAYOUT_GROUPS.map((group) => /* @__PURE__ */ a3("div", { class: "fnc-layout-row", children: [
1901
+ /* @__PURE__ */ a3("span", { class: "fnc-layout-count", children: group[0].split("-")[0] }),
1902
+ group.map((id) => /* @__PURE__ */ a3(
1903
+ "button",
1904
+ {
1905
+ class: "fnc-layout-opt",
1906
+ role: "menuitemradio",
1907
+ "aria-checked": id === m3.layout,
1908
+ "aria-label": `${t3.layout} ${id}`,
1909
+ "data-layout": id,
1910
+ onClick: () => {
1911
+ multi.setLayout(id);
1912
+ close();
1913
+ },
1914
+ children: /* @__PURE__ */ a3(LayoutGlyph, { id })
1915
+ }
1916
+ ))
1917
+ ] })) }),
1918
+ paneCount(LAYOUTS[m3.layout]) > 1 && /* @__PURE__ */ a3("div", { class: "fnc-tools-section", children: t3.sync }),
1919
+ paneCount(LAYOUTS[m3.layout]) > 1 && SYNC_KEYS.map(({ key, label }) => /* @__PURE__ */ a3(
1920
+ "button",
1921
+ {
1922
+ class: "fnc-menu-item fnc-sync-item",
1923
+ role: "menuitemcheckbox",
1924
+ "aria-checked": m3.sync[key],
1925
+ onClick: () => multi.setSync({ [key]: !m3.sync[key] }),
1926
+ children: [
1927
+ /* @__PURE__ */ a3("span", { children: t3[label] }),
1928
+ /* @__PURE__ */ a3("span", { class: "fnc-switch", "data-on": m3.sync[key] ? "true" : "false", "aria-hidden": "true" })
1929
+ ]
1930
+ }
1931
+ ))
1932
+ ] });
1933
+ }
1934
+ function ToolsMenu({ ctx }) {
1935
+ const s3 = useStore(ctx.store);
1936
+ const t3 = STRINGS[s3.locale];
1937
+ const [open, setOpen] = d2(false);
1938
+ const [scale, setScale] = d2(ctx.chart.settings.priceScale);
1939
+ const ref = T2(null);
1940
+ const menuRef = useKeepInView(open);
1941
+ const canSearch = typeof ctx.datafeed.searchSymbols === "function";
1942
+ const close = () => setOpen(false);
1943
+ A2(() => ctx.chart.on("settingsChange", (settings) => setScale(settings.priceScale)), [ctx.chart]);
1944
+ A2(() => {
1945
+ if (!open) return;
1946
+ const onDown = (e3) => !ref.current?.contains(e3.target) && setOpen(false);
1947
+ const onKey = (e3) => e3.key === "Escape" && setOpen(false);
1948
+ document.addEventListener("pointerdown", onDown);
1949
+ document.addEventListener("keydown", onKey);
1950
+ return () => {
1951
+ document.removeEventListener("pointerdown", onDown);
1952
+ document.removeEventListener("keydown", onKey);
1953
+ };
1954
+ }, [open]);
1955
+ const item = (cls, label, icon, onClick, opts = {}) => /* @__PURE__ */ a3(
1956
+ "button",
1957
+ {
1958
+ class: `fnc-menu-item fnc-tools-item ${cls}`,
1959
+ role: "menuitem",
1960
+ disabled: opts.disabled,
1961
+ "aria-pressed": opts.pressed,
1962
+ onClick: () => {
1963
+ close();
1964
+ onClick();
1965
+ },
1966
+ children: [
1967
+ icon,
1968
+ /* @__PURE__ */ a3("span", { children: label })
1969
+ ]
1970
+ }
1971
+ );
1972
+ return /* @__PURE__ */ a3("div", { class: "fnc-menu-wrap fnc-tools-wrap", ref, children: [
1973
+ /* @__PURE__ */ a3(
1974
+ "button",
1975
+ {
1976
+ class: "fnc-btn fnc-btn-wide fnc-btn-label fnc-btn-tools",
1977
+ "data-tip": t3.tools,
1978
+ "aria-label": t3.tools,
1979
+ "aria-haspopup": "menu",
1980
+ "aria-expanded": open,
1981
+ "aria-pressed": s3.replay.active || s3.replay.selecting || s3.compares.length > 0,
1982
+ onClick: () => setOpen(!open),
1983
+ children: [
1984
+ /* @__PURE__ */ a3(ToolsIcon, {}),
1985
+ /* @__PURE__ */ a3("span", { children: t3.tools }),
1986
+ /* @__PURE__ */ a3(ChevronDownIcon, {})
1987
+ ]
1988
+ }
1989
+ ),
1990
+ open && /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-tools-menu", role: "menu", "aria-label": t3.tools, ref: menuRef, children: [
1991
+ /* @__PURE__ */ a3("div", { class: "fnc-tools-section", children: t3.chartType }),
1992
+ /* @__PURE__ */ a3("div", { class: "fnc-seg fnc-tools-seg", role: "radiogroup", "aria-label": t3.chartType, children: TYPES.map(({ type, label, icon: Icon2 }) => /* @__PURE__ */ a3(
1993
+ "button",
1994
+ {
1995
+ type: "button",
1996
+ role: "radio",
1997
+ "aria-checked": s3.chartType === type,
1998
+ "data-type": type,
1999
+ onClick: () => {
2000
+ ctx.chart.setChartType(type);
2001
+ ctx.store.set({ chartType: type });
2002
+ },
2003
+ children: [
2004
+ /* @__PURE__ */ a3(Icon2, {}),
2005
+ /* @__PURE__ */ a3("span", { children: t3[label] })
2006
+ ]
2007
+ }
2008
+ )) }),
2009
+ ctx.multi && /* @__PURE__ */ a3(LayoutSection, { ctx, multi: ctx.multi, close }),
2010
+ /* @__PURE__ */ a3("div", { class: "fnc-tools-sep" }),
2011
+ canSearch && item("fnc-btn-compare", t3.compareSymbol, /* @__PURE__ */ a3(CompareIcon, {}), () => ctx.setSearchOpen(true, "compare"), {
2012
+ disabled: s3.compares.length >= 5
2013
+ }),
2014
+ item(
2015
+ "fnc-btn-replay",
2016
+ s3.replay.active ? t3.replayExit : t3.replay,
2017
+ /* @__PURE__ */ a3(ReplayIcon, {}),
2018
+ () => s3.replay.active ? ctx.chart.stopReplay() : ctx.chart.setReplaySelecting(true),
2019
+ { pressed: s3.replay.active || s3.replay.selecting }
2020
+ ),
2021
+ item("fnc-btn-screenshot", t3.screenshot, /* @__PURE__ */ a3(CameraIcon, {}), ctx.screenshot),
2022
+ /* @__PURE__ */ a3("div", { class: "fnc-tools-section", children: t3.settingsScale }),
2023
+ /* @__PURE__ */ a3("div", { class: "fnc-seg fnc-tools-seg", role: "radiogroup", "aria-label": t3.settingsScale, children: SCALES2.map(({ mode, label }) => /* @__PURE__ */ a3("button", { type: "button", role: "radio", "aria-checked": scale === mode, onClick: () => ctx.chart.setSettings({ priceScale: mode }), children: /* @__PURE__ */ a3("span", { children: t3[label] }) })) }),
2024
+ /* @__PURE__ */ a3("div", { class: "fnc-tools-sep" }),
2025
+ item("fnc-btn-settings", t3.chartSettingsMore, /* @__PURE__ */ a3(SettingsIcon, {}), () => ctx.store.set({ dialog: { kind: "chartSettings" } }))
2026
+ ] })
2027
+ ] });
2028
+ }
2029
+
2041
2030
  // src/components/toolbar.tsx
2042
2031
  function Toolbar({ ctx }) {
2043
2032
  const s3 = useStore(ctx.store);
2044
2033
  const t3 = STRINGS[s3.locale];
2045
2034
  const canSearch = typeof ctx.datafeed.searchSymbols === "function";
2046
- const mod = /Mac|iPhone|iPad/.test(navigator.platform) ? "\u2318" : "Ctrl";
2047
2035
  return /* @__PURE__ */ a3("div", { class: "fnc-toolbar", role: "toolbar", children: [
2048
2036
  /* @__PURE__ */ a3(
2049
2037
  "button",
@@ -2065,7 +2053,6 @@ function Toolbar({ ctx }) {
2065
2053
  /* @__PURE__ */ a3(Slot, { el: ctx.slots.toolbar }),
2066
2054
  /* @__PURE__ */ a3("div", { class: "fnc-spacer" }),
2067
2055
  /* @__PURE__ */ a3("div", { class: "fnc-tools", children: [
2068
- /* @__PURE__ */ a3(ChartTypeMenu, { ctx }),
2069
2056
  /* @__PURE__ */ a3(
2070
2057
  "button",
2071
2058
  {
@@ -2080,30 +2067,7 @@ function Toolbar({ ctx }) {
2080
2067
  ]
2081
2068
  }
2082
2069
  ),
2083
- canSearch && /* @__PURE__ */ a3(
2084
- "button",
2085
- {
2086
- class: "fnc-btn fnc-btn-compare",
2087
- "data-tip": t3.compareSymbol,
2088
- "aria-label": t3.compareSymbol,
2089
- "aria-haspopup": "dialog",
2090
- disabled: s3.compares.length >= 5,
2091
- onClick: () => ctx.setSearchOpen(true, "compare"),
2092
- children: /* @__PURE__ */ a3(CompareIcon, {})
2093
- }
2094
- ),
2095
- /* @__PURE__ */ a3(
2096
- "button",
2097
- {
2098
- class: "fnc-btn fnc-btn-replay",
2099
- "data-tip": s3.replay.selecting ? t3.replayPick : t3.replay,
2100
- "aria-label": t3.replay,
2101
- "aria-pressed": s3.replay.selecting || s3.replay.active,
2102
- onClick: () => s3.replay.active ? ctx.chart.stopReplay() : ctx.chart.setReplaySelecting(!s3.replay.selecting),
2103
- children: /* @__PURE__ */ a3(ReplayIcon, {})
2104
- }
2105
- ),
2106
- ctx.multi && /* @__PURE__ */ a3(LayoutMenu, { ctx, multi: ctx.multi })
2070
+ /* @__PURE__ */ a3(ToolsMenu, { ctx })
2107
2071
  ] }),
2108
2072
  /* @__PURE__ */ a3("div", { class: "fnc-sep" }),
2109
2073
  ctx.drawingToolbar && /* @__PURE__ */ a3(
@@ -2117,21 +2081,7 @@ function Toolbar({ ctx }) {
2117
2081
  children: /* @__PURE__ */ a3(PencilIcon, {})
2118
2082
  }
2119
2083
  ),
2120
- /* @__PURE__ */ a3("button", { class: "fnc-btn fnc-btn-undo", "data-tip": `${t3.undo} (${mod}+Z)`, "aria-label": t3.undo, disabled: !s3.canUndo, onClick: () => ctx.chart.undo(), children: /* @__PURE__ */ a3(UndoIcon, {}) }),
2121
- /* @__PURE__ */ a3("button", { class: "fnc-btn fnc-btn-undo", "data-tip": `${t3.redo} (${mod}+Shift+Z)`, "aria-label": t3.redo, disabled: !s3.canRedo, onClick: () => ctx.chart.redo(), children: /* @__PURE__ */ a3(RedoIcon, {}) }),
2122
2084
  /* @__PURE__ */ a3(Slot, { el: ctx.slots.toolbarEnd, fixed: true }),
2123
- /* @__PURE__ */ a3(
2124
- "button",
2125
- {
2126
- class: "fnc-btn fnc-btn-settings",
2127
- "data-tip": t3.chartSettings,
2128
- "aria-label": t3.chartSettings,
2129
- "aria-haspopup": "dialog",
2130
- onClick: () => ctx.store.set({ dialog: { kind: "chartSettings" } }),
2131
- children: /* @__PURE__ */ a3(SettingsIcon, {})
2132
- }
2133
- ),
2134
- /* @__PURE__ */ a3("button", { class: "fnc-btn fnc-btn-screenshot", "data-tip": t3.screenshot, "aria-label": t3.screenshot, onClick: ctx.screenshot, children: /* @__PURE__ */ a3(CameraIcon, {}) }),
2135
2085
  /* @__PURE__ */ a3(
2136
2086
  "button",
2137
2087
  {
@@ -2151,7 +2101,88 @@ function Toolbar({ ctx }) {
2151
2101
  import { createChart } from "@funnest/charts";
2152
2102
 
2153
2103
  // src/components/overlay.tsx
2154
- import { formatCompact, formatPrice as formatPrice2 } from "@funnest/charts";
2104
+ import { formatCompact, formatPrice as formatPrice3 } from "@funnest/charts";
2105
+
2106
+ // src/components/context-menu.tsx
2107
+ import { formatPrice as formatPrice2 } from "@funnest/charts";
2108
+ var MARGIN2 = 8;
2109
+ function ContextMenu({ ctx }) {
2110
+ const s3 = useStore(ctx.store);
2111
+ const t3 = STRINGS[s3.locale];
2112
+ const [target, setTarget] = d2(null);
2113
+ const ref = T2(null);
2114
+ A2(() => ctx.chart.on("contextMenu", setTarget), [ctx.chart]);
2115
+ A2(() => {
2116
+ if (!target) return;
2117
+ const close = () => setTarget(null);
2118
+ const onDown = (e3) => !ref.current?.contains(e3.target) && close();
2119
+ const onKey = (e3) => e3.key === "Escape" && close();
2120
+ document.addEventListener("pointerdown", onDown, true);
2121
+ document.addEventListener("keydown", onKey);
2122
+ window.addEventListener("wheel", close, { passive: true });
2123
+ window.addEventListener("resize", close);
2124
+ return () => {
2125
+ document.removeEventListener("pointerdown", onDown, true);
2126
+ document.removeEventListener("keydown", onKey);
2127
+ window.removeEventListener("wheel", close);
2128
+ window.removeEventListener("resize", close);
2129
+ };
2130
+ }, [target]);
2131
+ F2(() => {
2132
+ const el = ref.current;
2133
+ const stage = el?.closest(".fnc-stage");
2134
+ if (!el || !target || !stage) return;
2135
+ const w3 = el.offsetWidth;
2136
+ const h3 = el.offsetHeight;
2137
+ const { width, height } = stage.getBoundingClientRect();
2138
+ const x2 = target.x + w3 + MARGIN2 > width ? Math.max(MARGIN2, target.x - w3) : target.x;
2139
+ const y3 = target.y + h3 + MARGIN2 > height ? Math.max(MARGIN2, target.y - h3) : target.y;
2140
+ el.style.transform = `translate(${Math.round(x2)}px, ${Math.round(y3)}px)`;
2141
+ }, [target]);
2142
+ if (!target) return null;
2143
+ const mac = /Mac|iPhone|iPad/.test(navigator.platform);
2144
+ const mod = mac ? "\u2318" : "Ctrl+";
2145
+ const price = target.price;
2146
+ const precision = s3.symbol?.pricePrecision ?? 2;
2147
+ const extra = ctx.contextMenuItems?.({ ...target, chart: ctx.chart }) ?? [];
2148
+ const items = [
2149
+ { label: t3.undo, shortcut: `${mod}Z`, disabled: !s3.canUndo, onSelect: () => ctx.chart.undo() },
2150
+ { label: t3.redo, shortcut: `${mod}${mac ? "\u21E7" : "Shift+"}Z`, disabled: !s3.canRedo, onSelect: () => ctx.chart.redo() },
2151
+ "separator",
2152
+ ...extra.length ? [...extra, "separator"] : []
2153
+ ];
2154
+ if (price !== null) {
2155
+ const time = target.time ?? ctx.chart.bars[ctx.chart.bars.length - 1]?.time ?? Date.now();
2156
+ items.push({
2157
+ label: t3.horizontalAt.replace("{price}", formatPrice2(price, precision)),
2158
+ onSelect: () => ctx.chart.addDrawing({ type: "horizontal", points: [{ time, price }] })
2159
+ });
2160
+ }
2161
+ items.push(
2162
+ { label: t3.resetChart, onSelect: () => ctx.chart.resetView() },
2163
+ { label: t3.chartSettingsMore, onSelect: () => ctx.store.set({ dialog: { kind: "chartSettings" } }) }
2164
+ );
2165
+ return /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-ctx-menu", role: "menu", ref, onContextMenu: (e3) => e3.preventDefault(), children: items.map(
2166
+ (item) => item === "separator" ? /* @__PURE__ */ a3("div", { class: "fnc-ctx-sep", role: "separator" }) : /* @__PURE__ */ a3(
2167
+ "button",
2168
+ {
2169
+ class: "fnc-menu-item",
2170
+ role: "menuitem",
2171
+ disabled: item.disabled,
2172
+ onClick: () => {
2173
+ setTarget(null);
2174
+ item.onSelect();
2175
+ },
2176
+ children: [
2177
+ /* @__PURE__ */ a3("span", { children: item.label }),
2178
+ item.shortcut && /* @__PURE__ */ a3("kbd", { children: item.shortcut })
2179
+ ]
2180
+ }
2181
+ )
2182
+ ) });
2183
+ }
2184
+
2185
+ // src/components/overlay.tsx
2155
2186
  var ZOOM_STEP = 1.25;
2156
2187
  var CONTROL_OFFSET = 52;
2157
2188
  var dirClass = (v3) => v3 > 0 ? "fnc-up" : v3 < 0 ? "fnc-down" : "fnc-flat";
@@ -2243,7 +2274,7 @@ function Legend({ ctx }) {
2243
2274
  const bars = ctx.chart.bars;
2244
2275
  const bar = hover?.bar ?? bars[bars.length - 1];
2245
2276
  const prev = hover ? hover.prev : bars[bars.length - 2];
2246
- const fmt = (p3) => formatPrice2(p3, s3.symbol.pricePrecision);
2277
+ const fmt = (p3) => formatPrice3(p3, s3.symbol.pricePrecision);
2247
2278
  let values = null;
2248
2279
  if (bar) {
2249
2280
  const cls = dirClass(bar.close - bar.open);
@@ -2452,7 +2483,8 @@ function Overlay({ ctx }) {
2452
2483
  /* @__PURE__ */ a3(NavControls, { ctx }),
2453
2484
  /* @__PURE__ */ a3(ReplayBar, { ctx }),
2454
2485
  /* @__PURE__ */ a3(SelectionBar, { ctx }),
2455
- /* @__PURE__ */ a3(PaneTools, { ctx })
2486
+ /* @__PURE__ */ a3(PaneTools, { ctx }),
2487
+ /* @__PURE__ */ a3(ContextMenu, { ctx })
2456
2488
  ] });
2457
2489
  }
2458
2490
 
@@ -3255,14 +3287,12 @@ var CSS = (
3255
3287
  every toolbar button stays reachable. Ctrl/\u2318+Z still undoes when the buttons are hidden. \u2500\u2500 */
3256
3288
  .fnc-root { container: fnc / inline-size; }
3257
3289
  @container fnc (max-width: 1100px) {
3258
- .fnc-btn-undo { display: none; }
3259
3290
  .fnc-btn-label span { display: none; }
3260
3291
  .fnc-toolbar .fnc-btn-label { padding: 0 6px; }
3261
3292
  }
3262
3293
  @container fnc (max-width: 940px) {
3263
3294
  .fnc-intervals { display: none; }
3264
3295
  .fnc-interval-current { display: inline; }
3265
- .fnc-btn-screenshot { display: none; }
3266
3296
  }
3267
3297
 
3268
3298
  /* \u2500\u2500 Narrow phones (\u2264 400px): drop the menu chevrons and separators so every button fits \u2500\u2500 */
@@ -3369,6 +3399,24 @@ var CSS = (
3369
3399
  .fnc-switch[data-on='true'] { background: var(--fnc-text-2); }
3370
3400
  .fnc-switch[data-on='true']::after { transform: translateX(16px); }
3371
3401
 
3402
+ /* \u2500\u2500 Right-click menu \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 */
3403
+ .fnc-ctx-menu { top: 0; left: 0; z-index: 40; min-width: 220px; pointer-events: auto; }
3404
+ .fnc-ctx-menu .fnc-menu-item { justify-content: space-between; gap: 24px; color: var(--fnc-text); }
3405
+ .fnc-ctx-menu .fnc-menu-item:disabled { color: var(--fnc-text-2); }
3406
+ .fnc-ctx-menu kbd { font: inherit; font-size: 12px; color: var(--fnc-text-2); }
3407
+ .fnc-ctx-sep { height: 1px; margin: 4px 6px; background: var(--fnc-border); }
3408
+
3409
+ /* \u2500\u2500 \uB3C4\uAD6C menu: chart type, split, compare, replay, axis, screenshot, settings \u2500\u2500 */
3410
+ .fnc-tools-menu { width: 300px; padding: 8px; gap: 2px; max-height: 72vh; overflow-y: auto; }
3411
+ .fnc-tools-section { padding: 8px 6px 6px; font-size: 12px; font-weight: 700; color: var(--fnc-text-2); }
3412
+ .fnc-tools-seg { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; margin: 0 2px 4px; }
3413
+ .fnc-tools-seg button { display: inline-flex; align-items: center; justify-content: center; gap: 4px; min-width: 0; padding: 0 6px; }
3414
+ .fnc-tools-seg svg { width: 16px; height: 16px; flex: none; }
3415
+ .fnc-tools-layouts { display: flex; flex-direction: column; gap: 2px; padding: 0 2px 4px; }
3416
+ .fnc-tools-sep { height: 1px; margin: 6px 4px; background: var(--fnc-border); }
3417
+ .fnc-tools-item svg { flex: none; color: var(--fnc-text-2); }
3418
+ .fnc-tools-item[aria-pressed='true'] { background: var(--fnc-surface); color: var(--fnc-text); }
3419
+
3372
3420
  /* \u2500\u2500 Drawing tool groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
3373
3421
  .fnc-tool-group { position: relative; display: flex; }
3374
3422
  .fnc-tool-group[data-open='true'] { z-index: 30; }
@@ -3441,7 +3489,6 @@ var CSS = (
3441
3489
 
3442
3490
  /* \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 */
3443
3491
  @media (max-width: 640px) {
3444
- .fnc-btn-compare, .fnc-btn-replay, .fnc-btn-undo { display: none; }
3445
3492
  .fnc-toolbar { padding: 0 4px; gap: 2px; }
3446
3493
  .fnc-toolbar .fnc-btn { width: 32px; height: 32px; }
3447
3494
  .fnc-toolbar .fnc-btn-wide, .fnc-toolbar .fnc-btn-label { width: auto; padding: 0 6px; }
@@ -3450,9 +3497,7 @@ var CSS = (
3450
3497
  /* Logo only on phones; the name is in the chart legend right below. */
3451
3498
  .fnc-toolbar .fnc-symbol:has(.fnc-avatar) :is(.fnc-symbol-name, .fnc-icon) { display: none; }
3452
3499
  .fnc-chip { padding: 0 8px; }
3453
- .fnc-btn-screenshot { display: none; }
3454
3500
  .fnc-btn-label span { display: none; }
3455
- .fnc-btn-sync { display: none; }
3456
3501
  .fnc-intervals { display: none; }
3457
3502
  .fnc-interval-current { display: inline; }
3458
3503
  .fnc-interval-menu { left: auto; right: -40px; }
@@ -3809,6 +3854,7 @@ function createPane(shell, config) {
3809
3854
  showLogo: config.symbolLogo,
3810
3855
  drawingToolbar: config.drawingToolbar,
3811
3856
  attribution: config.attribution,
3857
+ contextMenuItems: config.contextMenuItems,
3812
3858
  slots: shell.slots,
3813
3859
  multi: config.multi,
3814
3860
  setDrawbarCollapsed: shell.setDrawbarCollapsed,
@@ -3883,6 +3929,7 @@ function createTradingChart(container, options) {
3883
3929
  chartType = "candles",
3884
3930
  attribution = true,
3885
3931
  colorScheme = "system",
3932
+ contextMenuItems,
3886
3933
  theme,
3887
3934
  ...chartOptions
3888
3935
  } = options;
@@ -3905,7 +3952,8 @@ function createTradingChart(container, options) {
3905
3952
  searchCategories,
3906
3953
  symbolLogo,
3907
3954
  drawingToolbar,
3908
- attribution
3955
+ attribution,
3956
+ contextMenuItems
3909
3957
  });
3910
3958
  shell.main.insertBefore(pane.stage, shell.slots.bottom);
3911
3959
  shell.setPanes(() => [pane]);
@@ -3976,6 +4024,7 @@ function createMultiChart(container, options) {
3976
4024
  chartType = "candles",
3977
4025
  attribution = true,
3978
4026
  colorScheme = "system",
4027
+ contextMenuItems,
3979
4028
  theme,
3980
4029
  ...chartOptions
3981
4030
  } = options;
@@ -4041,6 +4090,7 @@ function createMultiChart(container, options) {
4041
4090
  drawingToolbar,
4042
4091
  // One badge for the whole widget, not one per pane.
4043
4092
  attribution: attribution && index === 0,
4093
+ contextMenuItems,
4044
4094
  multi: controls
4045
4095
  });
4046
4096
  const cell = div("fnc-pane");
package/dist/pane.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** @jsxImportSource preact */
2
2
  import { type Chart, type ChartOptions, type ChartType, type Datafeed, type Resolution } from '@funnest/charts';
3
- import type { IntervalGroup, MultiControls, SearchCategory, UiState, WidgetContext } from './context';
3
+ import type { ContextMenuItems, IntervalGroup, MultiControls, SearchCategory, UiState, WidgetContext } from './context';
4
4
  import { type Shell } from './shell';
5
5
  import { Store } from './store';
6
6
  export interface PaneConfig {
@@ -14,6 +14,7 @@ export interface PaneConfig {
14
14
  symbolLogo: boolean;
15
15
  drawingToolbar: boolean;
16
16
  attribution: boolean;
17
+ contextMenuItems?: ContextMenuItems | undefined;
17
18
  multi?: MultiControls | undefined;
18
19
  }
19
20
  /**
@@ -1,6 +1,6 @@
1
1
  /** @jsxImportSource preact */
2
2
  import { type Chart, type ChartOptions, type ChartType, type Datafeed, type Locale, type ChartState, type Resolution, type ThemeTokens } from '@funnest/charts';
3
- import type { IntervalGroup, SearchCategory } from './context';
3
+ import type { ContextMenuItems, IntervalGroup, SearchCategory } from './context';
4
4
  import { type ColorScheme, type TradingChartSlots } from './shell';
5
5
  export type { ColorScheme, TradingChartSlots } from './shell';
6
6
  /** Interval menu: 분 | 시간 | 일. '12M' is shown as 1년. */
@@ -45,6 +45,11 @@ export interface TradingChartOptions extends Omit<ChartOptions, 'datafeed'> {
45
45
  * `theme` overrides are applied on top of whichever base is active.
46
46
  */
47
47
  colorScheme?: ColorScheme;
48
+ /**
49
+ * Extra rows for the chart's right-click menu (between undo/redo and the built-in actions),
50
+ * e.g. "이 가격에 알림 만들기". Called on every right-click with the price and time under the pointer.
51
+ */
52
+ contextMenuItems?: ContextMenuItems;
48
53
  }
49
54
  export interface TradingChart {
50
55
  /** The underlying engine, for anything the widget doesn't wrap. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts-ui",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Pro trading UI (toolbar, drawing tools, sheets) for Funnest Charts. Framework-agnostic.",
5
5
  "keywords": [
6
6
  "chart",
@@ -42,7 +42,7 @@
42
42
  "NOTICE"
43
43
  ],
44
44
  "dependencies": {
45
- "@funnest/charts": "0.6.0"
45
+ "@funnest/charts": "0.6.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"
@@ -1,5 +0,0 @@
1
- import type { JSX } from 'preact';
2
- import type { WidgetContext } from '../context';
3
- export declare function ChartTypeMenu({ ctx }: {
4
- ctx: WidgetContext;
5
- }): JSX.Element;