@funnest/charts-ui 0.6.23 → 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/i18n.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Locale, Resolution } from '@funnest/charts';
1
+ import type { Locale } from '@funnest/charts';
2
2
  export declare const STRINGS: {
3
3
  ko: {
4
4
  searchSymbol: string;
@@ -304,8 +304,4 @@ export declare function stringsOf(s: {
304
304
  locale: Locale;
305
305
  strings?: Partial<Strings> | undefined;
306
306
  }): Strings;
307
- /**
308
- * '1' → 1분, '60' → 1시간, '1D' → 일 (short) / 1일 (long), '12M' → 1년.
309
- * Short labels are for the toolbar chips, long ones for the interval menu.
310
- */
311
- export declare function resolutionLabel(resolution: Resolution, locale: Locale, form?: 'short' | 'long'): string;
307
+ export { resolutionLabel } from '@funnest/charts';
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
@@ -424,6 +424,7 @@ function G2(n2, t3) {
424
424
  }
425
425
 
426
426
  // src/i18n.ts
427
+ import { resolutionLabel } from "@funnest/charts";
427
428
  var STRINGS = {
428
429
  ko: {
429
430
  searchSymbol: "\uC885\uBAA9 \uAC80\uC0C9",
@@ -729,28 +730,6 @@ function stringsOf(s3) {
729
730
  if (!byLocale) merged.set(s3.strings, byLocale = {});
730
731
  return byLocale[s3.locale] ??= { ...STRINGS[s3.locale], ...s3.strings };
731
732
  }
732
- function resolutionLabel(resolution, locale, form = "short") {
733
- const match = /^(\d*)([SDWM]?)$/i.exec(resolution.trim());
734
- if (!match) return resolution;
735
- const n2 = match[1] ? Number(match[1]) : 1;
736
- const unit = (match[2] ?? "").toUpperCase();
737
- const ko = locale === "ko";
738
- const long = form === "long";
739
- switch (unit) {
740
- case "S":
741
- return ko ? `${n2}\uCD08` : `${n2}s`;
742
- case "D":
743
- return ko ? n2 === 1 && !long ? "\uC77C" : `${n2}\uC77C` : `${n2}D`;
744
- case "W":
745
- return ko ? n2 === 1 && !long ? "\uC8FC" : `${n2}\uC8FC` : `${n2}W`;
746
- case "M":
747
- if (n2 % 12 === 0) return ko ? `${n2 / 12}\uB144` : `${n2 / 12}Y`;
748
- return ko ? n2 === 1 && !long ? "\uC6D4" : `${n2}\uAC1C\uC6D4` : `${n2}M`;
749
- default:
750
- if (n2 >= 60 && n2 % 60 === 0) return ko ? `${n2 / 60}\uC2DC\uAC04` : `${n2 / 60}h`;
751
- return ko ? `${n2}\uBD84` : `${n2}m`;
752
- }
753
- }
754
733
 
755
734
  // ../../node_modules/.pnpm/preact@11.2.0/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs
756
735
  var f3 = /* @__PURE__ */ Symbol.for("pv");
@@ -1023,6 +1002,21 @@ function DrawingToolbar({ ctx }) {
1023
1002
  const s3 = useStore(ctx.store);
1024
1003
  const t3 = stringsOf(s3);
1025
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
+ }, []);
1026
1020
  const groups = toolGroups(ctx.chart.drawingDefinitions());
1027
1021
  const pick = (tool) => {
1028
1022
  ctx.chart.setDrawingTool(tool);
@@ -1033,6 +1027,7 @@ function DrawingToolbar({ ctx }) {
1033
1027
  /* @__PURE__ */ a3(
1034
1028
  "div",
1035
1029
  {
1030
+ ref: barRef,
1036
1031
  class: "fnc-drawbar",
1037
1032
  role: "toolbar",
1038
1033
  "aria-label": t3.drawingTools,
@@ -1693,8 +1688,14 @@ function useKeepInView(open) {
1693
1688
  el.style.translate = "";
1694
1689
  const fit = () => {
1695
1690
  el.style.translate = "";
1691
+ el.style.maxHeight = "";
1696
1692
  const r3 = el.getBoundingClientRect();
1697
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
+ }
1698
1699
  const minX = Math.max(0, root?.left ?? 0) + MARGIN;
1699
1700
  const maxX = Math.min(document.documentElement.clientWidth, root?.right ?? Infinity) - MARGIN;
1700
1701
  let dx = 0;
@@ -2416,6 +2417,16 @@ function ToolsMenu({ ctx }) {
2416
2417
  /* @__PURE__ */ a3("div", { class: "fnc-tools-section", children: t3.settingsScale }),
2417
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] }) })) }),
2418
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
+ ),
2419
2430
  item("fnc-btn-settings", t3.chartSettingsMore, /* @__PURE__ */ a3(SettingsIcon, {}), () => ctx.store.set({ dialog: { kind: "chartSettings" } }))
2420
2431
  ] })
2421
2432
  ] });
