@funnest/charts-ui 0.6.24 → 0.6.25

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.
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Nudges an open dropdown horizontally so it stays inside the widget and the viewport (its CSS
3
- * anchor may push it off-screen on phones; hosts often clip the widget with overflow: hidden).
2
+ * Keeps an open dropdown inside the widget and the viewport: nudged sideways (its CSS anchor may
3
+ * push it off-screen on phones), and no taller than the room below its top — scrolling inside
4
+ * instead (a short chart on a phone; hosts often clip the widget or put their own bars under it).
4
5
  * Attach the returned ref to the menu element.
5
6
  */
6
7
  export declare function useKeepInView<T extends HTMLElement>(open: boolean): import("preact").RefObject<T | null>;
@@ -3,7 +3,7 @@ import type { WidgetContext } from '../context';
3
3
  /**
4
4
  * One "도구" button for the chart's less frequent tools, so the toolbar keeps symbol, interval,
5
5
  * market info and 지표 in view: chart type, split layout (+ sync), compare, replay, price axis,
6
- * screenshot and chart settings.
6
+ * screenshot, the app's own rows (toolsMenuItems) and chart settings.
7
7
  */
8
8
  export declare function ToolsMenu({ ctx }: {
9
9
  ctx: WidgetContext;
package/dist/context.d.ts CHANGED
@@ -55,6 +55,23 @@ export type ContextMenuItem = {
55
55
  export type ContextMenuItems = (target: ChartContextMenu & {
56
56
  chart: Chart;
57
57
  }) => ContextMenuItem[];
58
+ /**
59
+ * A row an app adds to the 도구 (tools) menu, above 차트 설정 — e.g. its own display settings.
60
+ * `icon` is SVG markup (16×16, stroke="currentColor"), from the app itself.
61
+ */
62
+ export interface ToolsMenuItem {
63
+ label: string;
64
+ icon?: string;
65
+ disabled?: boolean;
66
+ onSelect(chart: Chart): void;
67
+ }
68
+ /** The app's rows for the 도구 menu, asked each time it opens (for the pane's chart). */
69
+ export type ToolsMenuItems = (chart: Chart) => ToolsMenuItem[];
70
+ /**
71
+ * The legend's OHLC row: 'always', or 'crosshair' — only while the crosshair is on the chart (a
72
+ * phone's touch), shown over the chart then, so the candles don't move when it appears.
73
+ */
74
+ export type LegendValues = 'always' | 'crosshair';
58
75
  export interface IntervalGroup {
59
76
  id: 'minutes' | 'hours' | 'days';
60
77
  intervals: Resolution[];
@@ -96,6 +113,8 @@ export interface WidgetContext {
96
113
  /** Show the Funnest logo badge on the chart. */
97
114
  attribution: boolean;
98
115
  contextMenuItems: ContextMenuItems | undefined;
116
+ toolsMenuItems: ToolsMenuItems | undefined;
117
+ legendValues: LegendValues;
99
118
  /** The app's own indicator browser for the 지표 button (instead of the built-in picker). */
100
119
  onIndicatorsClick: ((chart: Chart) => void) | undefined;
101
120
  slots: {
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 { ContextMenuItem, ContextMenuItems, IntervalGroup, LayoutSync, SearchCategory, SearchExchange } from './context';
11
+ export type { ContextMenuItem, ContextMenuItems, IntervalGroup, LayoutSync, LegendValues, SearchCategory, SearchExchange, ToolsMenuItem, ToolsMenuItems } from './context';
12
12
  export type { ColorScheme, TradingChart, TradingChartOptions, TradingChartSlots } from './trading-chart';
13
13
  export { resolutionLabel, STRINGS, stringsOf, type Strings } from './i18n';
package/dist/index.js CHANGED
@@ -1002,6 +1002,21 @@ function DrawingToolbar({ ctx }) {
1002
1002
  const s3 = useStore(ctx.store);
1003
1003
  const t3 = stringsOf(s3);
1004
1004
  const [openGroup, setOpenGroup] = d2(null);
1005
+ const barRef = T2(null);
1006
+ F2(() => {
1007
+ const el = barRef.current;
1008
+ if (!el) return;
1009
+ const fit = () => {
1010
+ el.removeAttribute("data-fit");
1011
+ if (el.scrollHeight <= el.clientHeight) return;
1012
+ el.dataset.fit = "compact";
1013
+ if (el.scrollHeight > el.clientHeight) el.dataset.fit = "scroll";
1014
+ };
1015
+ fit();
1016
+ const ro = new ResizeObserver(fit);
1017
+ ro.observe(el);
1018
+ return () => ro.disconnect();
1019
+ }, []);
1005
1020
  const groups = toolGroups(ctx.chart.drawingDefinitions());
1006
1021
  const pick = (tool) => {
1007
1022
  ctx.chart.setDrawingTool(tool);
@@ -1012,6 +1027,7 @@ function DrawingToolbar({ ctx }) {
1012
1027
  /* @__PURE__ */ a3(
1013
1028
  "div",
1014
1029
  {
1030
+ ref: barRef,
1015
1031
  class: "fnc-drawbar",
1016
1032
  role: "toolbar",
1017
1033
  "aria-label": t3.drawingTools,
@@ -1672,8 +1688,14 @@ function useKeepInView(open) {
1672
1688
  el.style.translate = "";
1673
1689
  const fit = () => {
1674
1690
  el.style.translate = "";
1691
+ el.style.maxHeight = "";
1675
1692
  const r3 = el.getBoundingClientRect();
1676
1693
  const root = el.closest(".fnc-root")?.getBoundingClientRect();
1694
+ const maxY = Math.min(document.documentElement.clientHeight, root?.bottom ?? Infinity) - MARGIN;
1695
+ if (r3.bottom > maxY) {
1696
+ el.style.maxHeight = `${Math.max(120, Math.floor(maxY - r3.top))}px`;
1697
+ el.style.overflowY = "auto";
1698
+ }
1677
1699
  const minX = Math.max(0, root?.left ?? 0) + MARGIN;
1678
1700
  const maxX = Math.min(document.documentElement.clientWidth, root?.right ?? Infinity) - MARGIN;
1679
1701
  let dx = 0;
@@ -2395,6 +2417,16 @@ function ToolsMenu({ ctx }) {
2395
2417
  /* @__PURE__ */ a3("div", { class: "fnc-tools-section", children: t3.settingsScale }),
2396
2418
  /* @__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] }) })) }),
2397
2419
  /* @__PURE__ */ a3("div", { class: "fnc-tools-sep" }),
2420
+ (ctx.toolsMenuItems?.(ctx.chart) ?? []).map(
2421
+ (extra) => item(
2422
+ "fnc-tools-app",
2423
+ extra.label,
2424
+ // The app's own SVG markup (it's the app's string, not user input).
2425
+ /* @__PURE__ */ a3("span", { class: "fnc-tools-icon", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: extra.icon ?? "" } }),
2426
+ () => extra.onSelect(ctx.chart),
2427
+ { disabled: extra.disabled ?? false }
2428
+ )
2429
+ ),
2398
2430
  item("fnc-btn-settings", t3.chartSettingsMore, /* @__PURE__ */ a3(SettingsIcon, {}), () => ctx.store.set({ dialog: { kind: "chartSettings" } }))
2399
2431
  ] })
2400
2432
  ] });
@@ -2679,7 +2711,10 @@ function Legend({ ctx }) {
2679
2711
  A2(() => {
2680
2712
  const el = ref.current;
2681
2713
  if (!el) return;
2682
- const update = () => ctx.chart.setTopInset(el.offsetTop + el.offsetHeight + 4);
2714
+ const update = () => {
2715
+ const floating = ctx.legendValues === "crosshair" ? el.querySelector(".fnc-legend-values") : null;
2716
+ ctx.chart.setTopInset(el.offsetTop + el.offsetHeight - (floating ? floating.offsetHeight + 2 : 0) + 4);
2717
+ };
2683
2718
  const ro = new ResizeObserver(update);
2684
2719
  ro.observe(el);
2685
2720
  return () => ro.disconnect();
@@ -2690,12 +2725,12 @@ function Legend({ ctx }) {
2690
2725
  const prev = hover ? hover.prev : bars[bars.length - 2];
2691
2726
  const fmt = (p3) => formatPrice3(p3, s3.symbol.pricePrecision);
2692
2727
  let values = null;
2693
- if (bar) {
2728
+ if (bar && (ctx.legendValues === "always" || hover)) {
2694
2729
  const cls = dirClass(bar.close - bar.open);
2695
2730
  const change = prev ? bar.close - prev.close : 0;
2696
2731
  const pct = prev && prev.close ? change / prev.close * 100 : 0;
2697
2732
  const sign = change > 0 ? "+" : "";
2698
- values = /* @__PURE__ */ a3("div", { class: "fnc-legend-values", children: [
2733
+ values = /* @__PURE__ */ a3("div", { class: ctx.legendValues === "crosshair" ? "fnc-legend-values fnc-legend-values-float" : "fnc-legend-values", children: [
2699
2734
  /* @__PURE__ */ a3("span", { children: [
2700
2735
  t3.open,
2701
2736
  " ",
@@ -3102,7 +3137,13 @@ var CSS = (
3102
3137
  border-right: 1px solid var(--fnc-border);
3103
3138
  background: var(--fnc-bg);
3104
3139
  }
3105
- .fnc-hsep { width: 20px; height: 1px; background: var(--fnc-border); margin: 4px 0; }
3140
+ .fnc-hsep { width: 20px; height: 1px; background: var(--fnc-border); margin: 4px 0; flex: none; }
3141
+ /* Too short for every tool (DrawingToolbar sets data-fit): closer buttons, then scrolling. */
3142
+ .fnc-drawbar[data-fit] { gap: 2px; padding: 4px 0; }
3143
+ .fnc-drawbar[data-fit] .fnc-btn { width: 32px; height: 32px; }
3144
+ .fnc-drawbar[data-fit] .fnc-hsep { margin: 2px 0; }
3145
+ .fnc-drawbar[data-fit='scroll'] { overflow-y: auto; scrollbar-width: none; }
3146
+ .fnc-drawbar[data-fit='scroll']::-webkit-scrollbar { display: none; }
3106
3147
 
3107
3148
  /* Selected drawing: floating color/delete bar */
3108
3149
  .fnc-selbar {
@@ -3160,6 +3201,14 @@ var CSS = (
3160
3201
  .fnc-legend-title b { color: var(--fnc-text); font-weight: 700; }
3161
3202
  .fnc-legend-values { display: flex; flex-wrap: wrap; gap: 0 10px; }
3162
3203
  .fnc-legend-values b { font-weight: 600; }
3204
+ /* legendValues: 'crosshair' \u2014 over the chart while it shows, so it gets a backing. */
3205
+ .fnc-legend-values-float {
3206
+ width: fit-content;
3207
+ margin-left: -4px;
3208
+ padding: 0 4px;
3209
+ border-radius: 6px;
3210
+ background: color-mix(in srgb, var(--fnc-bg) 88%, transparent);
3211
+ }
3163
3212
  /* Indicator legend rows */
3164
3213
  .fnc-ind {
3165
3214
  display: flex;
@@ -3854,6 +3903,8 @@ var CSS = (
3854
3903
  .fnc-tools-seg svg { width: 16px; height: 16px; flex: none; }
3855
3904
  .fnc-tools-layouts { display: flex; flex-direction: column; gap: 2px; padding: 0 2px 4px; }
3856
3905
  .fnc-tools-sep { height: 1px; margin: 6px 4px; background: var(--fnc-border); }
3906
+ .fnc-tools-icon { display: inline-flex; width: 20px; height: 20px; flex: none; }
3907
+ .fnc-tools-icon svg { width: 100%; height: 100%; }
3857
3908
  .fnc-tools-item svg { flex: none; color: var(--fnc-text-2); }
3858
3909
  .fnc-tools-item[aria-pressed='true'] { background: var(--fnc-surface); color: var(--fnc-text); }
3859
3910
 
@@ -4370,6 +4421,8 @@ function createPane(shell, config) {
4370
4421
  drawingToolbar: config.drawingToolbar,
4371
4422
  attribution: config.attribution,
4372
4423
  contextMenuItems: config.contextMenuItems,
4424
+ toolsMenuItems: config.toolsMenuItems,
4425
+ legendValues: config.legendValues ?? "always",
4373
4426
  onIndicatorsClick: config.onIndicatorsClick,
4374
4427
  slots: shell.slots,
4375
4428
  multi: config.multi,
@@ -4448,6 +4501,8 @@ function createTradingChart(container, options) {
4448
4501
  attribution = true,
4449
4502
  colorScheme = "system",
4450
4503
  contextMenuItems,
4504
+ toolsMenuItems,
4505
+ legendValues,
4451
4506
  onIndicatorsClick,
4452
4507
  theme,
4453
4508
  strings,
@@ -4480,6 +4535,8 @@ function createTradingChart(container, options) {
4480
4535
  drawingToolbar,
4481
4536
  attribution,
4482
4537
  contextMenuItems,
4538
+ toolsMenuItems,
4539
+ legendValues,
4483
4540
  onIndicatorsClick,
4484
4541
  strings
4485
4542
  });
@@ -4555,6 +4612,8 @@ function createMultiChart(container, options) {
4555
4612
  attribution = true,
4556
4613
  colorScheme = "system",
4557
4614
  contextMenuItems,
4615
+ toolsMenuItems,
4616
+ legendValues,
4558
4617
  onIndicatorsClick,
4559
4618
  theme,
4560
4619
  strings,
@@ -4629,6 +4688,8 @@ function createMultiChart(container, options) {
4629
4688
  // One badge for the whole widget, not one per pane.
4630
4689
  attribution: attribution && index === 0,
4631
4690
  contextMenuItems,
4691
+ toolsMenuItems,
4692
+ legendValues,
4632
4693
  onIndicatorsClick,
4633
4694
  multi: controls
4634
4695
  });
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 { ContextMenuItems, IntervalGroup, MultiControls, SearchCategory, SearchExchange, UiState, WidgetContext } from './context';
3
+ import type { ContextMenuItems, IntervalGroup, LegendValues, MultiControls, SearchCategory, SearchExchange, ToolsMenuItems, UiState, WidgetContext } from './context';
4
4
  import { type Shell } from './shell';
5
5
  import type { Strings } from './i18n';
6
6
  import { Store } from './store';
@@ -20,6 +20,8 @@ export interface PaneConfig {
20
20
  /** Ctrl/⌘+K opens this widget's symbol search (its footer shows the hint only then). */
21
21
  searchHotkey: boolean;
22
22
  contextMenuItems?: ContextMenuItems | undefined;
23
+ toolsMenuItems?: ToolsMenuItems | undefined;
24
+ legendValues?: LegendValues | undefined;
23
25
  onIndicatorsClick?: ((chart: Chart) => void) | undefined;
24
26
  multi?: MultiControls | undefined;
25
27
  strings?: Partial<Strings> | undefined;
@@ -1,6 +1,6 @@
1
1
  /** @jsxImportSource preact */
2
2
  import { type Chart, type ChartOptions, type ChartType, type Datafeed, type DirectionPreset, type Locale, type ChartState, type Resolution, type ThemeTokens } from '@funnest/charts';
3
- import type { ContextMenuItems, IntervalGroup, SearchCategory, SearchExchange } from './context';
3
+ import type { ContextMenuItems, IntervalGroup, LegendValues, SearchCategory, SearchExchange, ToolsMenuItems } from './context';
4
4
  import type { Strings } from './i18n';
5
5
  import { type ColorScheme, type TradingChartSlots } from './shell';
6
6
  export type { ColorScheme, TradingChartSlots } from './shell';
@@ -58,6 +58,16 @@ export interface TradingChartOptions extends Omit<ChartOptions, 'datafeed'> {
58
58
  * e.g. "이 가격에 알림 만들기". Called on every right-click with the price and time under the pointer.
59
59
  */
60
60
  contextMenuItems?: ContextMenuItems;
61
+ /**
62
+ * Extra rows for the 도구 (tools) menu, above 차트 설정 — e.g. the app's own display settings.
63
+ * Called each time the menu opens, with the pane's chart.
64
+ */
65
+ toolsMenuItems?: ToolsMenuItems;
66
+ /**
67
+ * The legend's OHLC row: 'always' (default), or 'crosshair' — only while the crosshair is on the
68
+ * chart, over the chart then (good for phones, where the legend takes up much of the screen).
69
+ */
70
+ legendValues?: LegendValues;
61
71
  /**
62
72
  * The toolbar's 지표 button opens this instead of the built-in indicator picker (an app's own
63
73
  * indicator browser). Called with the pane's chart; add indicators with chart.addIndicator().
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts-ui",
3
- "version": "0.6.24",
3
+ "version": "0.6.25",
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.24"
45
+ "@funnest/charts": "0.6.25"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"