@funnest/charts-ui 0.6.15 → 0.6.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/context.d.ts CHANGED
@@ -1,9 +1,12 @@
1
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
+ import type { Strings } from './i18n';
4
5
  import type { Store } from './store';
5
6
  export interface UiState {
6
7
  locale: Locale;
8
+ /** The app's replacements for built-in words (TradingChartOptions.strings). */
9
+ strings?: Partial<Strings> | undefined;
7
10
  symbol: SymbolInfo | null;
8
11
  resolution: Resolution;
9
12
  loading: boolean;
package/dist/i18n.d.ts CHANGED
@@ -292,6 +292,14 @@ export declare const STRINGS: {
292
292
  };
293
293
  };
294
294
  export type Strings = (typeof STRINGS)['ko'];
295
+ /**
296
+ * The widget's words: the built-in ones for its locale with the app's `strings` on top (a few
297
+ * replaced, or every one for a language of your own).
298
+ */
299
+ export declare function stringsOf(s: {
300
+ locale: Locale;
301
+ strings?: Partial<Strings> | undefined;
302
+ }): Strings;
295
303
  /**
296
304
  * '1' → 1분, '60' → 1시간, '1D' → 일 (short) / 1일 (long), '12M' → 1년.
297
305
  * Short labels are for the toolbar chips, long ones for the interval menu.
package/dist/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export { LAYOUT_GROUPS, LAYOUTS, MAX_PANES, paneCount } from './layouts';
10
10
  export type { LayoutId, SplitNode, SplitSizes } from './layouts';
11
11
  export type { ContextMenuItem, ContextMenuItems, IntervalGroup, LayoutSync, SearchCategory, SearchExchange } from './context';
12
12
  export type { ColorScheme, TradingChart, TradingChartOptions, TradingChartSlots } from './trading-chart';
13
- export { resolutionLabel } from './i18n';
13
+ export { resolutionLabel, STRINGS, stringsOf, type Strings } from './i18n';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // src/trading-chart.tsx
2
- import "@funnest/charts";
2
+ import {
3
+ browserLocale as browserLocale2
4
+ } from "@funnest/charts";
3
5
 
4
6
  // ../../node_modules/.pnpm/preact@11.2.0/node_modules/preact/dist/preact.mjs
5
7
  var n;
@@ -716,6 +718,13 @@ var STRINGS = {
716
718
  pane: "Pane"
717
719
  }
718
720
  };
721
+ var merged = /* @__PURE__ */ new WeakMap();
722
+ function stringsOf(s3) {
723
+ if (!s3.strings) return STRINGS[s3.locale];
724
+ let byLocale = merged.get(s3.strings);
725
+ if (!byLocale) merged.set(s3.strings, byLocale = {});
726
+ return byLocale[s3.locale] ??= { ...STRINGS[s3.locale], ...s3.strings };
727
+ }
719
728
  function resolutionLabel(resolution, locale, form = "short") {
720
729
  const match = /^(\d*)([SDWM]?)$/i.exec(resolution.trim());
721
730
  if (!match) return resolution;
@@ -965,7 +974,7 @@ function ToolGroup({
965
974
  onPick
966
975
  }) {
967
976
  const s3 = useStore(ctx.store);
968
- const t3 = STRINGS[s3.locale];
977
+ const t3 = stringsOf(s3);
969
978
  const [chosen, setChosen] = d2(tools[0].type);
970
979
  const armed = tools.find((d3) => d3.type === s3.drawingTool);
971
980
  const current = armed ?? tools.find((d3) => d3.type === chosen) ?? tools[0];
@@ -1008,7 +1017,7 @@ function ToolGroup({
1008
1017
  }
1009
1018
  function DrawingToolbar({ ctx }) {
1010
1019
  const s3 = useStore(ctx.store);
1011
- const t3 = STRINGS[s3.locale];
1020
+ const t3 = stringsOf(s3);
1012
1021
  const [openGroup, setOpenGroup] = d2(null);
1013
1022
  const groups = toolGroups(ctx.chart.drawingDefinitions());
1014
1023
  const pick = (tool) => {
@@ -1103,7 +1112,7 @@ function DashGlyph({ dash }) {
1103
1112
  }
1104
1113
  function SelectionBar({ ctx }) {
1105
1114
  const s3 = useStore(ctx.store);
1106
- const t3 = STRINGS[s3.locale];
1115
+ const t3 = stringsOf(s3);
1107
1116
  const d3 = s3.selection;
1108
1117
  const [offset, setOffset] = d2(() => readJson(POS_KEY, [0, 0]));
1109
1118
  const [recent, setRecent] = d2(() => readJson(RECENT_KEY, []));
@@ -1313,7 +1322,7 @@ var SCALES = [
1313
1322
  ];
1314
1323
  function SettingsDialog({ ctx }) {
1315
1324
  const { locale } = useStore(ctx.store);
1316
- const t3 = STRINGS[locale];
1325
+ const t3 = stringsOf(ctx.store.get());
1317
1326
  const [settings, setSettings] = d2(ctx.chart.settings);
1318
1327
  A2(() => ctx.chart.on("settingsChange", setSettings), [ctx.chart]);
1319
1328
  const close = () => ctx.store.set({ dialog: null });
@@ -1432,7 +1441,7 @@ function Dialog({ title, onClose, children, wide }) {
1432
1441
  var SEARCH_THRESHOLD = 8;
1433
1442
  function IndicatorPicker({ ctx }) {
1434
1443
  const { locale } = useStore(ctx.store);
1435
- const t3 = STRINGS[locale];
1444
+ const t3 = stringsOf(ctx.store.get());
1436
1445
  const [query, setQuery] = d2("");
1437
1446
  const close = () => ctx.store.set({ dialog: null });
1438
1447
  const add = (type) => {
@@ -1515,7 +1524,7 @@ function SourceSelect({ value, locale, label, onChange }) {
1515
1524
  }
1516
1525
  function IndicatorSettings({ ctx, id }) {
1517
1526
  const { locale, indicators } = useStore(ctx.store);
1518
- const t3 = STRINGS[locale];
1527
+ const t3 = stringsOf(ctx.store.get());
1519
1528
  const config = indicators.find((c3) => c3.id === id);
1520
1529
  const def = config ? ctx.chart.indicatorDefinition(config.type) ?? null : null;
1521
1530
  const hasLines = !!def?.params.some((p3) => p3.kind === "lines");
@@ -1695,7 +1704,7 @@ var GROUP_LABEL = {
1695
1704
  };
1696
1705
  function IntervalPicker({ ctx }) {
1697
1706
  const s3 = useStore(ctx.store);
1698
- const t3 = STRINGS[s3.locale];
1707
+ const t3 = stringsOf(s3);
1699
1708
  const [open, setOpen] = d2(false);
1700
1709
  const ref = T2(null);
1701
1710
  const menuRef = useKeepInView(open);
@@ -1821,6 +1830,8 @@ var DEBOUNCE_MS = 150;
1821
1830
  var RECENT_KEY2 = "recent-symbols";
1822
1831
  var FAVORITE_KEY = "favorite-symbols";
1823
1832
  var MAX_RECENT = 8;
1833
+ var FIRST_ROWS = 40;
1834
+ var MORE_ROWS = 80;
1824
1835
  var displayCode = (symbol) => symbol.slice(symbol.lastIndexOf(":") + 1);
1825
1836
  var toStored = (info) => JSON.parse(JSON.stringify({ ...info, tickSize: 0, price: void 0, changePercent: void 0, sparkline: void 0 }));
1826
1837
  function Highlight({ text, query }) {
@@ -1885,7 +1896,7 @@ function Sparkline({ values, up }) {
1885
1896
  function SymbolSearch({ ctx }) {
1886
1897
  const { locale, symbol: current, searchTarget } = useStore(ctx.store);
1887
1898
  const comparing = searchTarget === "compare";
1888
- const t3 = STRINGS[locale];
1899
+ const t3 = stringsOf(ctx.store.get());
1889
1900
  const [query, setQuery] = d2("");
1890
1901
  const [category, setCategory] = d2(ctx.searchCategories[0]?.id ?? "all");
1891
1902
  const [view, setView] = d2({ kind: "all" });
@@ -1896,8 +1907,10 @@ function SymbolSearch({ ctx }) {
1896
1907
  const [recent, setRecent] = d2(() => ctx.storage.get(RECENT_KEY2, []));
1897
1908
  const [favorites, setFavorites] = d2(() => ctx.storage.get(FAVORITE_KEY, []));
1898
1909
  const [active, setActive] = d2(0);
1910
+ const [limit, setLimit] = d2(FIRST_ROWS);
1899
1911
  const inputRef = T2(null);
1900
1912
  const listRef = T2(null);
1913
+ const endRef = T2(null);
1901
1914
  A2(() => inputRef.current?.focus(), []);
1902
1915
  A2(() => {
1903
1916
  let alive = true;
@@ -1923,7 +1936,8 @@ function SymbolSearch({ ctx }) {
1923
1936
  }, [query]);
1924
1937
  const matchCategory = ctx.searchCategories.find((c3) => c3.id === category)?.match ?? (() => true);
1925
1938
  const categoryOf = (info) => ctx.searchCategories.find((c3) => c3.id !== "all" && c3.match(info));
1926
- const isFavorite = (info) => favorites.some((f4) => f4.symbol === info.symbol);
1939
+ const favoriteSet = w2(() => new Set(favorites.map((f4) => f4.symbol)), [favorites]);
1940
+ const isFavorite = (info) => favoriteSet.has(info.symbol);
1927
1941
  const filter = (list) => list.filter((i3) => matchCategory(i3) && (!exchange || i3.exchange === exchange));
1928
1942
  const exchanges = w2(() => {
1929
1943
  const counts = /* @__PURE__ */ new Map();
@@ -1954,11 +1968,36 @@ function SymbolSearch({ ctx }) {
1954
1968
  }
1955
1969
  }
1956
1970
  const rows = [...recentRows, ...resultRows];
1971
+ const shownRows = resultRows.length > limit ? resultRows.slice(0, limit) : resultRows;
1957
1972
  const exchangeLabel = (name) => ctx.searchExchanges.find((e3) => e3.name === name)?.label?.[locale] ?? name;
1958
1973
  A2(() => setActive(0), [query, category, exchange, view, results]);
1959
1974
  A2(() => {
1960
- listRef.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView({ block: "nearest" });
1975
+ setLimit(FIRST_ROWS);
1976
+ if (listRef.current) listRef.current.scrollTop = 0;
1977
+ }, [query, category, exchange, view]);
1978
+ const hasMore = resultRows.length > limit;
1979
+ A2(() => {
1980
+ const end = endRef.current;
1981
+ if (!hasMore || !end) return;
1982
+ const io = new IntersectionObserver((entries) => entries.some((e3) => e3.isIntersecting) && setLimit((l3) => l3 + MORE_ROWS), {
1983
+ root: listRef.current,
1984
+ rootMargin: "0px 0px 600px 0px"
1985
+ });
1986
+ io.observe(end);
1987
+ return () => io.disconnect();
1988
+ }, [hasMore, limit]);
1989
+ const scrollToActive = T2(false);
1990
+ A2(() => {
1991
+ if (active - recentRows.length >= limit - 1) {
1992
+ scrollToActive.current = true;
1993
+ setLimit(active - recentRows.length + MORE_ROWS);
1994
+ } else listRef.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView({ block: "nearest" });
1961
1995
  }, [active]);
1996
+ A2(() => {
1997
+ if (!scrollToActive.current) return;
1998
+ scrollToActive.current = false;
1999
+ listRef.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView({ block: "nearest" });
2000
+ }, [limit]);
1962
2001
  const close = () => ctx.setSearchOpen(false);
1963
2002
  const save = (key, next, set) => {
1964
2003
  set(next);
@@ -2048,7 +2087,8 @@ function SymbolSearch({ ctx }) {
2048
2087
  }
2049
2088
  )
2050
2089
  ]
2051
- }
2090
+ },
2091
+ (isRecent ? "recent:" : "") + info.symbol
2052
2092
  );
2053
2093
  };
2054
2094
  const sideItem = (label, selected, onClick, extra, logoUrl) => /* @__PURE__ */ a3("button", { class: "fnc-sr-side-item", "aria-pressed": selected, onClick, children: [
@@ -2104,13 +2144,14 @@ function SymbolSearch({ ctx }) {
2104
2144
  ] }),
2105
2145
  recentRows.map((info, i3) => row2(info, i3, true)),
2106
2146
  (recentRows.length > 0 || view.kind === "favorites") && /* @__PURE__ */ a3("div", { class: "fnc-sr-section", children: view.kind === "favorites" ? t3.favorites : t3.symbols }),
2107
- loading && !results && view.kind === "all" ? Array.from({ length: 6 }, () => /* @__PURE__ */ a3("div", { class: "fnc-sr-skeleton", "aria-hidden": "true" })) : resultRows.map((info, i3) => {
2147
+ loading && !results && view.kind === "all" ? Array.from({ length: 6 }, () => /* @__PURE__ */ a3("div", { class: "fnc-sr-skeleton", "aria-hidden": "true" })) : shownRows.map((info, i3) => {
2108
2148
  const m3 = more.get(i3);
2109
2149
  return m3 ? /* @__PURE__ */ a3($, { children: [
2110
2150
  row2(info, recentRows.length + i3, false),
2111
2151
  /* @__PURE__ */ a3("button", { class: "fnc-sr-more", onClick: () => (setView({ kind: "all" }), setExchange(m3.exchange)), children: t3.showAllIn.replace("{exchange}", exchangeLabel(m3.exchange)).replace("{n}", m3.count.toLocaleString(locale === "ko" ? "ko-KR" : "en-US")) })
2112
- ] }) : row2(info, recentRows.length + i3, false);
2152
+ ] }, info.symbol) : row2(info, recentRows.length + i3, false);
2113
2153
  }),
2154
+ hasMore && /* @__PURE__ */ a3("div", { ref: endRef, class: "fnc-sr-end", "aria-hidden": "true" }),
2114
2155
  !loading && rows.length === 0 && /* @__PURE__ */ a3("div", { class: "fnc-empty", children: view.kind === "favorites" && favorites.length === 0 ? t3.noFavorites : t3.noResults })
2115
2156
  ] })
2116
2157
  ] }),
@@ -2234,7 +2275,7 @@ var SYNC_KEYS = [
2234
2275
  function LayoutSection({ ctx, multi, close }) {
2235
2276
  const s3 = useStore(ctx.store);
2236
2277
  const m3 = useStore(multi.store);
2237
- const t3 = STRINGS[s3.locale];
2278
+ const t3 = stringsOf(s3);
2238
2279
  return /* @__PURE__ */ a3($, { children: [
2239
2280
  /* @__PURE__ */ a3("div", { class: "fnc-tools-section", children: t3.layout }),
2240
2281
  /* @__PURE__ */ a3("div", { class: "fnc-tools-layouts", children: LAYOUT_GROUPS.map((group) => /* @__PURE__ */ a3("div", { class: "fnc-layout-row", children: [
@@ -2273,7 +2314,7 @@ function LayoutSection({ ctx, multi, close }) {
2273
2314
  }
2274
2315
  function ToolsMenu({ ctx }) {
2275
2316
  const s3 = useStore(ctx.store);
2276
- const t3 = STRINGS[s3.locale];
2317
+ const t3 = stringsOf(s3);
2277
2318
  const [open, setOpen] = d2(false);
2278
2319
  const [scale, setScale] = d2(ctx.chart.settings.priceScale);
2279
2320
  const ref = T2(null);
@@ -2370,7 +2411,7 @@ function ToolsMenu({ ctx }) {
2370
2411
  // src/components/toolbar.tsx
2371
2412
  function Toolbar({ ctx }) {
2372
2413
  const s3 = useStore(ctx.store);
2373
- const t3 = STRINGS[s3.locale];
2414
+ const t3 = stringsOf(s3);
2374
2415
  const canSearch = typeof ctx.datafeed.searchSymbols === "function";
2375
2416
  return /* @__PURE__ */ a3("div", { class: "fnc-toolbar", role: "toolbar", children: [
2376
2417
  /* @__PURE__ */ a3(
@@ -2438,7 +2479,7 @@ function Toolbar({ ctx }) {
2438
2479
  }
2439
2480
 
2440
2481
  // src/pane.tsx
2441
- import { createChart } from "@funnest/charts";
2482
+ import { browserLocale, createChart } from "@funnest/charts";
2442
2483
 
2443
2484
  // src/components/overlay.tsx
2444
2485
  import { formatCompact, formatPrice as formatPrice3 } from "@funnest/charts";
@@ -2448,7 +2489,7 @@ import { formatPrice as formatPrice2 } from "@funnest/charts";
2448
2489
  var MARGIN2 = 8;
2449
2490
  function ContextMenu({ ctx }) {
2450
2491
  const s3 = useStore(ctx.store);
2451
- const t3 = STRINGS[s3.locale];
2492
+ const t3 = stringsOf(s3);
2452
2493
  const [target, setTarget] = d2(null);
2453
2494
  const ref = T2(null);
2454
2495
  A2(() => ctx.chart.on("contextMenu", setTarget), [ctx.chart]);
@@ -2527,7 +2568,7 @@ var ZOOM_STEP = 1.25;
2527
2568
  var CONTROL_OFFSET = 52;
2528
2569
  var dirClass = (v3) => v3 > 0 ? "fnc-up" : v3 < 0 ? "fnc-down" : "fnc-flat";
2529
2570
  function IndicatorItem({ ctx, item }) {
2530
- const t3 = STRINGS[ctx.store.get().locale];
2571
+ const t3 = stringsOf(ctx.store.get());
2531
2572
  return /* @__PURE__ */ a3(
2532
2573
  "div",
2533
2574
  {
@@ -2585,7 +2626,7 @@ function IndicatorItem({ ctx, item }) {
2585
2626
  }
2586
2627
  function Legend({ ctx }) {
2587
2628
  const s3 = useStore(ctx.store);
2588
- const t3 = STRINGS[s3.locale];
2629
+ const t3 = stringsOf(s3);
2589
2630
  const [hover, setHover] = d2(null);
2590
2631
  const [, refresh] = _2((n2) => n2 + 1, 0);
2591
2632
  const [collapsed, setCollapsed] = d2(false);
@@ -2697,7 +2738,7 @@ function Legend({ ctx }) {
2697
2738
  }
2698
2739
  function NavControls({ ctx }) {
2699
2740
  const s3 = useStore(ctx.store);
2700
- const t3 = STRINGS[s3.locale];
2741
+ const t3 = stringsOf(s3);
2701
2742
  const [showButtons, setShowButtons] = d2(ctx.chart.settings.navigationButtons);
2702
2743
  A2(() => ctx.chart.on("settingsChange", (settings) => setShowButtons(settings.navigationButtons)), [ctx.chart]);
2703
2744
  const { plotWidth, panesHeight } = s3.layout;
@@ -2757,7 +2798,7 @@ function PaneMaximize({ ctx, multi }) {
2757
2798
  const s3 = useStore(ctx.store);
2758
2799
  const m3 = useStore(multi.store);
2759
2800
  if (s3.layout.plotWidth <= 0 || m3.narrow || m3.layout === "1" && m3.maximized === null) return null;
2760
- const t3 = STRINGS[s3.locale];
2801
+ const t3 = stringsOf(s3);
2761
2802
  const maximized = m3.maximized !== null;
2762
2803
  const label = maximized ? t3.restorePane : t3.maximizePane;
2763
2804
  return /* @__PURE__ */ a3(
@@ -2783,7 +2824,7 @@ var REPLAY_SPEEDS = [
2783
2824
  ];
2784
2825
  function ReplayBar({ ctx }) {
2785
2826
  const s3 = useStore(ctx.store);
2786
- const t3 = STRINGS[s3.locale];
2827
+ const t3 = stringsOf(s3);
2787
2828
  const r3 = s3.replay;
2788
2829
  const { plotWidth, panesHeight } = s3.layout;
2789
2830
  if (plotWidth <= 0 || !r3.active && !r3.selecting) return null;
@@ -2829,7 +2870,7 @@ function Overlay({ ctx }) {
2829
2870
  }
2830
2871
 
2831
2872
  // src/shell.ts
2832
- import { darkTheme, lightTheme } from "@funnest/charts";
2873
+ import { darkTheme, directionColors, lightTheme } from "@funnest/charts";
2833
2874
 
2834
2875
  // src/storage.ts
2835
2876
  function createStorage(prefix) {
@@ -3255,6 +3296,7 @@ var CSS = (
3255
3296
  .fnc-sr-spark-slot { display: inline-flex; flex: none; width: 72px; height: 24px; }
3256
3297
  .fnc-sr-more { display: block; width: calc(100% - 16px); margin: 2px 8px 8px; padding: 8px 12px; border-radius: var(--fnc-radius); background: var(--fnc-surface); color: var(--fnc-text-2); font-size: 13px; font-weight: 600; text-align: left; }
3257
3298
  .fnc-sr-more:hover { color: var(--fnc-text); }
3299
+ .fnc-sr-end { height: 1px; }
3258
3300
  .fnc-sr-side-item:hover { background: var(--fnc-surface); }
3259
3301
  .fnc-sr-side-item[aria-pressed='true'] { background: var(--fnc-surface); font-weight: 700; }
3260
3302
  .fnc-chip-token {
@@ -4054,7 +4096,7 @@ function createShell(container, options) {
4054
4096
  let themePatch = { ...options.theme };
4055
4097
  let panes = () => [];
4056
4098
  const isDark = () => scheme === "system" ? !!darkQuery?.matches : scheme === "dark";
4057
- const resolveTheme = () => ({ ...isDark() ? darkTheme : lightTheme, ...themePatch });
4099
+ const resolveTheme = () => ({ ...isDark() ? darkTheme : lightTheme, ...directionColors(options.direction), ...themePatch });
4058
4100
  let theme = resolveTheme();
4059
4101
  const root = div("fnc-root", container);
4060
4102
  const applyScheme = () => {
@@ -4227,7 +4269,8 @@ function createPane(shell, config) {
4227
4269
  canvasVars(chart.settings);
4228
4270
  chart.on("settingsChange", canvasVars);
4229
4271
  const store = new Store({
4230
- locale: config.chartOptions.locale ?? "ko",
4272
+ locale: config.chartOptions.locale ?? browserLocale(),
4273
+ strings: config.strings,
4231
4274
  symbol: null,
4232
4275
  resolution: config.resolution,
4233
4276
  loading: false,
@@ -4369,12 +4412,16 @@ function createTradingChart(container, options) {
4369
4412
  contextMenuItems,
4370
4413
  onIndicatorsClick,
4371
4414
  theme,
4415
+ strings,
4416
+ direction,
4372
4417
  ...chartOptions
4373
4418
  } = options;
4374
- const displayTimezone = defaultDisplayTimezone(chartOptions.locale ?? "ko", chartOptions.displayTimezone);
4419
+ const locale = chartOptions.locale ??= browserLocale2();
4420
+ const displayTimezone = defaultDisplayTimezone(locale, chartOptions.displayTimezone);
4375
4421
  const shell = createShell(container, {
4376
4422
  colorScheme,
4377
4423
  theme,
4424
+ direction: direction ?? (locale === "ko" ? "kr" : "global"),
4378
4425
  storageKey,
4379
4426
  drawingToolbar,
4380
4427
  favoriteIntervals: favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS,
@@ -4395,7 +4442,8 @@ function createTradingChart(container, options) {
4395
4442
  drawingToolbar,
4396
4443
  attribution,
4397
4444
  contextMenuItems,
4398
- onIndicatorsClick
4445
+ onIndicatorsClick,
4446
+ strings
4399
4447
  });
4400
4448
  shell.main.insertBefore(pane.stage, shell.slots.bottom);
4401
4449
  shell.setPanes(() => [pane]);
@@ -4411,9 +4459,9 @@ function createTradingChart(container, options) {
4411
4459
  const current = pane.store.get().symbol;
4412
4460
  return current ? pane.load(current.symbol, res) : Promise.resolve();
4413
4461
  },
4414
- setLocale: (locale) => {
4415
- pane.chart.setLocale(locale);
4416
- pane.store.set({ locale });
4462
+ setLocale: (locale2) => {
4463
+ pane.chart.setLocale(locale2);
4464
+ pane.store.set({ locale: locale2 });
4417
4465
  },
4418
4466
  save: () => pane.chart.save(),
4419
4467
  load: (state) => pane.chart.load(state),
@@ -4430,11 +4478,11 @@ function createTradingChart(container, options) {
4430
4478
  }
4431
4479
 
4432
4480
  // src/multi-chart.tsx
4433
- import { parseChartState } from "@funnest/charts";
4481
+ import { browserLocale as browserLocale3, parseChartState } from "@funnest/charts";
4434
4482
  var DEFAULT_SYNC = { crosshair: true, time: false, symbol: false, interval: false };
4435
4483
  function PaneTab({ pane, index, active, onSelect }) {
4436
4484
  const s3 = useStore(pane.store);
4437
- const t3 = STRINGS[s3.locale];
4485
+ const t3 = stringsOf(s3);
4438
4486
  return /* @__PURE__ */ a3("button", { class: "fnc-pane-tab", role: "tab", "aria-selected": active, onClick: onSelect, children: [
4439
4487
  /* @__PURE__ */ a3("span", { class: "fnc-pane-tab-num", children: index + 1 }),
4440
4488
  s3.symbol?.name ?? `${t3.pane} ${index + 1}`,
@@ -4471,13 +4519,17 @@ function createMultiChart(container, options) {
4471
4519
  contextMenuItems,
4472
4520
  onIndicatorsClick,
4473
4521
  theme,
4522
+ strings,
4523
+ direction,
4474
4524
  ...chartOptions
4475
4525
  } = options;
4476
4526
  if (paneSpecs.length === 0) throw new Error("funnest-charts: createMultiChart needs at least one pane");
4477
- const displayTimezone = defaultDisplayTimezone(chartOptions.locale ?? "ko", chartOptions.displayTimezone);
4527
+ const locale = chartOptions.locale ??= browserLocale3();
4528
+ const displayTimezone = defaultDisplayTimezone(locale, chartOptions.displayTimezone);
4478
4529
  const shell = createShell(container, {
4479
4530
  colorScheme,
4480
4531
  theme,
4532
+ direction: direction ?? (locale === "ko" ? "kr" : "global"),
4481
4533
  storageKey,
4482
4534
  drawingToolbar,
4483
4535
  favoriteIntervals: favoriteIntervals ?? DEFAULT_FAVORITE_INTERVALS,
@@ -4776,10 +4828,10 @@ function createMultiChart(container, options) {
4776
4828
  const current = pane.store.get().symbol;
4777
4829
  return current ? pane.load(current.symbol, res) : Promise.resolve();
4778
4830
  },
4779
- setLocale: (locale) => {
4831
+ setLocale: (locale2) => {
4780
4832
  for (const p3 of panes) {
4781
- p3.chart.setLocale(locale);
4782
- p3.store.set({ locale });
4833
+ p3.chart.setLocale(locale2);
4834
+ p3.store.set({ locale: locale2 });
4783
4835
  }
4784
4836
  },
4785
4837
  setTheme: shell.setTheme,
@@ -4824,8 +4876,10 @@ export {
4824
4876
  LAYOUTS,
4825
4877
  LAYOUT_GROUPS,
4826
4878
  MAX_PANES,
4879
+ STRINGS,
4827
4880
  createMultiChart,
4828
4881
  createTradingChart,
4829
4882
  paneCount,
4830
- resolutionLabel
4883
+ resolutionLabel,
4884
+ stringsOf
4831
4885
  };
package/dist/pane.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { type Chart, type ChartOptions, type ChartType, type Datafeed, type Resolution } from '@funnest/charts';
3
3
  import type { ContextMenuItems, IntervalGroup, MultiControls, SearchCategory, SearchExchange, UiState, WidgetContext } from './context';
4
4
  import { type Shell } from './shell';
5
+ import type { Strings } from './i18n';
5
6
  import { Store } from './store';
6
7
  export interface PaneConfig {
7
8
  datafeed: Datafeed;
@@ -21,6 +22,7 @@ export interface PaneConfig {
21
22
  contextMenuItems?: ContextMenuItems | undefined;
22
23
  onIndicatorsClick?: ((chart: Chart) => void) | undefined;
23
24
  multi?: MultiControls | undefined;
25
+ strings?: Partial<Strings> | undefined;
24
26
  }
25
27
  /**
26
28
  * One chart with its legend/navigation overlay and UI state. The toolbar and drawing bar
package/dist/shell.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Resolution, type ThemeTokens } from '@funnest/charts';
1
+ import { type DirectionPreset, type Resolution, type ThemeTokens } from '@funnest/charts';
2
2
  import type { UiState } from './context';
3
3
  import { type WidgetStorage } from './storage';
4
4
  import type { Store } from './store';
@@ -35,6 +35,8 @@ export interface ShellPane {
35
35
  export interface ShellOptions {
36
36
  colorScheme: ColorScheme;
37
37
  theme: Partial<ThemeTokens> | undefined;
38
+ /** Up/down colors under `theme` (TradingChartOptions.direction, resolved from the locale). */
39
+ direction: DirectionPreset;
38
40
  storageKey: string | null;
39
41
  drawingToolbar: boolean;
40
42
  favoriteIntervals: readonly Resolution[];
@@ -1,6 +1,7 @@
1
1
  /** @jsxImportSource preact */
2
- import { type Chart, type ChartOptions, type ChartType, type Datafeed, type Locale, type ChartState, type Resolution, type ThemeTokens } from '@funnest/charts';
2
+ import { type Chart, type ChartOptions, type ChartType, type Datafeed, type DirectionPreset, type Locale, type ChartState, type Resolution, type ThemeTokens } from '@funnest/charts';
3
3
  import type { ContextMenuItems, IntervalGroup, SearchCategory, SearchExchange } from './context';
4
+ import type { Strings } from './i18n';
4
5
  import { type ColorScheme, type TradingChartSlots } from './shell';
5
6
  export type { ColorScheme, TradingChartSlots } from './shell';
6
7
  /** Interval menu: 분 | 시간 | 일. '12M' is shown as 1년. */
@@ -62,6 +63,21 @@ export interface TradingChartOptions extends Omit<ChartOptions, 'datafeed'> {
62
63
  * indicator browser). Called with the pane's chart; add indicators with chart.addIndicator().
63
64
  */
64
65
  onIndicatorsClick?: (chart: Chart) => void;
66
+ /**
67
+ * Language of the toolbar, menus and dates. Default: Korean when the browser is set to Korean,
68
+ * English otherwise.
69
+ */
70
+ locale?: Locale;
71
+ /**
72
+ * Replace built-in words: a few (`{ indicators: 'Studies' }`) or all of them for a language of
73
+ * your own (start from `STRINGS.en`). Applied over whichever locale is active.
74
+ */
75
+ strings?: Partial<Strings>;
76
+ /**
77
+ * Up/down colors: 'kr' red up · blue down, 'global' green up · red down. Default follows the
78
+ * locale ('kr' for Korean, 'global' otherwise). `theme.up` / `theme.down` still win.
79
+ */
80
+ direction?: DirectionPreset;
65
81
  }
66
82
  export interface TradingChart {
67
83
  /** 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.15",
3
+ "version": "0.6.17",
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.15"
45
+ "@funnest/charts": "0.6.17"
46
46
  },
47
47
  "devDependencies": {
48
48
  "preact": "^11.2.0"