@@ -2700,7 +2711,10 @@ function Legend({ ctx }) {
2700
2711
  A2(() => {
2701
2712
  const el = ref.current;
2702
2713
  if (!el) return;
2703
- 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
+ };
2704
2718
  const ro = new ResizeObserver(update);
2705
2719
  ro.observe(el);
2706
2720
  return () => ro.disconnect();
@@ -2711,12 +2725,12 @@ function Legend({ ctx }) {
2711
2725
  const prev = hover ? hover.prev : bars[bars.length - 2];
2712
2726
  const fmt = (p3) => formatPrice3(p3, s3.symbol.pricePrecision);
2713
2727
  let values = null;
2714
- if (bar) {
2728
+ if (bar && (ctx.legendValues === "always" || hover)) {
2715
2729
  const cls = dirClass(bar.close - bar.open);
2716
2730
  const change = prev ? bar.close - prev.close : 0;
2717
2731
  const pct = prev && prev.close ? change / prev.close * 100 : 0;
2718
2732
  const sign = change > 0 ? "+" : "";
2719
- 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: [
2720
2734
  /* @__PURE__ */ a3("span", { children: [
2721
2735
  t3.open,
2722
2736
  " ",
@@ -3123,7 +3137,13 @@ var CSS = (
3123
3137
  border-right: 1px solid var(--fnc-border);
3124
3138
  background: var(--fnc-bg);
3125
3139
  }
3126
- .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; }
3127
3147
 
3128
3148
  /* Selected drawing: floating color/delete bar */
3129
3149
  .fnc-selbar {
@@ -3181,6 +3201,14 @@ var CSS = (
3181
3201
  .fnc-legend-title b { color: var(--fnc-text); font-weight: 700; }
3182
3202
  .fnc-legend-values { display: flex; flex-wrap: wrap; gap: 0 10px; }
3183
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
+ }
3184
3212
  /* Indicator legend rows */
3185
3213
  .fnc-ind {
3186
3214
  display: flex;
@@ -3875,6 +3903,8 @@ var CSS = (
3875
3903
  .fnc-tools-seg svg { width: 16px; height: 16px; flex: none; }
3876
3904
  .fnc-tools-layouts { display: flex; flex-direction: column; gap: 2px; padding: 0 2px 4px; }
3877
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%; }
3878
3908
  .fnc-tools-item svg { flex: none; color: var(--fnc-text-2); }
3879
3909
  .fnc-tools-item[aria-pressed='true'] { background: var(--fnc-surface); color: var(--fnc-text); }
3880
3910
 
@@ -4391,6 +4421,8 @@ function createPane(shell, config) {
4391
4421
  drawingToolbar: config.drawingToolbar,
4392
4422
  attribution: config.attribution,
4393
4423
  contextMenuItems: config.contextMenuItems,
4424
+ toolsMenuItems: config.toolsMenuItems,
4425
+ legendValues: config.legendValues ?? "always",
4394
4426
  onIndicatorsClick: config.onIndicatorsClick,
4395
4427
  slots: shell.slots,
4396
4428
  multi: config.multi,
@@ -4404,7 +4436,7 @@ function createPane(shell, config) {
4404
4436
  setSearchOpen: (open, target = "symbol") => store.set(open ? { searchOpen: true, searchTarget: target } : { searchOpen: false }),
4405
4437
  screenshot: () => {
4406
4438
  const s3 = store.get();
4407
- chart.takeScreenshot().toBlob((blob) => {
4439
+ chart.takeScreenshot({ legend: true }).toBlob((blob) => {
4408
4440
  if (!blob) return;
4409
4441
  const a4 = document.createElement("a");
4410
4442
  a4.href = URL.createObjectURL(blob);
@@ -4469,6 +4501,8 @@ function createTradingChart(container, options) {
4469
4501
  attribution = true,
4470
4502
  colorScheme = "system",
4471
4503
  contextMenuItems,
4504
+ toolsMenuItems,
4505
+ legendValues,
4472
4506
  onIndicatorsClick,
4473
4507
  theme,
4474
4508
  strings,
@@ -4501,6 +4535,8 @@ function createTradingChart(container, options) {
4501
4535
  drawingToolbar,
4502
4536
  attribution,
4503
4537
  contextMenuItems,
4538
+ toolsMenuItems,
4539
+ legendValues,
4504
4540
  onIndicatorsClick,
4505
4541
  strings
4506
4542
  });
@@ -4576,6 +4612,8 @@ function createMultiChart(container, options) {
4576
4612
  attribution = true,
4577
4613
  colorScheme = "system",
4578
4614
  contextMenuItems,
4615
+ toolsMenuItems,
4616
+ legendValues,
4579
4617
  onIndicatorsClick,
4580
4618
  theme,
4581
4619
  strings,
@@ -4650,6 +4688,8 @@ function createMultiChart(container, options) {
4650
4688
  // One badge for the whole widget, not one per pane.
4651
4689
  attribution: attribution && index === 0,
4652
4690
  contextMenuItems,
4691
+ toolsMenuItems,
4692
+ legendValues,
4653
4693
  onIndicatorsClick,
4654
4694
  multi: controls
4655
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.23",
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.23"
45
+ "@funnest/charts": "0.6.25"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"