@funnest/charts-ui 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
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,9 @@ export declare const STRINGS: {
47
47
  scaleLog: string;
48
48
  scalePercent: string;
49
49
  undo: string;
50
+ horizontalAt: string;
51
+ resetChart: string;
52
+ chartSettingsMore: string;
50
53
  redo: string;
51
54
  compare: string;
52
55
  compareSymbol: string;
@@ -173,6 +176,9 @@ export declare const STRINGS: {
173
176
  scaleLog: string;
174
177
  scalePercent: string;
175
178
  undo: string;
179
+ horizontalAt: string;
180
+ resetChart: string;
181
+ chartSettingsMore: string;
176
182
  redo: string;
177
183
  compare: string;
178
184
  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;
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,9 @@ var STRINGS = {
470
470
  scaleLog: "\uB85C\uADF8",
471
471
  scalePercent: "%",
472
472
  undo: "\uB418\uB3CC\uB9AC\uAE30",
473
+ horizontalAt: "{price}\uC5D0 \uC218\uD3C9\uC120 \uAE0B\uAE30",
474
+ resetChart: "\uCC28\uD2B8 \uCD08\uAE30\uD654",
475
+ chartSettingsMore: "\uCC28\uD2B8 \uC124\uC815\u2026",
473
476
  redo: "\uB2E4\uC2DC \uC2E4\uD589",
474
477
  compare: "\uBE44\uAD50",
475
478
  compareSymbol: "\uBE44\uAD50\uD560 \uC885\uBAA9 \uCD94\uAC00",
@@ -598,6 +601,9 @@ var STRINGS = {
598
601
  scaleLog: "Log",
599
602
  scalePercent: "%",
600
603
  undo: "Undo",
604
+ horizontalAt: "Horizontal line at {price}",
605
+ resetChart: "Reset chart",
606
+ chartSettingsMore: "Chart settings\u2026",
601
607
  redo: "Redo",
602
608
  compare: "Compare",
603
609
  compareSymbol: "Add symbol to compare",
@@ -815,14 +821,6 @@ var LinkIcon = () => /* @__PURE__ */ a3(Icon, { children: [
815
821
  ] });
816
822
  var MaximizeIcon = () => /* @__PURE__ */ a3(Icon, { size: 16, children: /* @__PURE__ */ a3("path", { d: "M11 4h5v5M9 16H4v-5M16 4l-5 5M4 16l5-5" }) });
817
823
  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
824
  var CompareIcon = () => /* @__PURE__ */ a3(Icon, { children: [
827
825
  /* @__PURE__ */ a3("path", { d: "M3 14l4-4 3 2 7-7" }),
828
826
  /* @__PURE__ */ a3("path", { d: "M3 9l4 2 3-3 7 5", opacity: "0.55" })
@@ -2043,7 +2041,6 @@ function Toolbar({ ctx }) {
2043
2041
  const s3 = useStore(ctx.store);
2044
2042
  const t3 = STRINGS[s3.locale];
2045
2043
  const canSearch = typeof ctx.datafeed.searchSymbols === "function";
2046
- const mod = /Mac|iPhone|iPad/.test(navigator.platform) ? "\u2318" : "Ctrl";
2047
2044
  return /* @__PURE__ */ a3("div", { class: "fnc-toolbar", role: "toolbar", children: [
2048
2045
  /* @__PURE__ */ a3(
2049
2046
  "button",
@@ -2117,8 +2114,6 @@ function Toolbar({ ctx }) {
2117
2114
  children: /* @__PURE__ */ a3(PencilIcon, {})
2118
2115
  }
2119
2116
  ),
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
2117
  /* @__PURE__ */ a3(Slot, { el: ctx.slots.toolbarEnd, fixed: true }),
2123
2118
  /* @__PURE__ */ a3(
2124
2119
  "button",
@@ -2151,7 +2146,88 @@ function Toolbar({ ctx }) {
2151
2146
  import { createChart } from "@funnest/charts";
2152
2147
 
2153
2148
  // src/components/overlay.tsx
2154
- import { formatCompact, formatPrice as formatPrice2 } from "@funnest/charts";
2149
+ import { formatCompact, formatPrice as formatPrice3 } from "@funnest/charts";
2150
+
2151
+ // src/components/context-menu.tsx
2152
+ import { formatPrice as formatPrice2 } from "@funnest/charts";
2153
+ var MARGIN2 = 8;
2154
+ function ContextMenu({ ctx }) {
2155
+ const s3 = useStore(ctx.store);
2156
+ const t3 = STRINGS[s3.locale];
2157
+ const [target, setTarget] = d2(null);
2158
+ const ref = T2(null);
2159
+ A2(() => ctx.chart.on("contextMenu", setTarget), [ctx.chart]);
2160
+ A2(() => {
2161
+ if (!target) return;
2162
+ const close = () => setTarget(null);
2163
+ const onDown = (e3) => !ref.current?.contains(e3.target) && close();
2164
+ const onKey = (e3) => e3.key === "Escape" && close();
2165
+ document.addEventListener("pointerdown", onDown, true);
2166
+ document.addEventListener("keydown", onKey);
2167
+ window.addEventListener("wheel", close, { passive: true });
2168
+ window.addEventListener("resize", close);
2169
+ return () => {
2170
+ document.removeEventListener("pointerdown", onDown, true);
2171
+ document.removeEventListener("keydown", onKey);
2172
+ window.removeEventListener("wheel", close);
2173
+ window.removeEventListener("resize", close);
2174
+ };
2175
+ }, [target]);
2176
+ F2(() => {
2177
+ const el = ref.current;
2178
+ const stage = el?.closest(".fnc-stage");
2179
+ if (!el || !target || !stage) return;
2180
+ const w3 = el.offsetWidth;
2181
+ const h3 = el.offsetHeight;
2182
+ const { width, height } = stage.getBoundingClientRect();
2183
+ const x2 = target.x + w3 + MARGIN2 > width ? Math.max(MARGIN2, target.x - w3) : target.x;
2184
+ const y3 = target.y + h3 + MARGIN2 > height ? Math.max(MARGIN2, target.y - h3) : target.y;
2185
+ el.style.transform = `translate(${Math.round(x2)}px, ${Math.round(y3)}px)`;
2186
+ }, [target]);
2187
+ if (!target) return null;
2188
+ const mac = /Mac|iPhone|iPad/.test(navigator.platform);
2189
+ const mod = mac ? "\u2318" : "Ctrl+";
2190
+ const price = target.price;
2191
+ const precision = s3.symbol?.pricePrecision ?? 2;
2192
+ const extra = ctx.contextMenuItems?.({ ...target, chart: ctx.chart }) ?? [];
2193
+ const items = [
2194
+ { label: t3.undo, shortcut: `${mod}Z`, disabled: !s3.canUndo, onSelect: () => ctx.chart.undo() },
2195
+ { label: t3.redo, shortcut: `${mod}${mac ? "\u21E7" : "Shift+"}Z`, disabled: !s3.canRedo, onSelect: () => ctx.chart.redo() },
2196
+ "separator",
2197
+ ...extra.length ? [...extra, "separator"] : []
2198
+ ];
2199
+ if (price !== null) {
2200
+ const time = target.time ?? ctx.chart.bars[ctx.chart.bars.length - 1]?.time ?? Date.now();
2201
+ items.push({
2202
+ label: t3.horizontalAt.replace("{price}", formatPrice2(price, precision)),
2203
+ onSelect: () => ctx.chart.addDrawing({ type: "horizontal", points: [{ time, price }] })
2204
+ });
2205
+ }
2206
+ items.push(
2207
+ { label: t3.resetChart, onSelect: () => ctx.chart.resetView() },
2208
+ { label: t3.chartSettingsMore, onSelect: () => ctx.store.set({ dialog: { kind: "chartSettings" } }) }
2209
+ );
2210
+ return /* @__PURE__ */ a3("div", { class: "fnc-menu fnc-ctx-menu", role: "menu", ref, onContextMenu: (e3) => e3.preventDefault(), children: items.map(
2211
+ (item) => item === "separator" ? /* @__PURE__ */ a3("div", { class: "fnc-ctx-sep", role: "separator" }) : /* @__PURE__ */ a3(
2212
+ "button",
2213
+ {
2214
+ class: "fnc-menu-item",
2215
+ role: "menuitem",
2216
+ disabled: item.disabled,
2217
+ onClick: () => {
2218
+ setTarget(null);
2219
+ item.onSelect();
2220
+ },
2221
+ children: [
2222
+ /* @__PURE__ */ a3("span", { children: item.label }),
2223
+ item.shortcut && /* @__PURE__ */ a3("kbd", { children: item.shortcut })
2224
+ ]
2225
+ }
2226
+ )
2227
+ ) });
2228
+ }
2229
+
2230
+ // src/components/overlay.tsx
2155
2231
  var ZOOM_STEP = 1.25;
2156
2232
  var CONTROL_OFFSET = 52;
2157
2233
  var dirClass = (v3) => v3 > 0 ? "fnc-up" : v3 < 0 ? "fnc-down" : "fnc-flat";
@@ -2243,7 +2319,7 @@ function Legend({ ctx }) {
2243
2319
  const bars = ctx.chart.bars;
2244
2320
  const bar = hover?.bar ?? bars[bars.length - 1];
2245
2321
  const prev = hover ? hover.prev : bars[bars.length - 2];
2246
- const fmt = (p3) => formatPrice2(p3, s3.symbol.pricePrecision);
2322
+ const fmt = (p3) => formatPrice3(p3, s3.symbol.pricePrecision);
2247
2323
  let values = null;
2248
2324
  if (bar) {
2249
2325
  const cls = dirClass(bar.close - bar.open);
@@ -2452,7 +2528,8 @@ function Overlay({ ctx }) {
2452
2528
  /* @__PURE__ */ a3(NavControls, { ctx }),
2453
2529
  /* @__PURE__ */ a3(ReplayBar, { ctx }),
2454
2530
  /* @__PURE__ */ a3(SelectionBar, { ctx }),
2455
- /* @__PURE__ */ a3(PaneTools, { ctx })
2531
+ /* @__PURE__ */ a3(PaneTools, { ctx }),
2532
+ /* @__PURE__ */ a3(ContextMenu, { ctx })
2456
2533
  ] });
2457
2534
  }
2458
2535
 
@@ -3255,7 +3332,6 @@ var CSS = (
3255
3332
  every toolbar button stays reachable. Ctrl/\u2318+Z still undoes when the buttons are hidden. \u2500\u2500 */
3256
3333
  .fnc-root { container: fnc / inline-size; }
3257
3334
  @container fnc (max-width: 1100px) {
3258
- .fnc-btn-undo { display: none; }
3259
3335
  .fnc-btn-label span { display: none; }
3260
3336
  .fnc-toolbar .fnc-btn-label { padding: 0 6px; }
3261
3337
  }
@@ -3369,6 +3445,13 @@ var CSS = (
3369
3445
  .fnc-switch[data-on='true'] { background: var(--fnc-text-2); }
3370
3446
  .fnc-switch[data-on='true']::after { transform: translateX(16px); }
3371
3447
 
3448
+ /* \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 */
3449
+ .fnc-ctx-menu { top: 0; left: 0; z-index: 40; min-width: 220px; pointer-events: auto; }
3450
+ .fnc-ctx-menu .fnc-menu-item { justify-content: space-between; gap: 24px; color: var(--fnc-text); }
3451
+ .fnc-ctx-menu .fnc-menu-item:disabled { color: var(--fnc-text-2); }
3452
+ .fnc-ctx-menu kbd { font: inherit; font-size: 12px; color: var(--fnc-text-2); }
3453
+ .fnc-ctx-sep { height: 1px; margin: 4px 6px; background: var(--fnc-border); }
3454
+
3372
3455
  /* \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
3456
  .fnc-tool-group { position: relative; display: flex; }
3374
3457
  .fnc-tool-group[data-open='true'] { z-index: 30; }
@@ -3441,7 +3524,7 @@ var CSS = (
3441
3524
 
3442
3525
  /* \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
3526
  @media (max-width: 640px) {
3444
- .fnc-btn-compare, .fnc-btn-replay, .fnc-btn-undo { display: none; }
3527
+ .fnc-btn-compare, .fnc-btn-replay { display: none; }
3445
3528
  .fnc-toolbar { padding: 0 4px; gap: 2px; }
3446
3529
  .fnc-toolbar .fnc-btn { width: 32px; height: 32px; }
3447
3530
  .fnc-toolbar .fnc-btn-wide, .fnc-toolbar .fnc-btn-label { width: auto; padding: 0 6px; }
@@ -3809,6 +3892,7 @@ function createPane(shell, config) {
3809
3892
  showLogo: config.symbolLogo,
3810
3893
  drawingToolbar: config.drawingToolbar,
3811
3894
  attribution: config.attribution,
3895
+ contextMenuItems: config.contextMenuItems,
3812
3896
  slots: shell.slots,
3813
3897
  multi: config.multi,
3814
3898
  setDrawbarCollapsed: shell.setDrawbarCollapsed,
@@ -3883,6 +3967,7 @@ function createTradingChart(container, options) {
3883
3967
  chartType = "candles",
3884
3968
  attribution = true,
3885
3969
  colorScheme = "system",
3970
+ contextMenuItems,
3886
3971
  theme,
3887
3972
  ...chartOptions
3888
3973
  } = options;
@@ -3905,7 +3990,8 @@ function createTradingChart(container, options) {
3905
3990
  searchCategories,
3906
3991
  symbolLogo,
3907
3992
  drawingToolbar,
3908
- attribution
3993
+ attribution,
3994
+ contextMenuItems
3909
3995
  });
3910
3996
  shell.main.insertBefore(pane.stage, shell.slots.bottom);
3911
3997
  shell.setPanes(() => [pane]);
@@ -3976,6 +4062,7 @@ function createMultiChart(container, options) {
3976
4062
  chartType = "candles",
3977
4063
  attribution = true,
3978
4064
  colorScheme = "system",
4065
+ contextMenuItems,
3979
4066
  theme,
3980
4067
  ...chartOptions
3981
4068
  } = options;
@@ -4041,6 +4128,7 @@ function createMultiChart(container, options) {
4041
4128
  drawingToolbar,
4042
4129
  // One badge for the whole widget, not one per pane.
4043
4130
  attribution: attribution && index === 0,
4131
+ contextMenuItems,
4044
4132
  multi: controls
4045
4133
  });
4046
4134
  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.1",
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.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"